UNPKG

@wordpress/compose

Version:
8 lines (7 loc) 7.55 kB
{ "version": 3, "sources": ["../../../src/hooks/use-focus-outside/index.ts"], "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { useCallback, useEffect, useRef } from '@wordpress/element';\n\n/**\n * Input types which are classified as button types, for use in considering\n * whether element is a (focus-normalized) button.\n */\nconst INPUT_BUTTON_TYPES = [ 'button', 'submit' ];\n\n/**\n * List of HTML button elements subject to focus normalization\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus\n */\ntype FocusNormalizedButton =\n\t| HTMLButtonElement\n\t| HTMLLinkElement\n\t| HTMLInputElement;\n\n/**\n * Returns true if the given element is a button element subject to focus\n * normalization, or false otherwise.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus\n *\n * @param eventTarget The target from a mouse or touch event.\n *\n * @return Whether the element is a button element subject to focus normalization.\n */\nfunction isFocusNormalizedButton(\n\teventTarget: EventTarget\n): eventTarget is FocusNormalizedButton {\n\tif ( ! ( eventTarget instanceof window.HTMLElement ) ) {\n\t\treturn false;\n\t}\n\tswitch ( eventTarget.nodeName ) {\n\t\tcase 'A':\n\t\tcase 'BUTTON':\n\t\t\treturn true;\n\n\t\tcase 'INPUT':\n\t\t\treturn INPUT_BUTTON_TYPES.includes(\n\t\t\t\t( eventTarget as HTMLInputElement ).type\n\t\t\t);\n\t}\n\n\treturn false;\n}\n\ntype UseFocusOutsideReturn = {\n\tonFocus: React.FocusEventHandler;\n\tonMouseDown: React.MouseEventHandler;\n\tonMouseUp: React.MouseEventHandler;\n\tonTouchStart: React.TouchEventHandler;\n\tonTouchEnd: React.TouchEventHandler;\n\tonBlur: React.FocusEventHandler;\n};\n\n/**\n * A react hook that can be used to check whether focus has moved outside the\n * element the event handlers are bound to.\n *\n * @param onFocusOutside A callback triggered when focus moves outside\n * the element the event handlers are bound to.\n *\n * @return An object containing event handlers. Bind the event handlers to a\n * wrapping element element to capture when focus moves outside that element.\n */\nexport default function useFocusOutside(\n\tonFocusOutside: ( ( event: React.FocusEvent ) => void ) | undefined\n): UseFocusOutsideReturn {\n\tconst currentOnFocusOutsideRef = useRef( onFocusOutside );\n\tuseEffect( () => {\n\t\tcurrentOnFocusOutsideRef.current = onFocusOutside;\n\t}, [ onFocusOutside ] );\n\n\tconst preventBlurCheckRef = useRef( false );\n\n\tconst blurCheckTimeoutIdRef = useRef< number >( undefined );\n\n\t/**\n\t * Cancel a blur check timeout.\n\t */\n\tconst cancelBlurCheck = useCallback( () => {\n\t\tclearTimeout( blurCheckTimeoutIdRef.current );\n\t}, [] );\n\n\t// Cancel a blur check if the callback or ref is no longer provided.\n\tuseEffect( () => {\n\t\tif ( ! onFocusOutside ) {\n\t\t\tcancelBlurCheck();\n\t\t}\n\t}, [ onFocusOutside, cancelBlurCheck ] );\n\n\t/**\n\t * Handles a mousedown or mouseup event to respectively assign and\n\t * unassign a flag for preventing blur check on button elements. Some\n\t * browsers, namely Firefox and Safari, do not emit a focus event on\n\t * button elements when clicked, while others do. The logic here\n\t * intends to normalize this as treating click on buttons as focus.\n\t *\n\t * @param event\n\t * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus\n\t */\n\tconst normalizeButtonFocus: React.EventHandler<\n\t\tReact.MouseEvent | React.TouchEvent\n\t> = useCallback( ( event ) => {\n\t\tconst { type, target } = event;\n\t\tconst isInteractionEnd = [ 'mouseup', 'touchend' ].includes( type );\n\n\t\tif ( isInteractionEnd ) {\n\t\t\tpreventBlurCheckRef.current = false;\n\t\t} else if ( isFocusNormalizedButton( target ) ) {\n\t\t\tpreventBlurCheckRef.current = true;\n\t\t}\n\t}, [] );\n\n\t/**\n\t * A callback triggered when a blur event occurs on the element the handler\n\t * is bound to.\n\t *\n\t * Calls the `onFocusOutside` callback in an immediate timeout if focus has\n\t * move outside the bound element and is still within the document.\n\t */\n\tconst queueBlurCheck: React.FocusEventHandler = useCallback( ( event ) => {\n\t\t// React does not allow using an event reference asynchronously\n\t\t// due to recycling behavior, except when explicitly persisted.\n\t\tevent.persist();\n\n\t\t// Skip blur check if clicking button. See `normalizeButtonFocus`.\n\t\tif ( preventBlurCheckRef.current ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// The usage of this attribute should be avoided. The only use case\n\t\t// would be when we load modals that are not React components and\n\t\t// therefore don't exist in the React tree. An example is opening\n\t\t// the Media Library modal from another dialog.\n\t\t// This attribute should contain a selector of the related target\n\t\t// we want to ignore, because we still need to trigger the blur event\n\t\t// on all other cases.\n\t\tconst ignoreForRelatedTarget = event.target.getAttribute(\n\t\t\t'data-unstable-ignore-focus-outside-for-relatedtarget'\n\t\t);\n\t\tif (\n\t\t\tignoreForRelatedTarget &&\n\t\t\tevent.relatedTarget?.closest( ignoreForRelatedTarget )\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tblurCheckTimeoutIdRef.current = setTimeout( () => {\n\t\t\t// If document is not focused then focus should remain\n\t\t\t// inside the wrapped component and therefore we cancel\n\t\t\t// this blur event thereby leaving focus in place.\n\t\t\t// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus.\n\t\t\tif ( ! document.hasFocus() ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( 'function' === typeof currentOnFocusOutsideRef.current ) {\n\t\t\t\tcurrentOnFocusOutsideRef.current( event );\n\t\t\t}\n\t\t}, 0 );\n\t}, [] );\n\n\treturn {\n\t\tonFocus: cancelBlurCheck,\n\t\tonMouseDown: normalizeButtonFocus,\n\t\tonMouseUp: normalizeButtonFocus,\n\t\tonTouchStart: normalizeButtonFocus,\n\t\tonTouchEnd: normalizeButtonFocus,\n\t\tonBlur: queueBlurCheck,\n\t};\n}\n"], "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,qBAA+C;AAM/C,IAAM,qBAAqB,CAAE,UAAU,QAAS;AAsBhD,SAAS,wBACR,aACuC;AACvC,MAAK,EAAI,uBAAuB,OAAO,cAAgB;AACtD,WAAO;AAAA,EACR;AACA,UAAS,YAAY,UAAW;AAAA,IAC/B,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IAER,KAAK;AACJ,aAAO,mBAAmB;AAAA,QACvB,YAAkC;AAAA,MACrC;AAAA,EACF;AAEA,SAAO;AACR;AAqBe,SAAR,gBACN,gBACwB;AACxB,QAAM,+BAA2B,uBAAQ,cAAe;AACxD,gCAAW,MAAM;AAChB,6BAAyB,UAAU;AAAA,EACpC,GAAG,CAAE,cAAe,CAAE;AAEtB,QAAM,0BAAsB,uBAAQ,KAAM;AAE1C,QAAM,4BAAwB,uBAAkB,MAAU;AAK1D,QAAM,sBAAkB,4BAAa,MAAM;AAC1C,iBAAc,sBAAsB,OAAQ;AAAA,EAC7C,GAAG,CAAC,CAAE;AAGN,gCAAW,MAAM;AAChB,QAAK,CAAE,gBAAiB;AACvB,sBAAgB;AAAA,IACjB;AAAA,EACD,GAAG,CAAE,gBAAgB,eAAgB,CAAE;AAYvC,QAAM,2BAEF,4BAAa,CAAE,UAAW;AAC7B,UAAM,EAAE,MAAM,OAAO,IAAI;AACzB,UAAM,mBAAmB,CAAE,WAAW,UAAW,EAAE,SAAU,IAAK;AAElE,QAAK,kBAAmB;AACvB,0BAAoB,UAAU;AAAA,IAC/B,WAAY,wBAAyB,MAAO,GAAI;AAC/C,0BAAoB,UAAU;AAAA,IAC/B;AAAA,EACD,GAAG,CAAC,CAAE;AASN,QAAM,qBAA0C,4BAAa,CAAE,UAAW;AAGzE,UAAM,QAAQ;AAGd,QAAK,oBAAoB,SAAU;AAClC;AAAA,IACD;AASA,UAAM,yBAAyB,MAAM,OAAO;AAAA,MAC3C;AAAA,IACD;AACA,QACC,0BACA,MAAM,eAAe,QAAS,sBAAuB,GACpD;AACD;AAAA,IACD;AAEA,0BAAsB,UAAU,WAAY,MAAM;AAKjD,UAAK,CAAE,SAAS,SAAS,GAAI;AAC5B,cAAM,eAAe;AACrB;AAAA,MACD;AAEA,UAAK,eAAe,OAAO,yBAAyB,SAAU;AAC7D,iCAAyB,QAAS,KAAM;AAAA,MACzC;AAAA,IACD,GAAG,CAAE;AAAA,EACN,GAAG,CAAC,CAAE;AAEN,SAAO;AAAA,IACN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,WAAW;AAAA,IACX,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,QAAQ;AAAA,EACT;AACD;", "names": [] }