useMenuTrigger
Provides the behavior and accessibility implementation for a menu trigger.
install | yarn add @react-aria/menu |
---|---|
version | 3.0.0-alpha.1 |
usage | import {useMenuTrigger} from '@react-aria/menu' |
API#
useMenuTrigger(
props: MenuTriggerAriaProps,
state: MenuTriggerState,
ref: RefObject<HTMLElement>
): MenuTriggerAria
Features#
There is no native element to implement a menu in HTML that is widely supported. useMenuTrigger
combined with useMenu helps achieve accessible menu components that can be styled as needed.
- Exposed to assistive technology as a button with a
menu
popup using ARIA (combined with useMenu) - Support for mouse, touch, and keyboard interactions
- Keyboard support for opening the menu using the arrow keys, including automatically focusing the first or last item accordingly
Anatomy#
A menu trigger consists of a button or other trigger element combined with a popup menu. It should be combined with useButton and useMenu, which handle the implementation of the button and popup menu respectively.
useMenuTrigger
returns props that you should spread onto the appropriate element:
Name | Type | Description |
menuTriggerProps | AriaButtonProps | Props for the menu trigger element. |
menuProps | HTMLAttributes<HTMLElement> | Props for the menu. |
State is managed by the useMenuTriggerState
hook from @react-stately/menu
. The state object should be passed as an option to useMenuTrigger
Example#
This example shows how to build a menu button
using useMenuTrigger
, useButton
,
and useMenu
.
The menu popup uses useMenu
and useMenuItem
to render the menu and
its items. In addition, a <FocusScope
>
is used to automatically restore focus to the trigger when the menu closes. A
hidden <DismissButton
>
is added at the start and end of the menu to allow screen reader users to dismiss it easily.
This example does not do any advanced popover positioning or portaling to escape its visual container.
See useOverlayTrigger for an example of how to implement this
using useOverlayPosition
.
In addition, see useMenu for examples of menu item groups, and more complex item content.
import {useMenuTriggerState} from '@react-stately/menu';
import {useButton} from '@react-aria/button';
import {useMenu useMenuItem} from '@react-aria/menu';
import {useTreeState} from '@react-stately/tree';
import {Item} from '@react-stately/collections';
import {mergeProps} from '@react-aria/utils';
import {FocusScope} from '@react-aria/focus';
import {useFocus} from '@react-aria/interactions';
import {useOverlay DismissButton} from '@react-aria/overlays';
function MenuButton(props) {
// Create state based on the incoming props
let state = useMenuTriggerState(props);
// Get props for the menu trigger and menu elements
let ref = ReactuseRef();
let {menuTriggerProps menuProps} = useMenuTrigger({} state ref);
// Get props for the button based on the trigger props from useMenuTrigger
let {buttonProps} = useButton(menuTriggerProps ref);
return (
<div style={position: 'relative' display: 'inline-block'}>
<button
...buttonProps
ref= ref
style={height: 30 fontSize: 14}>
propslabel
<span aria-hidden="true" style={paddingLeft: 5}>▼</span>
</button>
stateisOpen &&
<MenuPopup
...props
domProps= menuProps
autoFocus= statefocusStrategy
onClose=() => stateclose() />
</div>
);
}
function MenuPopup(props) {
// Create menu state based on the incoming props
let state = useTreeState({...props selectionMode: 'none'});
// Get props for the menu element
let ref = ReactuseRef();
let {menuProps} = useMenu(props state ref);
// Handle events that should cause the menu to close,
// e.g. blur, clicking outside, or pressing the escape key.
let overlayRef = ReactuseRef();
let {overlayProps} = useOverlay({
onClose: propsonClose
shouldCloseOnBlur: true
isOpen: true
isDismissable: true
} overlayRef);
// Wrap in <FocusScope> so that focus is restored back to the
// trigger when the menu is closed. In addition, add hidden
// <DismissButton> components at the start and end of the list
// to allow screen reader users to dismiss the popup easily.
return (
<FocusScope restoreFocus>
<div ...overlayProps ref= overlayRef>
<DismissButton onDismiss= propsonClose />
<ul
...mergeProps(menuProps propsdomProps)
ref= ref
style={
position: 'absolute'
width: '100%'
margin: '4px 0 0 0'
padding: 0
listStyle: 'none'
border: '1px solid gray'
background: 'lightgray'
}>
[...statecollection]map(item => (
<MenuItem
key= itemkey
item= item
state= state
onAction= propsonAction
onClose= propsonClose />
))
</ul>
<DismissButton onDismiss= propsonClose />
</div>
</FocusScope>
);
}
function MenuItem({item state onAction onClose}) {
// Get props for the menu item element
let ref = ReactuseRef();
let {menuItemProps} = useMenuItem({
key: itemkey
isDisabled: itemisDisabled
onAction
onClose
} state ref);
// Handle focus events so we can apply highlighted
// style to the focused menu item
let [isFocused setFocused] = ReactuseState(false);
let {focusProps} = useFocus({onFocusChange: setFocused});
return (
<li
...mergeProps(menuItemProps focusProps)
ref= ref
style={
background: isFocused ? 'gray' : 'transparent'
color: isFocused ? 'white' : 'black'
padding: '2px 5px'
outline: 'none'
cursor: 'pointer'
}>
itemrendered
</li>
);
}
<MenuButton label="Actions" onAction= alert>
<Item key="copy">Copy</Item>
<Item key="cut">Cut</Item>
<Item key="paste">Paste</Item>
</MenuButton>
import {useMenuTriggerState} from '@react-stately/menu';
import {useButton} from '@react-aria/button';
import {useMenu useMenuItem} from '@react-aria/menu';
import {useTreeState} from '@react-stately/tree';
import {Item} from '@react-stately/collections';
import {mergeProps} from '@react-aria/utils';
import {FocusScope} from '@react-aria/focus';
import {useFocus} from '@react-aria/interactions';
import {
useOverlay
DismissButton
} from '@react-aria/overlays';
function MenuButton(props) {
// Create state based on the incoming props
let state = useMenuTriggerState(props);
// Get props for the menu trigger and menu elements
let ref = ReactuseRef();
let {menuTriggerProps menuProps} = useMenuTrigger(
{}
state
ref
);
// Get props for the button based on the trigger props from useMenuTrigger
let {buttonProps} = useButton(menuTriggerProps ref);
return (
<div
style={
position: 'relative'
display: 'inline-block'
}>
<button
...buttonProps
ref= ref
style={height: 30 fontSize: 14}>
propslabel
<span aria-hidden="true" style={paddingLeft: 5}>
▼
</span>
</button>
stateisOpen && (
<MenuPopup
...props
domProps= menuProps
autoFocus= statefocusStrategy
onClose=() => stateclose()
/>
)
</div>
);
}
function MenuPopup(props) {
// Create menu state based on the incoming props
let state = useTreeState({
...props
selectionMode: 'none'
});
// Get props for the menu element
let ref = ReactuseRef();
let {menuProps} = useMenu(props state ref);
// Handle events that should cause the menu to close,
// e.g. blur, clicking outside, or pressing the escape key.
let overlayRef = ReactuseRef();
let {overlayProps} = useOverlay(
{
onClose: propsonClose
shouldCloseOnBlur: true
isOpen: true
isDismissable: true
}
overlayRef
);
// Wrap in <FocusScope> so that focus is restored back to the
// trigger when the menu is closed. In addition, add hidden
// <DismissButton> components at the start and end of the list
// to allow screen reader users to dismiss the popup easily.
return (
<FocusScope restoreFocus>
<div ...overlayProps ref= overlayRef>
<DismissButton onDismiss= propsonClose />
<ul
...mergeProps(menuProps propsdomProps)
ref= ref
style={
position: 'absolute'
width: '100%'
margin: '4px 0 0 0'
padding: 0
listStyle: 'none'
border: '1px solid gray'
background: 'lightgray'
}>
[...statecollection]map((item) => (
<MenuItem
key= itemkey
item= item
state= state
onAction= propsonAction
onClose= propsonClose
/>
))
</ul>
<DismissButton onDismiss= propsonClose />
</div>
</FocusScope>
);
}
function MenuItem({item state onAction onClose}) {
// Get props for the menu item element
let ref = ReactuseRef();
let {menuItemProps} = useMenuItem(
{
key: itemkey
isDisabled: itemisDisabled
onAction
onClose
}
state
ref
);
// Handle focus events so we can apply highlighted
// style to the focused menu item
let [isFocused setFocused] = ReactuseState(false);
let {focusProps} = useFocus({onFocusChange: setFocused});
return (
<li
...mergeProps(menuItemProps focusProps)
ref= ref
style={
background: isFocused ? 'gray' : 'transparent'
color: isFocused ? 'white' : 'black'
padding: '2px 5px'
outline: 'none'
cursor: 'pointer'
}>
itemrendered
</li>
);
}
<MenuButton label="Actions" onAction= alert>
<Item key="copy">Copy</Item>
<Item key="cut">Cut</Item>
<Item key="paste">Paste</Item>
</MenuButton>
import {useMenuTriggerState} from '@react-stately/menu';
import {useButton} from '@react-aria/button';
import {
useMenu
useMenuItem
} from '@react-aria/menu';
import {useTreeState} from '@react-stately/tree';
import {Item} from '@react-stately/collections';
import {mergeProps} from '@react-aria/utils';
import {FocusScope} from '@react-aria/focus';
import {useFocus} from '@react-aria/interactions';
import {
useOverlay
DismissButton
} from '@react-aria/overlays';
function MenuButton(
props
) {
// Create state based on the incoming props
let state = useMenuTriggerState(
props
);
// Get props for the menu trigger and menu elements
let ref = ReactuseRef();
let {
menuTriggerProps
menuProps
} = useMenuTrigger(
{}
state
ref
);
// Get props for the button based on the trigger props from useMenuTrigger
let {
buttonProps
} = useButton(
menuTriggerProps
ref
);
return (
<div
style={
position:
'relative'
display:
'inline-block'
}>
<button
...buttonProps
ref= ref
style={
height: 30
fontSize: 14
}>
propslabel
<span
aria-hidden="true"
style={
paddingLeft: 5
}>
▼
</span>
</button>
stateisOpen && (
<MenuPopup
...props
domProps=
menuProps
autoFocus=
statefocusStrategy
onClose=() =>
stateclose()
/>
)
</div>
);
}
function MenuPopup(
props
) {
// Create menu state based on the incoming props
let state = useTreeState(
{
...props
selectionMode:
'none'
}
);
// Get props for the menu element
let ref = ReactuseRef();
let {
menuProps
} = useMenu(
props
state
ref
);
// Handle events that should cause the menu to close,
// e.g. blur, clicking outside, or pressing the escape key.
let overlayRef = ReactuseRef();
let {
overlayProps
} = useOverlay(
{
onClose:
propsonClose
shouldCloseOnBlur: true
isOpen: true
isDismissable: true
}
overlayRef
);
// Wrap in <FocusScope> so that focus is restored back to the
// trigger when the menu is closed. In addition, add hidden
// <DismissButton> components at the start and end of the list
// to allow screen reader users to dismiss the popup easily.
return (
<FocusScope
restoreFocus>
<div
...overlayProps
ref= overlayRef>
<DismissButton
onDismiss=
propsonClose
/>
<ul
...mergeProps(
menuProps
propsdomProps
)
ref= ref
style={
position:
'absolute'
width:
'100%'
margin:
'4px 0 0 0'
padding: 0
listStyle:
'none'
border:
'1px solid gray'
background:
'lightgray'
}>
[
...statecollection
]map(
(item) => (
<MenuItem
key=
itemkey
item=
item
state=
state
onAction=
propsonAction
onClose=
propsonClose
/>
)
)
</ul>
<DismissButton
onDismiss=
propsonClose
/>
</div>
</FocusScope>
);
}
function MenuItem({
item
state
onAction
onClose
}) {
// Get props for the menu item element
let ref = ReactuseRef();
let {
menuItemProps
} = useMenuItem(
{
key: itemkey
isDisabled:
itemisDisabled
onAction
onClose
}
state
ref
);
// Handle focus events so we can apply highlighted
// style to the focused menu item
let [
isFocused
setFocused
] = ReactuseState(
false
);
let {
focusProps
} = useFocus({
onFocusChange: setFocused
});
return (
<li
...mergeProps(
menuItemProps
focusProps
)
ref= ref
style={
background: isFocused
? 'gray'
: 'transparent'
color: isFocused
? 'white'
: 'black'
padding:
'2px 5px'
outline: 'none'
cursor: 'pointer'
}>
itemrendered
</li>
);
}
<MenuButton
label="Actions"
onAction= alert>
<Item key="copy">
Copy
</Item>
<Item key="cut">
Cut
</Item>
<Item key="paste">
Paste
</Item>
</MenuButton>
Internationalization#
RTL#
In right-to-left languages, the menu button should be mirrored. The arrow should be on the left, and the label should be on the right. In addition, the content of menu items should flip. Ensure that your CSS accounts for this.
Name | Type | Description |
collection | Collection<Node<T>> | A collection of items in the tree. |
disabledKeys | Set<Key> | A set of keys for items that are disabled. |
expandedKeys | Set<Key> | A set of keys for items that are expanded. |
toggleKey | (
(key: Key
)) => void | Toggles the expanded state for an item by its key. |
selectionManager | SelectionManager | A selection manager to read and update multiple selection state. |
A generic interface to access a readonly sequential collection of unique keyed items.
Properties
Name | Type | Description |
size | number | The number of items in the collection. |
Methods
Method | Description |
getKeys(
(
)): Iterable<Key> | Iterate over all keys in the collection. |
getItem(
(key: Key
)): T | Get an item by its key. |
getKeyBefore(
(key: Key
)): Key | null | Get the key that comes before the given key in the collection. |
getKeyAfter(
(key: Key
)): Key | null | Get the key that comes after the given key in the collection. |
getFirstKey(
(
)): Key | null | Get the first key in the collection. |
getLastKey(
(
)): Key | null | Get the last key in the collection. |
An interface for reading and updating multiple selection state.
Properties
Name | Type | Description |
selectionMode | SelectionMode | The type of selection that is allowed in the collection. |
disallowEmptySelection | boolean | Whether the collection allows empty selection. |
isFocused | boolean | Whether the collection is currently focused. |
focusedKey | Key | The current focused key in the collection. |
selectedKeys | Set<Key> | The currently selected keys in the collection. |
isEmpty | any | Whether the selection is empty. |
isSelectAll | any | Whether all items in the collection are selected. |
Methods
Method | Description |
setFocused(
(isFocused: boolean
)): void | Sets whether the collection is focused. |
setFocusedKey(
(key: Key
)): void | Sets the focused key. |
isSelected(
(key: Key
)): void | Returns whether a key is selected. |
extendSelection(
(toKey: Key
)): void | Extends the selection to the given key. |
toggleSelection(
(key: Key
)): void | Toggles whether the given key is selected. |
replaceSelection(
(key: Key
)): void | Replaces the selection with only the given key. |
selectAll(
(
)): void | Selects all items in the collection. |
clearSelection(
(
)): void | Removes all keys from the selection. |
toggleSelectAll(
(
)): void | Toggles between select all and an empty selection. |
A FocusScope manages focus for its descendants. It supports containing focus inside the scope, restoring focus to the previously focused element on unmount, and auto focusing children on mount. It also acts as a container for a programmatic focus management interface that can be used to move focus forward and back in response to user events.
Name | Type | Description |
children | ReactNode | The contents of the focus scope. |
contain | boolean | Whether to contain focus inside the scope, so users cannot move focus outside, for example in a modal dialog. |
restoreFocus | boolean | Whether to restore focus back to the element that was focused when the focus scope mounted, after the focus scope unmounts. |
autoFocus | boolean | Whether to auto focus the first focusable element in the focus scope on mount. |
A visually hidden button that can be used to allow screen reader users to dismiss a modal or popup when there is no visual affordance to do so.
Name | Type | Description |
onDismiss | (
(
)) => void | Called when the dismiss button is activated. |
Handles positioning overlays like popovers and menus relative to a trigger element, and updating the position when the window resizes.
useOverlayPosition(
(props: AriaPositionProps
)): PositionAria
Name | Type | Description |
targetRef | RefObject<HTMLElement> | |
overlayRef | RefObject<HTMLElement> | |
scrollRef | RefObject<HTMLElement> | |
shouldUpdatePosition | boolean | |
placement | Placement | |
containerPadding | number | |
offset | number | |
crossOffset | number | |
shouldFlip | boolean | |
boundaryElement | HTMLElement | |
isOpen | boolean |
'bottom'
| 'bottom left'
| 'bottom right'
| 'bottom start'
| 'bottom end'
| 'top'
| 'top left'
| 'top right'
| 'top start'
| 'top end'
| 'left'
| 'left top'
| 'left bottom'
| 'start'
| 'start top'
| 'start bottom'
| 'right'
| 'right top'
| 'right bottom'
| 'end'
| 'end top'
| 'end bottom'
Name | Type | Description |
overlayProps | HTMLAttributes<Element> | |
arrowProps | HTMLAttributes<Element> | |
placement | PlacementAxis |
Axis | 'center'
'top'
| 'bottom'
| 'left'
| 'right'