1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import * as React from 'react';
    
  11. import useEvent from './useEvent';
    
  12. 
    
  13. const {useCallback, useEffect, useLayoutEffect, useRef} = React;
    
  14. 
    
  15. type FocusEvent = SyntheticEvent<EventTarget>;
    
  16. 
    
  17. type UseFocusOptions = {
    
  18.   disabled?: boolean,
    
  19.   onBlur?: ?(FocusEvent) => void,
    
  20.   onFocus?: ?(FocusEvent) => void,
    
  21.   onFocusChange?: ?(boolean) => void,
    
  22.   onFocusVisibleChange?: ?(boolean) => void,
    
  23. };
    
  24. 
    
  25. type UseFocusWithinOptions = {
    
  26.   disabled?: boolean,
    
  27.   onAfterBlurWithin?: FocusEvent => void,
    
  28.   onBeforeBlurWithin?: FocusEvent => void,
    
  29.   onBlurWithin?: FocusEvent => void,
    
  30.   onFocusWithin?: FocusEvent => void,
    
  31.   onFocusWithinChange?: boolean => void,
    
  32.   onFocusWithinVisibleChange?: boolean => void,
    
  33. };
    
  34. 
    
  35. const isMac =
    
  36.   typeof window !== 'undefined' && window.navigator != null
    
  37.     ? /^Mac/.test(window.navigator.platform)
    
  38.     : false;
    
  39. 
    
  40. const hasPointerEvents =
    
  41.   typeof window !== 'undefined' && window.PointerEvent != null;
    
  42. 
    
  43. const globalFocusVisibleEvents = hasPointerEvents
    
  44.   ? ['keydown', 'pointermove', 'pointerdown', 'pointerup']
    
  45.   : [
    
  46.       'keydown',
    
  47.       'mousedown',
    
  48.       'mousemove',
    
  49.       'mouseup',
    
  50.       'touchmove',
    
  51.       'touchstart',
    
  52.       'touchend',
    
  53.     ];
    
  54. 
    
  55. // Global state for tracking focus visible and emulation of mouse
    
  56. let isGlobalFocusVisible = true;
    
  57. let hasTrackedGlobalFocusVisible = false;
    
  58. 
    
  59. function trackGlobalFocusVisible() {
    
  60.   globalFocusVisibleEvents.forEach(type => {
    
  61.     window.addEventListener(type, handleGlobalFocusVisibleEvent, true);
    
  62.   });
    
  63. }
    
  64. 
    
  65. function isValidKey(nativeEvent: KeyboardEvent): boolean {
    
  66.   const {metaKey, altKey, ctrlKey} = nativeEvent;
    
  67.   return !(metaKey || (!isMac && altKey) || ctrlKey);
    
  68. }
    
  69. 
    
  70. function isTextInput(nativeEvent: KeyboardEvent): boolean {
    
  71.   const {key, target} = nativeEvent;
    
  72.   if (key === 'Tab' || key === 'Escape') {
    
  73.     return false;
    
  74.   }
    
  75.   const {isContentEditable, tagName} = (target: any);
    
  76.   return tagName === 'INPUT' || tagName === 'TEXTAREA' || isContentEditable;
    
  77. }
    
  78. 
    
  79. function handleGlobalFocusVisibleEvent(
    
  80.   nativeEvent: MouseEvent | TouchEvent | KeyboardEvent,
    
  81. ): void {
    
  82.   if (nativeEvent.type === 'keydown') {
    
  83.     if (isValidKey(((nativeEvent: any): KeyboardEvent))) {
    
  84.       isGlobalFocusVisible = true;
    
  85.     }
    
  86.   } else {
    
  87.     const nodeName = (nativeEvent.target: any).nodeName;
    
  88.     // Safari calls mousemove/pointermove events when you tab out of the active
    
  89.     // Safari frame.
    
  90.     if (nodeName === 'HTML') {
    
  91.       return;
    
  92.     }
    
  93.     // Handle all the other mouse/touch/pointer events
    
  94.     isGlobalFocusVisible = false;
    
  95.   }
    
  96. }
    
  97. 
    
  98. function handleFocusVisibleTargetEvents(
    
  99.   event: SyntheticEvent<EventTarget>,
    
  100.   callback: boolean => void,
    
  101. ): void {
    
  102.   if (event.type === 'keydown') {
    
  103.     const {nativeEvent} = (event: any);
    
  104.     if (isValidKey(nativeEvent) && !isTextInput(nativeEvent)) {
    
  105.       callback(true);
    
  106.     }
    
  107.   } else {
    
  108.     callback(false);
    
  109.   }
    
  110. }
    
  111. 
    
  112. function isRelatedTargetWithin(
    
  113.   focusWithinTarget: Object,
    
  114.   relatedTarget: null | EventTarget,
    
  115. ): boolean {
    
  116.   if (relatedTarget == null) {
    
  117.     return false;
    
  118.   }
    
  119.   // As the focusWithinTarget can be a Scope Instance (experimental API),
    
  120.   // we need to use the containsNode() method. Otherwise, focusWithinTarget
    
  121.   // must be a Node, which means we can use the contains() method.
    
  122.   return typeof focusWithinTarget.containsNode === 'function'
    
  123.     ? focusWithinTarget.containsNode(relatedTarget)
    
  124.     : focusWithinTarget.contains(relatedTarget);
    
  125. }
    
  126. 
    
  127. function setFocusVisibleListeners(
    
  128.   // $FlowFixMe[missing-local-annot]
    
  129.   focusVisibleHandles,
    
  130.   focusTarget: EventTarget,
    
  131.   callback: boolean => void,
    
  132. ) {
    
  133.   focusVisibleHandles.forEach(focusVisibleHandle => {
    
  134.     focusVisibleHandle.setListener(focusTarget, event =>
    
  135.       handleFocusVisibleTargetEvents(event, callback),
    
  136.     );
    
  137.   });
    
  138. }
    
  139. 
    
  140. function useFocusVisibleInputHandles() {
    
  141.   return [
    
  142.     useEvent('mousedown'),
    
  143.     useEvent(hasPointerEvents ? 'pointerdown' : 'touchstart'),
    
  144.     useEvent('keydown'),
    
  145.   ];
    
  146. }
    
  147. 
    
  148. function useFocusLifecycles() {
    
  149.   useEffect(() => {
    
  150.     if (!hasTrackedGlobalFocusVisible) {
    
  151.       hasTrackedGlobalFocusVisible = true;
    
  152.       trackGlobalFocusVisible();
    
  153.     }
    
  154.   }, []);
    
  155. }
    
  156. 
    
  157. export function useFocus(
    
  158.   focusTargetRef: {current: null | Node},
    
  159.   {
    
  160.     disabled,
    
  161.     onBlur,
    
  162.     onFocus,
    
  163.     onFocusChange,
    
  164.     onFocusVisibleChange,
    
  165.   }: UseFocusOptions,
    
  166. ): void {
    
  167.   // Setup controlled state for this useFocus hook
    
  168.   const stateRef = useRef<null | {
    
  169.     isFocused: boolean,
    
  170.     isFocusVisible: boolean,
    
  171.   }>({isFocused: false, isFocusVisible: false});
    
  172.   const focusHandle = useEvent('focusin');
    
  173.   const blurHandle = useEvent('focusout');
    
  174.   const focusVisibleHandles = useFocusVisibleInputHandles();
    
  175. 
    
  176.   useLayoutEffect(() => {
    
  177.     const focusTarget = focusTargetRef.current;
    
  178.     const state = stateRef.current;
    
  179. 
    
  180.     if (focusTarget !== null && state !== null && focusTarget.nodeType === 1) {
    
  181.       // Handle focus visible
    
  182.       setFocusVisibleListeners(
    
  183.         focusVisibleHandles,
    
  184.         focusTarget,
    
  185.         isFocusVisible => {
    
  186.           if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
    
  187.             state.isFocusVisible = isFocusVisible;
    
  188.             if (onFocusVisibleChange) {
    
  189.               onFocusVisibleChange(isFocusVisible);
    
  190.             }
    
  191.           }
    
  192.         },
    
  193.       );
    
  194. 
    
  195.       // Handle focus
    
  196.       focusHandle.setListener(focusTarget, (event: FocusEvent) => {
    
  197.         if (disabled === true) {
    
  198.           return;
    
  199.         }
    
  200.         if (!state.isFocused && focusTarget === event.target) {
    
  201.           state.isFocused = true;
    
  202.           state.isFocusVisible = isGlobalFocusVisible;
    
  203.           if (onFocus) {
    
  204.             onFocus(event);
    
  205.           }
    
  206.           if (onFocusChange) {
    
  207.             onFocusChange(true);
    
  208.           }
    
  209.           if (state.isFocusVisible && onFocusVisibleChange) {
    
  210.             onFocusVisibleChange(true);
    
  211.           }
    
  212.         }
    
  213.       });
    
  214. 
    
  215.       // Handle blur
    
  216.       blurHandle.setListener(focusTarget, (event: FocusEvent) => {
    
  217.         if (disabled === true) {
    
  218.           return;
    
  219.         }
    
  220.         if (state.isFocused) {
    
  221.           state.isFocused = false;
    
  222.           state.isFocusVisible = isGlobalFocusVisible;
    
  223.           if (onBlur) {
    
  224.             onBlur(event);
    
  225.           }
    
  226.           if (onFocusChange) {
    
  227.             onFocusChange(false);
    
  228.           }
    
  229.           if (state.isFocusVisible && onFocusVisibleChange) {
    
  230.             onFocusVisibleChange(false);
    
  231.           }
    
  232.         }
    
  233.       });
    
  234.     }
    
  235.   }, [
    
  236.     blurHandle,
    
  237.     disabled,
    
  238.     focusHandle,
    
  239.     focusTargetRef,
    
  240.     focusVisibleHandles,
    
  241.     onBlur,
    
  242.     onFocus,
    
  243.     onFocusChange,
    
  244.     onFocusVisibleChange,
    
  245.   ]);
    
  246. 
    
  247.   // Mount/Unmount logic
    
  248.   useFocusLifecycles();
    
  249. }
    
  250. 
    
  251. export function useFocusWithin<T>(
    
  252.   focusWithinTargetRef:
    
  253.     | {current: null | T}
    
  254.     | ((focusWithinTarget: null | T) => void),
    
  255.   {
    
  256.     disabled,
    
  257.     onAfterBlurWithin,
    
  258.     onBeforeBlurWithin,
    
  259.     onBlurWithin,
    
  260.     onFocusWithin,
    
  261.     onFocusWithinChange,
    
  262.     onFocusWithinVisibleChange,
    
  263.   }: UseFocusWithinOptions,
    
  264. ): (focusWithinTarget: null | T) => void {
    
  265.   // Setup controlled state for this useFocus hook
    
  266.   const stateRef = useRef<null | {
    
  267.     isFocused: boolean,
    
  268.     isFocusVisible: boolean,
    
  269.   }>({isFocused: false, isFocusVisible: false});
    
  270.   const focusHandle = useEvent('focusin');
    
  271.   const blurHandle = useEvent('focusout');
    
  272.   const afterBlurHandle = useEvent('afterblur');
    
  273.   const beforeBlurHandle = useEvent('beforeblur');
    
  274.   const focusVisibleHandles = useFocusVisibleInputHandles();
    
  275. 
    
  276.   const useFocusWithinRef = useCallback(
    
  277.     (focusWithinTarget: null | T) => {
    
  278.       // Handle the incoming focusTargetRef. It can be either a function ref
    
  279.       // or an object ref.
    
  280.       if (typeof focusWithinTargetRef === 'function') {
    
  281.         focusWithinTargetRef(focusWithinTarget);
    
  282.       } else {
    
  283.         focusWithinTargetRef.current = focusWithinTarget;
    
  284.       }
    
  285.       const state = stateRef.current;
    
  286. 
    
  287.       if (focusWithinTarget !== null && state !== null) {
    
  288.         // Handle focus visible
    
  289.         setFocusVisibleListeners(
    
  290.           focusVisibleHandles,
    
  291.           // $FlowFixMe[incompatible-call] focusWithinTarget is not null here
    
  292.           focusWithinTarget,
    
  293.           isFocusVisible => {
    
  294.             if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
    
  295.               state.isFocusVisible = isFocusVisible;
    
  296.               if (onFocusWithinVisibleChange) {
    
  297.                 onFocusWithinVisibleChange(isFocusVisible);
    
  298.               }
    
  299.             }
    
  300.           },
    
  301.         );
    
  302. 
    
  303.         // Handle focus
    
  304.         // $FlowFixMe[incompatible-call] focusWithinTarget is not null here
    
  305.         focusHandle.setListener(focusWithinTarget, (event: FocusEvent) => {
    
  306.           if (disabled) {
    
  307.             return;
    
  308.           }
    
  309.           if (!state.isFocused) {
    
  310.             state.isFocused = true;
    
  311.             state.isFocusVisible = isGlobalFocusVisible;
    
  312.             if (onFocusWithinChange) {
    
  313.               onFocusWithinChange(true);
    
  314.             }
    
  315.             if (state.isFocusVisible && onFocusWithinVisibleChange) {
    
  316.               onFocusWithinVisibleChange(true);
    
  317.             }
    
  318.           }
    
  319.           if (!state.isFocusVisible && isGlobalFocusVisible) {
    
  320.             state.isFocusVisible = isGlobalFocusVisible;
    
  321.             if (onFocusWithinVisibleChange) {
    
  322.               onFocusWithinVisibleChange(true);
    
  323.             }
    
  324.           }
    
  325.           if (onFocusWithin) {
    
  326.             onFocusWithin(event);
    
  327.           }
    
  328.         });
    
  329. 
    
  330.         // Handle blur
    
  331.         // $FlowFixMe[incompatible-call] focusWithinTarget is not null here
    
  332.         blurHandle.setListener(focusWithinTarget, (event: FocusEvent) => {
    
  333.           if (disabled) {
    
  334.             return;
    
  335.           }
    
  336.           const {relatedTarget} = (event.nativeEvent: any);
    
  337. 
    
  338.           if (
    
  339.             state.isFocused &&
    
  340.             !isRelatedTargetWithin(focusWithinTarget, relatedTarget)
    
  341.           ) {
    
  342.             state.isFocused = false;
    
  343.             if (onFocusWithinChange) {
    
  344.               onFocusWithinChange(false);
    
  345.             }
    
  346.             if (state.isFocusVisible && onFocusWithinVisibleChange) {
    
  347.               onFocusWithinVisibleChange(false);
    
  348.             }
    
  349.             if (onBlurWithin) {
    
  350.               onBlurWithin(event);
    
  351.             }
    
  352.           }
    
  353.         });
    
  354. 
    
  355.         // Handle before blur. This is a special
    
  356.         // React provided event.
    
  357.         // $FlowFixMe[incompatible-call] focusWithinTarget is not null here
    
  358.         beforeBlurHandle.setListener(focusWithinTarget, (event: FocusEvent) => {
    
  359.           if (disabled) {
    
  360.             return;
    
  361.           }
    
  362.           if (onBeforeBlurWithin) {
    
  363.             onBeforeBlurWithin(event);
    
  364.             // Add an "afterblur" listener on document. This is a special
    
  365.             // React provided event.
    
  366.             afterBlurHandle.setListener(
    
  367.               document,
    
  368.               (afterBlurEvent: FocusEvent) => {
    
  369.                 if (onAfterBlurWithin) {
    
  370.                   onAfterBlurWithin(afterBlurEvent);
    
  371.                 }
    
  372.                 // Clear listener on document
    
  373.                 afterBlurHandle.setListener(document, null);
    
  374.               },
    
  375.             );
    
  376.           }
    
  377.         });
    
  378.       }
    
  379.     },
    
  380.     [
    
  381.       afterBlurHandle,
    
  382.       beforeBlurHandle,
    
  383.       blurHandle,
    
  384.       disabled,
    
  385.       focusHandle,
    
  386.       focusWithinTargetRef,
    
  387.       onAfterBlurWithin,
    
  388.       onBeforeBlurWithin,
    
  389.       onBlurWithin,
    
  390.       onFocusWithin,
    
  391.       onFocusWithinChange,
    
  392.       onFocusWithinVisibleChange,
    
  393.     ],
    
  394.   );
    
  395. 
    
  396.   // Mount/Unmount logic
    
  397.   useFocusLifecycles();
    
  398. 
    
  399.   return useFocusWithinRef;
    
  400. }