diff --git a/packages/components/src/higher-order/with-focus-outside/index.js b/packages/components/src/higher-order/with-focus-outside/index.js index ca966eb048af37..c83de77f009060 100644 --- a/packages/components/src/higher-order/with-focus-outside/index.js +++ b/packages/components/src/higher-order/with-focus-outside/index.js @@ -1,142 +1,33 @@ -/** - * External dependencies - */ -import { includes } from 'lodash'; - /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; -import { createHigherOrderComponent } from '@wordpress/compose'; - -/** - * Input types which are classified as button types, for use in considering - * whether element is a (focus-normalized) button. - * - * @type {string[]} - */ -const INPUT_BUTTON_TYPES = [ 'button', 'submit' ]; - -/** - * Returns true if the given element is a button element subject to focus - * normalization, or false otherwise. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus - * - * @param {Element} element Element to test. - * - * @return {boolean} Whether element is a button. - */ -function isFocusNormalizedButton( element ) { - switch ( element.nodeName ) { - case 'A': - case 'BUTTON': - return true; - - case 'INPUT': - return includes( INPUT_BUTTON_TYPES, element.type ); - } - - return false; -} - -export default createHigherOrderComponent( ( WrappedComponent ) => { - return class extends Component { - constructor() { - super( ...arguments ); - - this.bindNode = this.bindNode.bind( this ); - this.cancelBlurCheck = this.cancelBlurCheck.bind( this ); - this.queueBlurCheck = this.queueBlurCheck.bind( this ); - this.normalizeButtonFocus = this.normalizeButtonFocus.bind( this ); - } - - componentWillUnmount() { - this.cancelBlurCheck(); - } - - bindNode( node ) { - if ( node ) { - this.node = node; - } else { - delete this.node; - this.cancelBlurCheck(); - } - } - - queueBlurCheck( event ) { - // React does not allow using an event reference asynchronously - // due to recycling behavior, except when explicitly persisted. - event.persist(); - - // Skip blur check if clicking button. See `normalizeButtonFocus`. - if ( this.preventBlurCheck ) { - return; - } - - this.blurCheckTimeout = setTimeout( () => { - // If document is not focused then focus should remain - // inside the wrapped component and therefore we cancel - // this blur event thereby leaving focus in place. - // https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus. - if ( ! document.hasFocus() ) { - event.preventDefault(); - return; - } - if ( 'function' === typeof this.node.handleFocusOutside ) { - this.node.handleFocusOutside( event ); - } - }, 0 ); - } - - cancelBlurCheck() { - clearTimeout( this.blurCheckTimeout ); - } - - /** - * Handles a mousedown or mouseup event to respectively assign and - * unassign a flag for preventing blur check on button elements. Some - * browsers, namely Firefox and Safari, do not emit a focus event on - * button elements when clicked, while others do. The logic here - * intends to normalize this as treating click on buttons as focus. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus - * - * @param {MouseEvent} event Event for mousedown or mouseup. - */ - normalizeButtonFocus( event ) { - const { type, target } = event; - - const isInteractionEnd = includes( - [ 'mouseup', 'touchend' ], - type - ); - - if ( isInteractionEnd ) { - this.preventBlurCheck = false; - } else if ( isFocusNormalizedButton( target ) ) { - this.preventBlurCheck = true; - } - } - - render() { - // Disable reason: See `normalizeButtonFocus` for browser-specific - // focus event normalization. - - /* eslint-disable jsx-a11y/no-static-element-interactions */ - return ( -
- -
- ); - /* eslint-enable jsx-a11y/no-static-element-interactions */ - } - }; -}, 'withFocusOutside' ); +import { useCallback, useState } from '@wordpress/element'; +import { + createHigherOrderComponent, + __experimentalUseFocusOutside as useFocusOutside, +} from '@wordpress/compose'; + +export default createHigherOrderComponent( + ( WrappedComponent ) => ( props ) => { + const [ handleFocusOutside, setHandleFocusOutside ] = useState(); + const bindFocusOutsideHandler = useCallback( + ( node ) => + setHandleFocusOutside( () => + node?.handleFocusOutside + ? node.handleFocusOutside.bind( node ) + : undefined + ), + [] + ); + + return ( +
+ +
+ ); + }, + 'withFocusOutside' +); diff --git a/packages/components/src/higher-order/with-focus-outside/index.native.js b/packages/components/src/higher-order/with-focus-outside/index.native.js index 98249c09fbdd22..e23ae14b0b52de 100644 --- a/packages/components/src/higher-order/with-focus-outside/index.native.js +++ b/packages/components/src/higher-order/with-focus-outside/index.native.js @@ -1,133 +1,37 @@ /** * External dependencies */ -import { includes } from 'lodash'; import { View } from 'react-native'; - /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; -import { createHigherOrderComponent } from '@wordpress/compose'; - -/** - * Input types which are classified as button types, for use in considering - * whether element is a (focus-normalized) button. - * - * @type {string[]} - */ -const INPUT_BUTTON_TYPES = [ 'button', 'submit' ]; - -/** - * Returns true if the given element is a button element subject to focus - * normalization, or false otherwise. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus - * - * @param {Element} element Element to test. - * - * @return {boolean} Whether element is a button. - */ -function isFocusNormalizedButton( element ) { - switch ( element.nodeName ) { - case 'A': - case 'BUTTON': - return true; - - case 'INPUT': - return includes( INPUT_BUTTON_TYPES, element.type ); - } - - return false; -} - -export default createHigherOrderComponent( ( WrappedComponent ) => { - return class extends Component { - constructor() { - super( ...arguments ); - - this.bindNode = this.bindNode.bind( this ); - this.cancelBlurCheck = this.cancelBlurCheck.bind( this ); - this.queueBlurCheck = this.queueBlurCheck.bind( this ); - this.normalizeButtonFocus = this.normalizeButtonFocus.bind( this ); - } - - componentWillUnmount() { - this.cancelBlurCheck(); - } - - bindNode( node ) { - if ( node ) { - this.node = node; - } else { - delete this.node; - this.cancelBlurCheck(); - } - } - - queueBlurCheck( event ) { - // React does not allow using an event reference asynchronously - // due to recycling behavior, except when explicitly persisted. - event.persist(); - - // Skip blur check if clicking button. See `normalizeButtonFocus`. - if ( this.preventBlurCheck ) { - return; - } - - this.blurCheckTimeout = setTimeout( () => { - if ( 'function' === typeof this.node.handleFocusOutside ) { - this.node.handleFocusOutside( event ); - } - }, 0 ); - } - - cancelBlurCheck() { - clearTimeout( this.blurCheckTimeout ); - } - - /** - * Handles a mousedown or mouseup event to respectively assign and - * unassign a flag for preventing blur check on button elements. Some - * browsers, namely Firefox and Safari, do not emit a focus event on - * button elements when clicked, while others do. The logic here - * intends to normalize this as treating click on buttons as focus. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus - * - * @param {MouseEvent} event Event for mousedown or mouseup. - */ - normalizeButtonFocus( event ) { - const { type, target } = event; - - const isInteractionEnd = includes( - [ 'mouseup', 'touchend' ], - type - ); - - if ( isInteractionEnd ) { - this.preventBlurCheck = false; - } else if ( isFocusNormalizedButton( target ) ) { - this.preventBlurCheck = true; - } - } - - render() { - // Disable reason: See `normalizeButtonFocus` for browser-specific - // focus event normalization. - - return ( - - - - ); - } - }; -}, 'withFocusOutside' ); +import { useCallback, useState } from '@wordpress/element'; +import { + createHigherOrderComponent, + __experimentalUseFocusOutside as useFocusOutside, +} from '@wordpress/compose'; + +export default createHigherOrderComponent( + ( WrappedComponent ) => ( props ) => { + const [ handleFocusOutside, setHandleFocusOutside ] = useState(); + const bindFocusOutsideHandler = useCallback( + ( node ) => + setHandleFocusOutside( () => + node?.handleFocusOutside + ? node.handleFocusOutside.bind( node ) + : undefined + ), + [] + ); + + return ( + + + + ); + }, + 'withFocusOutside' +); diff --git a/packages/compose/src/hooks/use-focus-outside/index.js b/packages/compose/src/hooks/use-focus-outside/index.js new file mode 100644 index 00000000000000..2169d4fd83c371 --- /dev/null +++ b/packages/compose/src/hooks/use-focus-outside/index.js @@ -0,0 +1,191 @@ +/** + * External dependencies + */ +import { includes } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useCallback, useEffect, useRef } from '@wordpress/element'; + +/** + * Input types which are classified as button types, for use in considering + * whether element is a (focus-normalized) button. + * + * @type {string[]} + */ +const INPUT_BUTTON_TYPES = [ 'button', 'submit' ]; + +/** + * @typedef {HTMLButtonElement | HTMLLinkElement | HTMLInputElement} FocusNormalizedButton + */ + +// Disable reason: Rule doesn't support predicate return types +/* eslint-disable jsdoc/valid-types */ +/** + * Returns true if the given element is a button element subject to focus + * normalization, or false otherwise. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus + * + * @param {EventTarget} eventTarget The target from a mouse or touch event. + * + * @return {eventTarget is FocusNormalizedButton} Whether element is a button. + */ +function isFocusNormalizedButton( eventTarget ) { + if ( ! ( eventTarget instanceof window.HTMLElement ) ) { + return false; + } + switch ( eventTarget.nodeName ) { + case 'A': + case 'BUTTON': + return true; + + case 'INPUT': + return includes( + INPUT_BUTTON_TYPES, + /** @type {HTMLInputElement} */ ( eventTarget ).type + ); + } + + return false; +} +/* eslint-enable jsdoc/valid-types */ + +/** + * @typedef {import('react').SyntheticEvent} SyntheticEvent + */ + +/** + * @callback EventCallback + * @param {SyntheticEvent} event input related event. + */ + +/** + * @typedef FocusOutsideReactElement + * @property {EventCallback} handleFocusOutside callback for a focus outside event. + */ + +/** + * @typedef {import('react').MutableRefObject} FocusOutsideRef + */ + +/** + * @typedef {Object} FocusOutsideReturnValue + * @property {EventCallback} onFocus An event handler for focus events. + * @property {EventCallback} onBlur An event handler for blur events. + * @property {EventCallback} onMouseDown An event handler for mouse down events. + * @property {EventCallback} onMouseUp An event handler for mouse up events. + * @property {EventCallback} onTouchStart An event handler for touch start events. + * @property {EventCallback} onTouchEnd An event handler for touch end events. + */ + +/** + * A react hook that can be used to check whether focus has moved outside the + * element the event handlers are bound to. + * + * @param {EventCallback} onFocusOutside A callback triggered when focus moves outside + * the element the event handlers are bound to. + * + * @return {FocusOutsideReturnValue} An object containing event handlers. Bind the event handlers + * to a wrapping element element to capture when focus moves + * outside that element. + */ +export default function useFocusOutside( onFocusOutside ) { + const currentOnFocusOutside = useRef( onFocusOutside ); + useEffect( () => { + currentOnFocusOutside.current = onFocusOutside; + }, [ onFocusOutside ] ); + + const preventBlurCheck = useRef( false ); + + /** + * @type {import('react').MutableRefObject} + */ + const blurCheckTimeoutId = useRef(); + + /** + * Cancel a blur check timeout. + */ + const cancelBlurCheck = useCallback( () => { + clearTimeout( blurCheckTimeoutId.current ); + }, [] ); + + // Cancel blur checks on unmount. + useEffect( () => { + return () => cancelBlurCheck(); + }, [] ); + + // Cancel a blur check if the callback or ref is no longer provided. + useEffect( () => { + if ( ! onFocusOutside ) { + cancelBlurCheck(); + } + }, [ onFocusOutside, cancelBlurCheck ] ); + + /** + * Handles a mousedown or mouseup event to respectively assign and + * unassign a flag for preventing blur check on button elements. Some + * browsers, namely Firefox and Safari, do not emit a focus event on + * button elements when clicked, while others do. The logic here + * intends to normalize this as treating click on buttons as focus. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus + * + * @param {SyntheticEvent} event Event for mousedown or mouseup. + */ + const normalizeButtonFocus = useCallback( ( event ) => { + const { type, target } = event; + const isInteractionEnd = includes( [ 'mouseup', 'touchend' ], type ); + + if ( isInteractionEnd ) { + preventBlurCheck.current = false; + } else if ( isFocusNormalizedButton( target ) ) { + preventBlurCheck.current = true; + } + }, [] ); + + /** + * A callback triggered when a blur event occurs on the element the handler + * is bound to. + * + * Calls the `onFocusOutside` callback in an immediate timeout if focus has + * move outside the bound element and is still within the document. + * + * @param {SyntheticEvent} event Blur event. + */ + const queueBlurCheck = useCallback( ( event ) => { + // React does not allow using an event reference asynchronously + // due to recycling behavior, except when explicitly persisted. + event.persist(); + + // Skip blur check if clicking button. See `normalizeButtonFocus`. + if ( preventBlurCheck.current ) { + return; + } + + blurCheckTimeoutId.current = setTimeout( () => { + // If document is not focused then focus should remain + // inside the wrapped component and therefore we cancel + // this blur event thereby leaving focus in place. + // https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus. + if ( ! document.hasFocus() ) { + event.preventDefault(); + return; + } + + if ( 'function' === typeof currentOnFocusOutside.current ) { + currentOnFocusOutside.current( event ); + } + }, 0 ); + }, [] ); + + return { + onFocus: cancelBlurCheck, + onMouseDown: normalizeButtonFocus, + onMouseUp: normalizeButtonFocus, + onTouchStart: normalizeButtonFocus, + onTouchEnd: normalizeButtonFocus, + onBlur: queueBlurCheck, + }; +} diff --git a/packages/compose/src/hooks/use-focus-outside/index.native.js b/packages/compose/src/hooks/use-focus-outside/index.native.js new file mode 100644 index 00000000000000..6c5c35766379f9 --- /dev/null +++ b/packages/compose/src/hooks/use-focus-outside/index.native.js @@ -0,0 +1,179 @@ +/** + * External dependencies + */ +import { includes } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useCallback, useEffect, useRef } from '@wordpress/element'; + +/** + * Input types which are classified as button types, for use in considering + * whether element is a (focus-normalized) button. + * + * @type {string[]} + */ +const INPUT_BUTTON_TYPES = [ 'button', 'submit' ]; + +/** + * @typedef {HTMLButtonElement | HTMLLinkElement | HTMLInputElement} FocusNormalizedButton + */ + +// Disable reason: Rule doesn't support predicate return types +/* eslint-disable jsdoc/valid-types */ +/** + * Returns true if the given element is a button element subject to focus + * normalization, or false otherwise. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus + * + * @param {EventTarget} eventTarget The target from a mouse or touch event. + * + * @return {eventTarget is FocusNormalizedButton} Whether element is a button. + */ +function isFocusNormalizedButton( eventTarget ) { + switch ( eventTarget.nodeName ) { + case 'A': + case 'BUTTON': + return true; + + case 'INPUT': + return includes( + INPUT_BUTTON_TYPES, + /** @type {HTMLInputElement} */ ( eventTarget ).type + ); + } + + return false; +} +/* eslint-enable jsdoc/valid-types */ + +/** + * @typedef {import('react').SyntheticEvent} SyntheticEvent + */ + +/** + * @callback EventCallback + * @param {SyntheticEvent} event input related event. + */ + +/** + * @typedef FocusOutsideReactElement + * @property {EventCallback} handleFocusOutside callback for a focus outside event. + */ + +/** + * @typedef {import('react').MutableRefObject} FocusOutsideRef + */ + +/** + * @typedef {Object} FocusOutsideReturnValue + * @property {EventCallback} onFocus An event handler for focus events. + * @property {EventCallback} onBlur An event handler for blur events. + * @property {EventCallback} onMouseDown An event handler for mouse down events. + * @property {EventCallback} onMouseUp An event handler for mouse up events. + * @property {EventCallback} onTouchStart An event handler for touch start events. + * @property {EventCallback} onTouchEnd An event handler for touch end events. + */ + +/** + * A react hook that can be used to check whether focus has moved outside the + * element the event handlers are bound to. + * + * @param {EventCallback} onFocusOutside A callback triggered when focus moves outside + * the element the event handlers are bound to. + * + * @return {FocusOutsideReturnValue} An object containing event handlers. Bind the event handlers + * to a wrapping element element to capture when focus moves + * outside that element. + */ +export default function useFocusOutside( onFocusOutside ) { + const currentOnFocusOutside = useRef( onFocusOutside ); + useEffect( () => { + currentOnFocusOutside.current = onFocusOutside; + }, [ onFocusOutside ] ); + + const preventBlurCheck = useRef( false ); + + /** + * @type {import('react').MutableRefObject} + */ + const blurCheckTimeoutId = useRef(); + + /** + * Cancel a blur check timeout. + */ + const cancelBlurCheck = useCallback( () => { + clearTimeout( blurCheckTimeoutId.current ); + }, [] ); + + // Cancel blur checks on unmount. + useEffect( () => { + return () => cancelBlurCheck(); + }, [] ); + + // Cancel a blur check if the callback or ref is no longer provided. + useEffect( () => { + if ( ! onFocusOutside ) { + cancelBlurCheck(); + } + }, [ onFocusOutside, cancelBlurCheck ] ); + + /** + * Handles a mousedown or mouseup event to respectively assign and + * unassign a flag for preventing blur check on button elements. Some + * browsers, namely Firefox and Safari, do not emit a focus event on + * button elements when clicked, while others do. The logic here + * intends to normalize this as treating click on buttons as focus. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus + * + * @param {SyntheticEvent} event Event for mousedown or mouseup. + */ + const normalizeButtonFocus = useCallback( ( event ) => { + const { type, target } = event; + const isInteractionEnd = includes( [ 'mouseup', 'touchend' ], type ); + + if ( isInteractionEnd ) { + preventBlurCheck.current = false; + } else if ( isFocusNormalizedButton( target ) ) { + preventBlurCheck.current = true; + } + }, [] ); + + /** + * A callback triggered when a blur event occurs on the element the handler + * is bound to. + * + * Calls the `onFocusOutside` callback in an immediate timeout if focus has + * move outside the bound element and is still within the document. + * + * @param {SyntheticEvent} event Blur event. + */ + const queueBlurCheck = useCallback( ( event ) => { + // React does not allow using an event reference asynchronously + // due to recycling behavior, except when explicitly persisted. + event.persist(); + + // Skip blur check if clicking button. See `normalizeButtonFocus`. + if ( preventBlurCheck.current ) { + return; + } + + blurCheckTimeoutId.current = setTimeout( () => { + if ( 'function' === typeof currentOnFocusOutside.current ) { + currentOnFocusOutside.current( event ); + } + }, 0 ); + }, [] ); + + return { + onFocus: cancelBlurCheck, + onMouseDown: normalizeButtonFocus, + onMouseUp: normalizeButtonFocus, + onTouchStart: normalizeButtonFocus, + onTouchEnd: normalizeButtonFocus, + onBlur: queueBlurCheck, + }; +} diff --git a/packages/compose/src/hooks/use-focus-outside/test/index.js b/packages/compose/src/hooks/use-focus-outside/test/index.js new file mode 100644 index 00000000000000..90a27102f90813 --- /dev/null +++ b/packages/compose/src/hooks/use-focus-outside/test/index.js @@ -0,0 +1,124 @@ +/** + * External dependencies + */ +import TestUtils from 'react-dom/test-utils'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import useFocusOutside from '../'; +import ReactDOM from 'react-dom'; + +let wrapper, onFocusOutside; + +describe( 'useFocusOutside', () => { + let origHasFocus; + + const FocusOutsideComponent = ( { onFocusOutside: callback } ) => ( +
+ + +
+ ); + + // this is needed because TestUtils does not accept a stateless component. + // anything run through a HOC ends up as a stateless component. + const getTestComponent = ( WrappedComponent, props ) => { + class TestComponent extends Component { + render() { + return ; + } + } + return ; + }; + + const simulateEvent = ( event, index = 0 ) => { + const element = TestUtils.scryRenderedDOMComponentsWithTag( + wrapper, + 'input' + ); + TestUtils.Simulate[ event ]( element[ index ] ); + }; + + beforeEach( () => { + // Mock document.hasFocus() to always be true for testing + // note: we overide this for some tests. + origHasFocus = document.hasFocus; + document.hasFocus = () => true; + + onFocusOutside = jest.fn(); + wrapper = TestUtils.renderIntoDocument( + getTestComponent( FocusOutsideComponent, { onFocusOutside } ) + ); + } ); + + afterEach( () => { + document.hasFocus = origHasFocus; + } ); + + it( 'should not call handler if focus shifts to element within component', () => { + simulateEvent( 'focus' ); + simulateEvent( 'blur' ); + simulateEvent( 'focus', 1 ); + + jest.runAllTimers(); + + expect( onFocusOutside ).not.toHaveBeenCalled(); + } ); + + it( 'should not call handler if focus transitions via click to button', () => { + simulateEvent( 'focus' ); + simulateEvent( 'mouseDown', 1 ); + simulateEvent( 'blur' ); + + // In most browsers, the input at index 1 would receive a focus event + // at this point, but this is not guaranteed, which is the intention of + // the normalization behavior tested here. + simulateEvent( 'mouseUp', 1 ); + + jest.runAllTimers(); + + expect( onFocusOutside ).not.toHaveBeenCalled(); + } ); + + it( 'should call handler if focus doesn’t shift to element within component', () => { + simulateEvent( 'focus' ); + simulateEvent( 'blur' ); + + jest.runAllTimers(); + + expect( onFocusOutside ).toHaveBeenCalled(); + } ); + + it( 'should not call handler if focus shifts outside the component when the document does not have focus', () => { + // Force document.hasFocus() to return false to simulate the window/document losing focus + // See https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus. + document.hasFocus = () => false; + + simulateEvent( 'focus' ); + simulateEvent( 'blur' ); + + jest.runAllTimers(); + + expect( onFocusOutside ).not.toHaveBeenCalled(); + } ); + + it( 'should cancel check when unmounting while queued', () => { + simulateEvent( 'focus' ); + simulateEvent( 'input' ); + + ReactDOM.unmountComponentAtNode( + // eslint-disable-next-line react/no-find-dom-node + ReactDOM.findDOMNode( wrapper ).parentNode + ); + + jest.runAllTimers(); + + expect( onFocusOutside ).not.toHaveBeenCalled(); + } ); +} ); diff --git a/packages/compose/src/index.js b/packages/compose/src/index.js index 2e9d979e4c2fa6..4df6a8391bc3c7 100644 --- a/packages/compose/src/index.js +++ b/packages/compose/src/index.js @@ -17,6 +17,7 @@ export { default as useConstrainedTabbing } from './hooks/use-constrained-tabbin export { default as useCopyOnClick } from './hooks/use-copy-on-click'; export { default as __experimentalUseDragging } from './hooks/use-dragging'; export { default as useFocusOnMount } from './hooks/use-focus-on-mount'; +export { default as __experimentalUseFocusOutside } from './hooks/use-focus-outside'; export { default as useInstanceId } from './hooks/use-instance-id'; export { default as useKeyboardShortcut } from './hooks/use-keyboard-shortcut'; export { default as useMediaQuery } from './hooks/use-media-query'; diff --git a/packages/compose/src/index.native.js b/packages/compose/src/index.native.js index 8181c262214536..04334739cd4e1f 100644 --- a/packages/compose/src/index.native.js +++ b/packages/compose/src/index.native.js @@ -15,6 +15,7 @@ export { default as withState } from './higher-order/with-state'; // Hooks export { default as useConstrainedTabbing } from './hooks/use-constrained-tabbing'; export { default as __experimentalUseDragging } from './hooks/use-dragging'; +export { default as __experimentalUseFocusOutside } from './hooks/use-focus-outside'; export { default as useInstanceId } from './hooks/use-instance-id'; export { default as useKeyboardShortcut } from './hooks/use-keyboard-shortcut'; export { default as useMediaQuery } from './hooks/use-media-query';