Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/react-core/src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ export interface MenuProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'r
ouiaId?: number | string;
/** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */
ouiaSafe?: boolean;
/** @beta Determines the accessible role of the menu. For a non-checkbox menu that can have
* one or more items selected, pass in "listbox". */
role?: string;
}

export interface MenuState {
Expand All @@ -89,7 +92,8 @@ class MenuBase extends React.Component<MenuProps, MenuState> {
ouiaSafe: true,
isRootMenu: true,
isPlain: false,
isScrollable: false
isScrollable: false,
role: 'menu'
};

constructor(props: MenuProps) {
Expand Down Expand Up @@ -272,6 +276,7 @@ class MenuBase extends React.Component<MenuProps, MenuState> {
innerRef,
isRootMenu,
activeMenu,
role,
/* eslint-enable @typescript-eslint/no-unused-vars */
...props
} = this.props;
Expand All @@ -292,7 +297,8 @@ class MenuBase extends React.Component<MenuProps, MenuState> {
onGetMenuHeight,
flyoutRef: this.state.flyoutRef,
setFlyoutRef: flyoutRef => this.setState({ flyoutRef }),
disableHover: this.state.disableHover
disableHover: this.state.disableHover,
role
}}
>
{isRootMenu && (
Expand Down
4 changes: 3 additions & 1 deletion packages/react-core/src/components/Menu/MenuContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const MenuContext = React.createContext<{
flyoutRef?: React.Ref<HTMLLIElement>;
setFlyoutRef?: (ref: React.Ref<HTMLLIElement>) => void;
disableHover?: boolean;
role?: string;
}>({
menuId: null,
parentMenu: null,
Expand All @@ -34,7 +35,8 @@ export const MenuContext = React.createContext<{
onGetMenuHeight: () => null,
flyoutRef: null,
setFlyoutRef: () => null,
disableHover: false
disableHover: false,
role: 'menu'
});

export const MenuItemContext = React.createContext<{
Expand Down
7 changes: 5 additions & 2 deletions packages/react-core/src/components/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ const MenuItemBase: React.FunctionComponent<MenuItemProps> = ({
onDrillOut,
flyoutRef,
setFlyoutRef,
disableHover
disableHover,
role: menuRole
} = React.useContext(MenuContext);
let Component = (to ? 'a' : component) as any;
if (hasCheck && !to) {
Expand Down Expand Up @@ -290,6 +291,7 @@ const MenuItemBase: React.FunctionComponent<MenuItemProps> = ({
setFlyoutRef(null);
}
};
const isSelectMenu = menuRole === 'listbox';

return (
<li
Expand All @@ -316,7 +318,8 @@ const MenuItemBase: React.FunctionComponent<MenuItemProps> = ({
className={css(styles.menuItem, getIsSelected() && !hasCheck && styles.modifiers.selected, className)}
aria-current={getAriaCurrent()}
{...(!hasCheck && { disabled: isDisabled })}
{...(!hasCheck && !flyoutMenu && { role: 'menuitem' })}
{...(!hasCheck && !flyoutMenu && { role: isSelectMenu ? 'option' : 'menuitem' })}
{...(!hasCheck && !flyoutMenu && isSelectMenu && { 'aria-selected': getIsSelected() })}
ref={innerRef}
{...(!hasCheck && {
onClick: (event: React.KeyboardEvent | React.MouseEvent) => {
Expand Down
25 changes: 20 additions & 5 deletions packages/react-core/src/components/Menu/MenuList.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import * as React from 'react';
import styles from '@patternfly/react-styles/css/components/Menu/menu';
import { css } from '@patternfly/react-styles';
import { MenuContext } from './MenuContext';

export interface MenuListProps extends React.HTMLProps<HTMLUListElement> {
/** Anything that can be rendered inside of menu list */
children: React.ReactNode;
/** Additional classes added to the menu list */
className?: string;
/** @beta Indicates to assistive technologies whether more than one item can be selected
* for a non-checkbox menu. Only applies when the menu's role is "listbox".
*/
isAriaMultiselectable?: boolean;
}

export const MenuList: React.FunctionComponent<MenuListProps> = ({
children = null,
className,
isAriaMultiselectable = false,
...props
}: MenuListProps) => (
<ul role="menu" className={css(styles.menuList, className)} {...props}>
{children}
</ul>
);
}: MenuListProps) => {
const { role } = React.useContext(MenuContext);

return (
<ul
role={role}
{...(role === 'listbox' && { 'aria-multiselectable': isAriaMultiselectable })}
className={css(styles.menuList, className)}
{...props}
>
{children}
</ul>
);
};
MenuList.displayName = 'MenuList';
30 changes: 17 additions & 13 deletions packages/react-core/src/components/Menu/examples/Menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ A menu may contain multiple variations of `<MenuItem>` components. The following

- Use the `itemId` property to link to callbacks. In this example, the `onSelect` property logs information to the console when a menu item is selected. In practice, specific actions can be linked to `onSelect` callbacks.
- Use the `to` property to direct users to other resources or webpages after selecting a menu item, and the `onClick` property to pass in a callback for specific menu items.
- Use the `isDisabled` property to disable a menu item.
- Use the `isPlain` property to remove the outer box shadow and style the menu plainly instead.
- Use the `isDisabled` property to disable a menu item.
- Use the `isPlain` property to remove the outer box shadow and style the menu plainly instead.

```ts file="MenuBasic.tsx"
```
Expand All @@ -46,11 +46,11 @@ Use the `icon` property to add a familiar icon before a `<MenuItem>` to accelera

### With actions

To connect a menu item to an action icon, add a `<MenuItemAction>` to a `<MenuItem>`, and use the `icon` property to load an easily recognizable icon.
To connect a menu item to an action icon, add a `<MenuItemAction>` to a `<MenuItem>`, and use the `icon` property to load an easily recognizable icon.

To trigger an action when any menu action icon is selected, pass a callback to the `onActionClick` property of the `<Menu>`. The following example logs to the console any time any action icon is selected.
To trigger an action when any menu action icon is selected, pass a callback to the `onActionClick` property of the `<Menu>`. The following example logs to the console any time any action icon is selected.

To trigger an action when a specific item's action icon is selected, pass in the `onClick` property to that `<MenuItemAction>`. The following example logs "clicked on code icon" to the console when the "code" icon is selected.
To trigger an action when a specific item's action icon is selected, pass in the `onClick` property to that `<MenuItemAction>`. The following example logs "clicked on code icon" to the console when the "code" icon is selected.

```ts file="MenuWithActions.tsx"
```
Expand All @@ -64,7 +64,7 @@ Use the `to` property to add a link to a `<MenuItem>` that directs users to a ne

### With descriptions

Use the `description` property to add short descriptive text below any menu item that needs additional context.
Use the `description` property to add short descriptive text below any menu item that needs additional context.

```ts file="MenuWithDescription.tsx"
```
Expand All @@ -85,14 +85,14 @@ Add a `<MenuFooter>` that contains separate, but related actions at the bottom o

### Separated items

Use a [divider](/components/divider) to visually separate `<MenuContent>`. Use the `component` property to specify the type of divider component to use.
Use a [divider](/components/divider) to visually separate `<MenuContent>`. Use the `component` property to specify the type of divider component to use.

```ts file="MenuWithSeparators.tsx"
```

### Titled groups of items

Add a `<MenuGroup>` to organize `<MenuContent>` and use the `label` property to title a group of menu items. Use the `labelHeadingLevel` property to assign a heading level to the menu group label.
Add a `<MenuGroup>` to organize `<MenuContent>` and use the `label` property to title a group of menu items. Use the `labelHeadingLevel` property to assign a heading level to the menu group label.

```ts file="MenuWithTitledGroups.tsx"
```
Expand All @@ -115,12 +115,16 @@ A [search input](/components/search-input) component can be placed within `<Menu

The following example demonstrates a single option select menu that persists a selected menu item. Use the `selected` property on the `<Menu>` to label a selected item with a checkmark. You can also use the `isSelected` property on a `<MenuItem>` to indicate that it is selected.

You must also use the `role` property on the `<Menu>` with a value of `"listbox"` when using a non-checkbox select menu.

```ts file="MenuOptionSingleSelect.tsx"
```

### Option multi select menu

To persist multiple selections that a user makes, use a multiple option select menu. To enable multi select, pass an array containing each selected `itemId` to the `selected` property.
To persist multiple selections that a user makes, use a multiple option select menu. To enable multi select, pass an array containing each selected `itemId` to the `selected` property on the `<Menu>`, and pass the `isAriaMultiselectable` property on the `<MenuList>`.

Similar to a single select menu, you must also pass `role="listbox"` to the `<Menu>`.

```ts file="MenuOptionMultiSelect.tsx"
```
Expand Down Expand Up @@ -150,12 +154,12 @@ In this example, 3 additional menu items are revealed each time the "view more"

### With drilldown

Use a drilldown menu to contain different levels of menu items. When a parent menu item (an item that has a submenu of children) is selected, the menu is replaced with the children items.
Use a drilldown menu to contain different levels of menu items. When a parent menu item (an item that has a submenu of children) is selected, the menu is replaced with the children items.

- To indicate that a menu contains a drilldown, use the `containsDrilldown` property.
- To indicate the path of drilled-in menu item ids, use the `drilldownItemPath` property.
- To indicate the path of drilled-in menu item ids, use the `drilldownItemPath` property.
- Pass in an array of drilled-in menus with the `drilledInMenus` property.
- Use the `onDrillIn` and `onDrillOut` properties to contain callbacks for drilling into and drilling out of a submenu, respectively.
- Use the `onDrillIn` and `onDrillOut` properties to contain callbacks for drilling into and drilling out of a submenu, respectively.
- To account for updated heights as menus drill in and out of use, use the `onGetMenuHeight` property. When starting from a drilled-in state, the `onGetMenuHeight` property must define the height of the root menu.

```ts file="./MenuWithDrilldown.tsx" isBeta
Expand Down Expand Up @@ -187,4 +191,4 @@ To control the height of a menu, use the `maxMenuHeight` property. Selecting the
### With drilldown and inline filter

```ts file="MenuFilterDrilldown.tsx"
```
```
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ export const MenuOptionMultiSelect: React.FunctionComponent = () => {
};

return (
<Menu onSelect={onSelect} activeItemId={0} selected={selectedItems}>
<Menu role="listbox" onSelect={onSelect} activeItemId={0} selected={selectedItems}>
<MenuContent>
<MenuList>
<MenuList isAriaMultiselectable aria-label="Menu multi select example">
<MenuItem itemId={0}>Option 1</MenuItem>
<MenuItem itemId={1}>Option 2</MenuItem>
<MenuItem icon={<TableIcon aria-hidden />} itemId={2}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export const MenuOptionSingleSelect: React.FunctionComponent = () => {
};

return (
<Menu onSelect={onSelect} activeItemId={activeItem} selected={selectedItem}>
<Menu role="listbox" onSelect={onSelect} activeItemId={activeItem} selected={selectedItem}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we note the use of listbox in the text above these two updated examples? since erin has written such beautiful documentation for our Menu examples 😊

<MenuContent>
<MenuList>
<MenuList aria-label="Menu single select example">
<MenuItem itemId={0}>Option 1</MenuItem>
<MenuItem itemId={1}>Option 2</MenuItem>
<MenuItem icon={<TableIcon aria-hidden />} itemId={2}>
Expand Down
25 changes: 12 additions & 13 deletions packages/react-core/src/next/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,28 +58,27 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
const menuRef = (innerRef as React.RefObject<HTMLDivElement>) || localMenuRef;
React.useEffect(() => {
const handleMenuKeys = (event: KeyboardEvent) => {
if (!isOpen && toggleRef.current?.contains(event.target as Node)) {
// toggle was clicked open, focus on first menu item
if (event.key === 'Enter') {
setTimeout(() => {
const firstElement = menuRef.current.querySelector('li > button:not(:disabled)');
firstElement && (firstElement as HTMLElement).focus();
}, 0);
}
}
// Close the menu on tab or escape if onOpenChange is provided
if (
(isOpen && onOpenChange && menuRef.current?.contains(event.target as Node)) ||
toggleRef.current?.contains(event.target as Node)
) {
if (event.key === 'Escape' || event.key === 'Tab') {
onOpenChange(!isOpen);
onOpenChange(false);
toggleRef.current?.focus();
}
}
};

const handleClickOutside = (event: MouseEvent) => {
const handleClick = (event: MouseEvent) => {
// toggle was clicked open via keyboard, focus on first menu item
if (isOpen && toggleRef.current?.contains(event.target as Node) && event.detail === 0) {
setTimeout(() => {
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled),li input:not(:disabled)');
firstElement && (firstElement as HTMLElement).focus();
}, 0);
}

// If the event is not on the toggle and onOpenChange callback is provided, close the menu
if (isOpen && onOpenChange && !toggleRef?.current?.contains(event.target as Node)) {
if (isOpen && !menuRef.current?.contains(event.target as Node)) {
Expand All @@ -89,11 +88,11 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
};

window.addEventListener('keydown', handleMenuKeys);
window.addEventListener('click', handleClickOutside);
window.addEventListener('click', handleClick);

return () => {
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClickOutside);
window.removeEventListener('click', handleClick);
};
}, [isOpen, menuRef, onOpenChange]);

Expand Down
29 changes: 16 additions & 13 deletions packages/react-core/src/next/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface SelectProps extends MenuProps, OUIAProps {
innerRef?: React.Ref<HTMLDivElement>;
/** z-index of the select menu */
zIndex?: number;
/** @beta Determines the accessible role of the select. For a checkbox select pass in "menu". */
role?: string;
}

const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
Expand All @@ -42,6 +44,7 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
minWidth,
innerRef,
zIndex = 9999,
role = 'listbox',
...props
}: SelectProps & OUIAProps) => {
const localMenuRef = React.useRef<HTMLDivElement>();
Expand All @@ -51,28 +54,27 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
const menuRef = (innerRef as React.RefObject<HTMLDivElement>) || localMenuRef;
React.useEffect(() => {
const handleMenuKeys = (event: KeyboardEvent) => {
if (!isOpen && toggleRef.current?.contains(event.target as Node)) {
// toggle was clicked open, focus on first menu item
if (event.key === 'Enter' || event.key === 'Space') {
setTimeout(() => {
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled),li input:not(:disabled)');
firstElement && (firstElement as HTMLElement).focus();
}, 0);
}
}
// Close the menu on tab or escape if onOpenChange is provided
if (
(isOpen && onOpenChange && menuRef.current?.contains(event.target as Node)) ||
toggleRef.current?.contains(event.target as Node)
) {
if (event.key === 'Escape' || event.key === 'Tab') {
onOpenChange(!isOpen);
onOpenChange(false);
toggleRef.current?.focus();
}
}
};

const handleClickOutside = (event: MouseEvent) => {
const handleClick = (event: MouseEvent) => {
// toggle was clicked open via keyboard, focus on first menu item
if (isOpen && toggleRef.current?.contains(event.target as Node) && event.detail === 0) {
setTimeout(() => {
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled),li input:not(:disabled)');
firstElement && (firstElement as HTMLElement).focus();
}, 0);
}

// If the event is not on the toggle and onOpenChange callback is provided, close the menu
if (isOpen && onOpenChange && !toggleRef?.current?.contains(event.target as Node)) {
if (isOpen && !menuRef.current?.contains(event.target as Node)) {
Expand All @@ -82,16 +84,17 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
};

window.addEventListener('keydown', handleMenuKeys);
window.addEventListener('click', handleClickOutside);
window.addEventListener('click', handleClick);

return () => {
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClickOutside);
window.removeEventListener('click', handleClick);
};
}, [isOpen, menuRef, onOpenChange]);

const menu = (
<Menu
role={role}
className={css(className)}
ref={menuRef}
onSelect={(event, itemId) => onSelect(event, itemId)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ export interface SelectListProps extends MenuListProps {
children: React.ReactNode;
/** Classes applied to root element of select list */
className?: string;
/** @beta Indicates to assistive technologies whether more than one item can be selected
* for a non-checkbox select.
*/
isAriaMultiselectable?: boolean;
}

export const SelectList: React.FunctionComponent<MenuListProps> = ({
children,
className,
isAriaMultiselectable = false,
...props
}: SelectListProps) => (
<MenuList className={css(className)} {...props}>
<MenuList isAriaMultiselectable={isAriaMultiselectable} className={css(className)} {...props}>
{children}
</MenuList>
);
Expand Down
Loading