Skip to content

Commit 2f591d5

Browse files
committed
move event listener attachment to an effect
1 parent 4cd669d commit 2f591d5

File tree

1 file changed

+19
-16
lines changed

1 file changed

+19
-16
lines changed

packages/@react-aria/autocomplete/src/useAutocomplete.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {AriaLabelingProps, BaseEvent, DOMProps, FocusableElement, FocusEvents, KeyboardEvents, Node, RefObject, ValueBase} from '@react-types/shared';
1414
import {AriaTextFieldProps} from '@react-aria/textfield';
1515
import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete';
16-
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useObjectRef} from '@react-aria/utils';
16+
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useLayoutEffect, useObjectRef} from '@react-aria/utils';
1717
import {dispatchVirtualBlur, dispatchVirtualFocus, getVirtuallyFocusedElement, moveVirtualFocus} from '@react-aria/focus';
1818
import {getInteractionModality} from '@react-aria/interactions';
1919
// @ts-ignore
@@ -92,7 +92,6 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
9292
let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
9393
let delayNextActiveDescendant = useRef(false);
9494
let queuedActiveDescendant = useRef<string | null>(null);
95-
let lastCollectionNode = useRef<HTMLElement>(null);
9695

9796
// For mobile screen readers, we don't want virtual focus, instead opting to disable FocusScope's restoreFocus and manually
9897
// moving focus back to the subtriggers
@@ -106,7 +105,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
106105
return () => clearTimeout(timeout.current);
107106
}, []);
108107

109-
let updateActiveDescendant = useCallback((e: Event) => {
108+
let updateActiveDescendantEvent = useEffectEvent((e: Event) => {
110109
// Ensure input is focused if the user clicks on the collection directly.
111110
if (!e.isTrusted && shouldUseVirtualFocus && inputRef.current && getActiveElement(getOwnerDocument(inputRef.current)) !== inputRef.current) {
112111
inputRef.current.focus();
@@ -138,29 +137,33 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
138137
}
139138

140139
delayNextActiveDescendant.current = false;
141-
}, [shouldUseVirtualFocus, inputRef, collectionRef, state]);
140+
});
142141

143-
let callbackRef = useCallback((collectionNode) => {
144-
if (collectionNode != null) {
145-
// When typing forward, we want to delay the setting of active descendant to not interrupt the native screen reader announcement
146-
// of the letter you just typed. If we recieve another focus event then we clear the queued update
147-
// We track lastCollectionNode to do proper cleanup since callbackRefs just pass null when unmounting. This also handles
148-
// React 19's extra call of the callback ref in strict mode
149-
lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant);
150-
lastCollectionNode.current = collectionNode;
151-
collectionNode.addEventListener('focusin', updateActiveDescendant);
142+
let [collectionNode, setCollectionNode] = useState<HTMLElement | null>(null);
143+
let callbackRef = useCallback((node) => {
144+
setCollectionNode(node);
145+
if (node != null) {
152146
// If useSelectableCollection isn't passed shouldUseVirtualFocus even when useAutocomplete provides it
153147
// that means the collection doesn't support it (e.g. Table). If that is the case, we need to disable it here regardless
154148
// of what the user's provided so that the input doesn't recieve the onKeyDown and autocomplete props.
155-
if (collectionNode.getAttribute('tabindex') != null) {
149+
if (node.getAttribute('tabindex') != null) {
156150
setShouldUseVirtualFocus(false);
157151
}
158152
setHasCollection(true);
159153
} else {
160-
lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant);
161154
setHasCollection(false);
162155
}
163-
}, [updateActiveDescendant]);
156+
}, []);
157+
useLayoutEffect(() => {
158+
if (collectionNode != null) {
159+
// When typing forward, we want to delay the setting of active descendant to not interrupt the native screen reader announcement
160+
// of the letter you just typed. If we recieve another focus event then we clear the queued update
161+
collectionNode.addEventListener('focusin', updateActiveDescendantEvent);
162+
}
163+
return () => {
164+
collectionNode?.removeEventListener('focusin', updateActiveDescendantEvent);
165+
};
166+
}, [collectionNode]);
164167

165168
// Make sure to memo so that React doesn't keep registering a new event listeners on every rerender of the wrapped collection
166169
let mergedCollectionRef = useObjectRef(useMemo(() => mergeRefs(collectionRef, callbackRef), [collectionRef, callbackRef]));

0 commit comments

Comments
 (0)