UNPKG

@wordpress/compose

Version:
133 lines (125 loc) 4.05 kB
/** * External dependencies */ import type { KeyboardEvent, KeyboardEventHandler, RefCallback, SyntheticEvent, } from 'react'; /** * WordPress dependencies */ import { useRef, useEffect, useCallback } from '@wordpress/element'; /** * Internal dependencies */ import useConstrainedTabbing from '../use-constrained-tabbing'; import { useFocusOnMount } from '../use-focus-on-mount'; import useFocusReturn from '../use-focus-return'; import useFocusOutside from '../use-focus-outside'; import useMergeRefs from '../use-merge-refs'; type DialogOptions = { /** * Determines focus behavior when the dialog mounts. * * - `"firstElement"` focuses the first tabbable element within. * - `"firstInputElement"` focuses the first value control within. * - `true` focuses the element itself. * - `false` does nothing and _should not be used unless an accessible * substitute behavior is implemented_. * * @default 'firstElement' */ focusOnMount?: useFocusOnMount.Mode; /** * Determines whether tabbing is constrained to within the popover, * preventing keyboard focus from leaving the popover content without * explicit focus elsewhere, or whether the popover remains part of the * wider tab order. * If no value is passed, it will be derived from `focusOnMount`. * * @see focusOnMount * @default `focusOnMount` !== false */ constrainTabbing?: boolean; onClose?: () => void; /** * Optional `onKeyDown` handler, merged with the built-in one. */ onKeyDown?: KeyboardEventHandler< HTMLElement >; /** * Use the `onClose` prop instead. * * @deprecated */ __unstableOnClose?: ( type: string | undefined, event: SyntheticEvent ) => void; }; type useDialogReturn = [ RefCallback< HTMLElement >, ReturnType< typeof useFocusOutside > & { onKeyDown: ( event: KeyboardEvent< HTMLElement > ) => void; } & Pick< HTMLElement, 'tabIndex' >, ]; /** * Returns a ref and props to apply to a dialog wrapper to enable the following behaviors: * - constrained tabbing. * - focus on mount. * - return focus on unmount. * - focus outside. * * @param options Dialog Options. */ function useDialog( options: DialogOptions ): useDialogReturn { const currentOptions = useRef< DialogOptions >( undefined ); const { constrainTabbing = options.focusOnMount !== false } = options; useEffect( () => { currentOptions.current = options; }, Object.values( options ) ); const constrainedTabbingRef = useConstrainedTabbing(); const focusOnMountRef = useFocusOnMount( options.focusOnMount ); const focusReturnRef = useFocusReturn(); const focusOutsideProps = useFocusOutside( ( event ) => { // This unstable prop is here only to manage backward compatibility // for the Popover component otherwise, the onClose should be enough. if ( currentOptions.current?.__unstableOnClose ) { currentOptions.current.__unstableOnClose( 'focus-outside', event ); } else if ( currentOptions.current?.onClose ) { currentOptions.current.onClose(); } } ); // Close on Escape via a React `onKeyDown` (rather than a native listener) // so portaled descendants that handle Escape and call // `event.stopPropagation()` correctly prevent the dialog from closing. // See https://github.com/WordPress/gutenberg/issues/78432. const onKeyDown = useCallback( ( event: KeyboardEvent< HTMLElement > ) => { // Let the consumer-provided handler (if any) run first so it can // call `preventDefault()` to opt out of close-on-Escape. currentOptions.current?.onKeyDown?.( event ); if ( event.key === 'Escape' && ! event.defaultPrevented && currentOptions.current?.onClose ) { event.preventDefault(); event.stopPropagation(); currentOptions.current.onClose(); } }, [] ); return [ useMergeRefs( [ constrainTabbing ? constrainedTabbingRef : null, options.focusOnMount !== false ? focusReturnRef : null, options.focusOnMount !== false ? focusOnMountRef : null, ] ), { ...focusOutsideProps, onKeyDown, tabIndex: -1, }, ]; } export default useDialog;