Repository: adobe/react-spectrum
File: packages/@react-aria/autocomplete/src/useAutocomplete.ts (TypeScript)

Code before:

  let collectionId = useId();
  let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  let delayNextActiveDescendant = useRef(false);
  let queuedActiveDescendant = useRef<string | null>(null);

  // For mobile screen readers, we don't want virtual focus, instead opting to disable FocusScope's restoreFocus and manually
  // moving focus back to the subtriggers
  let isMobileScreenReader = getInteractionModality() === 'virtual' && (isIOS() || isAndroid());
  let [shouldUseVirtualFocus, setShouldUseVirtualFocus] = useState(!isMobileScreenReader && !disableVirtualFocus);
  // Tracks if a collection has been connected to the autocomplete. If false, we don't want to add various attributes to the autocomplete input
  // since it isn't attached to a filterable collection (e.g. Tabs)
  let [hasCollection, setHasCollection] = useState(false);

  useEffect(() => {
    return () => clearTimeout(timeout.current);
  }, []);

  let updateActiveDescendantEvent = useEffectEvent((e: Event) => {
    // Ensure input is focused if the user clicks on the collection directly.
    if (!e.isTrusted && shouldUseVirtualFocus && inputRef.current && getActiveElement(getOwnerDocument(inputRef.current)) !== inputRef.current) {
      inputRef.current.focus();
    }

    let target = e.target as Element | null;
    if (e.isTrusted || !target || queuedActiveDescendant.current === target.id) {
      return;
    }

    clearTimeout(timeout.current);
    if (target !== collectionRef.current) {
      if (delayNextActiveDescendant.current) {
        queuedActiveDescendant.current = target.id;
        timeout.current = setTimeout(() => {
          state.setFocusedNodeId(target.id);
        }, 500);
      } else {
        queuedActiveDescendant.current = target.id;
        state.setFocusedNodeId(target.id);
      }
    } else if (queuedActiveDescendant.current && !document.getElementById(queuedActiveDescendant.current)) {
      // If we recieve a focus event refocusing the collection, either we have newly refocused the input and are waiting for the
      // wrapped collection to refocus the previously focused node if any OR
      // we are in a state where we've filtered to such a point that there aren't any matching items in the collection to focus.
      // In this case we want to clear tracked item if any and clear active descendant
      queuedActiveDescendant.current = null;
      state.setFocusedNodeId(null);
    }

    delayNextActiveDescendant.current = false;
  });

Review comment (suggestion):
Realized one case this won't handle: if the last pointer event happened before the autocomplete mounted. We may need to track the pointer type globally. useFocusVisible does this already but only stores "pointer" rather than the actual pointer type...

Code after:

  let collectionId = useId();
  let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  let delayNextActiveDescendant = useRef(false);
  let queuedActiveDescendant = useRef<string | null>(null);
  let lastPointerType = useRef<string | null>(null);

  // For mobile screen readers, we don't want virtual focus, instead opting to disable FocusScope's restoreFocus and manually
  // moving focus back to the subtriggers
  let isMobileScreenReader = getInteractionModality() === 'virtual' && (isIOS() || isAndroid());
  let [shouldUseVirtualFocus, setShouldUseVirtualFocus] = useState(!isMobileScreenReader && !disableVirtualFocus);
  // Tracks if a collection has been connected to the autocomplete. If false, we don't want to add various attributes to the autocomplete input
  // since it isn't attached to a filterable collection (e.g. Tabs)
  let [hasCollection, setHasCollection] = useState(false);

  useEffect(() => {
    return () => clearTimeout(timeout.current);
  }, []);

  useEffect(() => {
    let handlePointerDown = (e: PointerEvent) => {
      lastPointerType.current = e.pointerType;
    };

    if (typeof PointerEvent !== 'undefined') {
      document.addEventListener('pointerdown', handlePointerDown, true);
      return () => {
        document.removeEventListener('pointerdown', handlePointerDown, true);
      };
    }
  }, []);

  let updateActiveDescendantEvent = useEffectEvent((e: Event) => {
    // Ensure input is focused if the user clicks on the collection directly.
    // don't trigger on touch so that mobile keyboard doesnt appear when tapping on options
    if (!e.isTrusted && shouldUseVirtualFocus && inputRef.current && getActiveElement(getOwnerDocument(inputRef.current)) !== inputRef.current && lastPointerType.current !== 'touch') {
      inputRef.current.focus();
    }

    let target = e.target as Element | null;
    if (e.isTrusted || !target || queuedActiveDescendant.current === target.id) {
      return;
    }

    clearTimeout(timeout.current);
    if (target !== collectionRef.current) {
      if (delayNextActiveDescendant.current) {
        queuedActiveDescendant.current = target.id;
        timeout.current = setTimeout(() => {
          state.setFocusedNodeId(target.id);
        }, 500);