useOverlayTrigger
Handles the behavior and accessibility for an overlay trigger, e.g. a button that opens a popover, menu, or other overlay that is positioned relative to the trigger.
install | yarn add @react-aria/overlays |
---|---|
version | 3.8.1 |
usage | import {useOverlayTrigger} from '@react-aria/overlays' |
API#
useOverlayTrigger(
props: OverlayTriggerProps,
state: OverlayTriggerState,
ref: RefObject<HTMLElement>
): OverlayTriggerAria
Features#
There is no built in way to create popovers or other types of overlays in
HTML. useOverlayTrigger
,
combined with useOverlayPosition
,
helps achieve accessible overlays that can be styled as needed.
Note: useOverlayTrigger
only handles the overlay itself. It should be combined
with useDialog to create fully accessible popovers. For menus,
see useMenuTrigger, which builds on useOverlayTrigger
and provides
additional functionality specific to menus.
- Exposes overlay trigger and connects trigger to overlay with ARIA
- Positions the overlay relative to the trigger when combined with
useOverlayPosition
- Hides content behind the overlay from screen readers when combined with
useModal
- Handles closing the overlay when interacting outside and pressing the Escape key, when combined with
useOverlay
Anatomy#
An overlay trigger consists of a trigger element (e.g. button) and an overlay
(e.g. popover, menu, listbox, etc.). useOverlayTrigger
handles exposing the
trigger and overlay to assistive technology with ARIA. It should be combined
with useOverlay
to handle
closing the overlay, useModal
to handle hiding content behind the overlay from screen readers, and optionally
with useOverlayPosition
to
handle positioning the overlay relative to the trigger.
useOverlayTrigger
returns props that you should spread onto the trigger element and
the appropriate element:
Name | Type | Description |
triggerProps | AriaButtonProps | Props for the trigger element. |
overlayProps | HTMLAttributes<HTMLElement> | Props for the overlay container element. |
State is managed by the useOverlayTriggerState
hook in @react-stately/overlays
. The state object should be passed as an argument to useOverlayTrigger
.
Example#
This example shows how to build a typical popover overlay that is positioned relative to
a trigger button. The content of the popover is a dialog, built
with useDialog
.
The popover can be closed by clicking or interacting outside the popover, or by pressing the Escape key.
This is handled by useOverlay
.
When the popover is closed, focus is restored back to its trigger button by a
<FocusScope
>.
Content outside the popover is hidden from screen readers
by useModal
.
This improves the experience for screen reader users by ensuring that they don't
navigate out of context. This is especially important when the popover is rendered
into a portal at the end of the document, and the content just before it is
unrelated to the original trigger.
To allow screen reader users to more easily dismiss the popover, a visually hidden
<DismissButton
>
is added at the end of the dialog.
The application is contained in an OverlayProvider
,
which is used to hide the content from screen readers with aria-hidden
while an overlay is open.
In addition, each overlay must be contained in an OverlayContainer
,
which uses a React Portal to render the overlay at the
end of the document body. If a nested overlay is opened, then the first overlay will also be set
to aria-hidden
, so that only the top-most overlay is accessible to screen readers.
Note: useModal
only hides content within parent OverlayProvider
components. However, if you have additional
content in your application outside any OverlayProvider
, then you should use the @react-aria/aria-modal-polyfill
package to ensure that this content is hidden while modals are open as well. See the
watchModals docs for more information.
import {useOverlayTriggerState} from '@react-stately/overlays';
import {
DismissButton,
OverlayContainer,
OverlayProvider,
useModal,
useOverlay,
useOverlayPosition,
useOverlayTrigger
} from '@react-aria/overlays';
import {useDialog} from '@react-aria/dialog';
import {FocusScope} from '@react-aria/focus';
import {useButton} from '@react-aria/button';
import {mergeProps} from '@react-aria/utils';
const Popover = React.forwardRef(({
title,
children,
isOpen,
onClose,
style,
...otherProps
}, ref) => {
// Handle interacting outside the dialog and pressing
// the Escape key to close the modal.
let { overlayProps } = useOverlay({
onClose,
isOpen,
isDismissable: true
}, ref);
// Hide content outside the modal from screen readers.
let { modalProps } = useModal();
// Get props for the dialog and its title
let { dialogProps, titleProps } = useDialog({}, ref);
return (
<FocusScope restoreFocus>
<div
{...mergeProps(overlayProps, dialogProps, otherProps, modalProps)}
ref={ref}
style={{
background: 'white',
color: 'black',
padding: 30,
...style
}}
>
<h3
{...titleProps}
style={{ marginTop: 0 }}
>
{title}
</h3>
{children}
<DismissButton onDismiss={onClose} />
</div>
</FocusScope>
);
});
function Example() {
let state = useOverlayTriggerState({});
let triggerRef = React.useRef();
let overlayRef = React.useRef();
// Get props for the trigger and overlay. This also handles
// hiding the overlay when a parent element of the trigger scrolls
// (which invalidates the popover positioning).
let { triggerProps, overlayProps } = useOverlayTrigger(
{ type: 'dialog' },
state,
triggerRef
);
// Get popover positioning props relative to the trigger
let { overlayProps: positionProps } = useOverlayPosition({
targetRef: triggerRef,
overlayRef,
placement: 'top',
offset: 5,
isOpen: state.isOpen
});
// useButton ensures that focus management is handled correctly,
// across all browsers. Focus is restored to the button once the
// popover closes.
let { buttonProps } = useButton({
onPress: () => state.open()
}, triggerRef);
return (
<>
<button
{...buttonProps}
{...triggerProps}
ref={triggerRef}
>
Open Popover
</button>
{state.isOpen &&
(
<OverlayContainer>
<Popover
{...overlayProps}
{...positionProps}
ref={overlayRef}
title="Popover title"
isOpen={state.isOpen}
onClose={state.close}
>
This is the content of the popover.
</Popover>
</OverlayContainer>
)}
</>
);
}
// Application must be wrapped in an OverlayProvider so that it can be
// hidden from screen readers when an overlay opens.
<OverlayProvider>
<Example />
</OverlayProvider>
import {useOverlayTriggerState} from '@react-stately/overlays';
import {
DismissButton,
OverlayContainer,
OverlayProvider,
useModal,
useOverlay,
useOverlayPosition,
useOverlayTrigger
} from '@react-aria/overlays';
import {useDialog} from '@react-aria/dialog';
import {FocusScope} from '@react-aria/focus';
import {useButton} from '@react-aria/button';
import {mergeProps} from '@react-aria/utils';
const Popover = React.forwardRef(({
title,
children,
isOpen,
onClose,
style,
...otherProps
}, ref) => {
// Handle interacting outside the dialog and pressing
// the Escape key to close the modal.
let { overlayProps } = useOverlay({
onClose,
isOpen,
isDismissable: true
}, ref);
// Hide content outside the modal from screen readers.
let { modalProps } = useModal();
// Get props for the dialog and its title
let { dialogProps, titleProps } = useDialog({}, ref);
return (
<FocusScope restoreFocus>
<div
{...mergeProps(
overlayProps,
dialogProps,
otherProps,
modalProps
)}
ref={ref}
style={{
background: 'white',
color: 'black',
padding: 30,
...style
}}
>
<h3
{...titleProps}
style={{ marginTop: 0 }}
>
{title}
</h3>
{children}
<DismissButton onDismiss={onClose} />
</div>
</FocusScope>
);
});
function Example() {
let state = useOverlayTriggerState({});
let triggerRef = React.useRef();
let overlayRef = React.useRef();
// Get props for the trigger and overlay. This also handles
// hiding the overlay when a parent element of the trigger scrolls
// (which invalidates the popover positioning).
let { triggerProps, overlayProps } = useOverlayTrigger(
{ type: 'dialog' },
state,
triggerRef
);
// Get popover positioning props relative to the trigger
let { overlayProps: positionProps } = useOverlayPosition({
targetRef: triggerRef,
overlayRef,
placement: 'top',
offset: 5,
isOpen: state.isOpen
});
// useButton ensures that focus management is handled correctly,
// across all browsers. Focus is restored to the button once the
// popover closes.
let { buttonProps } = useButton({
onPress: () => state.open()
}, triggerRef);
return (
<>
<button
{...buttonProps}
{...triggerProps}
ref={triggerRef}
>
Open Popover
</button>
{state.isOpen &&
(
<OverlayContainer>
<Popover
{...overlayProps}
{...positionProps}
ref={overlayRef}
title="Popover title"
isOpen={state.isOpen}
onClose={state.close}
>
This is the content of the popover.
</Popover>
</OverlayContainer>
)}
</>
);
}
// Application must be wrapped in an OverlayProvider so that it can be
// hidden from screen readers when an overlay opens.
<OverlayProvider>
<Example />
</OverlayProvider>
import {useOverlayTriggerState} from '@react-stately/overlays';
import {
DismissButton,
OverlayContainer,
OverlayProvider,
useModal,
useOverlay,
useOverlayPosition,
useOverlayTrigger
} from '@react-aria/overlays';
import {useDialog} from '@react-aria/dialog';
import {FocusScope} from '@react-aria/focus';
import {useButton} from '@react-aria/button';
import {mergeProps} from '@react-aria/utils';
const Popover = React
.forwardRef(({
title,
children,
isOpen,
onClose,
style,
...otherProps
}, ref) => {
// Handle interacting outside the dialog and pressing
// the Escape key to close the modal.
let {
overlayProps
} = useOverlay({
onClose,
isOpen,
isDismissable: true
}, ref);
// Hide content outside the modal from screen readers.
let { modalProps } =
useModal();
// Get props for the dialog and its title
let {
dialogProps,
titleProps
} = useDialog(
{},
ref
);
return (
<FocusScope
restoreFocus
>
<div
{...mergeProps(
overlayProps,
dialogProps,
otherProps,
modalProps
)}
ref={ref}
style={{
background:
'white',
color:
'black',
padding: 30,
...style
}}
>
<h3
{...titleProps}
style={{
marginTop:
0
}}
>
{title}
</h3>
{children}
<DismissButton
onDismiss={onClose}
/>
</div>
</FocusScope>
);
});
function Example() {
let state =
useOverlayTriggerState(
{}
);
let triggerRef = React
.useRef();
let overlayRef = React
.useRef();
// Get props for the trigger and overlay. This also handles
// hiding the overlay when a parent element of the trigger scrolls
// (which invalidates the popover positioning).
let {
triggerProps,
overlayProps
} = useOverlayTrigger(
{ type: 'dialog' },
state,
triggerRef
);
// Get popover positioning props relative to the trigger
let {
overlayProps:
positionProps
} = useOverlayPosition(
{
targetRef:
triggerRef,
overlayRef,
placement: 'top',
offset: 5,
isOpen:
state.isOpen
}
);
// useButton ensures that focus management is handled correctly,
// across all browsers. Focus is restored to the button once the
// popover closes.
let { buttonProps } =
useButton({
onPress: () =>
state.open()
}, triggerRef);
return (
<>
<button
{...buttonProps}
{...triggerProps}
ref={triggerRef}
>
Open Popover
</button>
{state.isOpen &&
(
<OverlayContainer>
<Popover
{...overlayProps}
{...positionProps}
ref={overlayRef}
title="Popover title"
isOpen={state
.isOpen}
onClose={state
.close}
>
This is the
content of
the
popover.
</Popover>
</OverlayContainer>
)}
</>
);
}
// Application must be wrapped in an OverlayProvider so that it can be
// hidden from screen readers when an overlay opens.
<OverlayProvider>
<Example />
</OverlayProvider>