Skip to content

Commit 2e47bf2

Browse files
committed
Merge branch 'autocomplete_audit_for_release' of github.com:adobe/react-spectrum into subdialogs
2 parents 5291b18 + 1bd7004 commit 2e47bf2

File tree

7 files changed

+70
-5
lines changed

7 files changed

+70
-5
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,24 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state:
6767
let collectionId = useId();
6868
let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
6969
let delayNextActiveDescendant = useRef(false);
70+
let queuedActiveDescendant = useRef(null);
7071
let lastCollectionNode = useRef<HTMLElement>(null);
7172

7273
let updateActiveDescendant = useEffectEvent((e) => {
7374
let {target} = e;
75+
if (queuedActiveDescendant.current === target.id) {
76+
return;
77+
}
78+
7479
clearTimeout(timeout.current);
7580
e.stopPropagation();
7681

7782
if (target !== collectionRef.current) {
7883
if (delayNextActiveDescendant.current) {
84+
queuedActiveDescendant.current = target.id;
7985
timeout.current = setTimeout(() => {
8086
state.setFocusedNodeId(target.id);
87+
queuedActiveDescendant.current = null;
8188
}, 500);
8289
} else {
8390
state.setFocusedNodeId(target.id);
@@ -159,7 +166,12 @@ export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state:
159166
// Early return for Escape here so it doesn't leak the Escape event from the simulated collection event below and
160167
// close the dialog prematurely. Ideally that should be up to the discretion of the input element hence the check
161168
// for isPropagationStopped
169+
// Also set the inputValue to '' to cover Firefox case where Esc doesn't actually clear searchfields. Normally we already
170+
// handle this in useSearchField, but we are directly setting the inputValue on the input element in RAC Autocomplete instead of
171+
// passing it to the SearchField via props. This means that a controlled value set on the Autocomplete isn't synced up with the
172+
// SearchField until the user makes a change to the field's value via typing
162173
if (e.isPropagationStopped()) {
174+
state.setInputValue('');
163175
return;
164176
}
165177
break;

packages/@react-aria/searchfield/src/useSearchField.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ export function useSearchField(
7373
}
7474

7575
if (key === 'Escape') {
76-
if (state.value === '') {
76+
// Also check the inputRef value for the case where the value was set directly on the input element instead of going through
77+
// the hook
78+
if (state.value === '' && (!inputRef.current || inputRef.current.value === '')) {
7779
e.continuePropagation();
7880
} else {
7981
state.setValue('');

packages/@react-aria/selection/src/useSelectableCollection.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, focusWithoutScrolling, isCtrlKeyPressed, mergeProps, scrollIntoView, scrollIntoViewport, UPDATE_ACTIVEDESCENDANT, useEffectEvent, useEvent, useRouter, useUpdateEffect} from '@react-aria/utils';
13+
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, focusWithoutScrolling, isCtrlKeyPressed, mergeProps, scrollIntoView, scrollIntoViewport, UPDATE_ACTIVEDESCENDANT, useEffectEvent, useEvent, useRouter, useUpdateLayoutEffect} from '@react-aria/utils';
1414
import {DOMAttributes, FocusableElement, FocusStrategy, Key, KeyboardDelegate, RefObject} from '@react-types/shared';
1515
import {flushSync} from 'react-dom';
1616
import {FocusEvent, KeyboardEvent, useEffect, useRef} from 'react';
@@ -429,7 +429,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
429429
}
430430
});
431431

432-
useUpdateEffect(() => {
432+
useUpdateLayoutEffect(() => {
433433
if (shouldVirtualFocusFirst.current) {
434434
updateActiveDescendant();
435435
}
@@ -445,7 +445,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
445445
}
446446
});
447447

448-
useUpdateEffect(() => {
448+
useUpdateLayoutEffect(() => {
449449
resetFocusFirstFlag();
450450
}, [manager.focusedKey, resetFocusFirstFlag]);
451451

packages/@react-aria/selection/src/useSelectableItem.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
160160
};
161161

162162
// Focus the associated DOM node when this item becomes the focusedKey
163+
// TODO: can't make this useLayoutEffect bacause it breaks menus inside dialogs
164+
// However, if this is a useEffect, it runs twice and dispatches two UPDATE_ACTIVEDESCENDANT and immediately sets
165+
// aria-activeDescendant in useAutocomplete... I've worked around this for now
163166
useEffect(() => {
164167
let isFocused = key === manager.focusedKey;
165168
if (isFocused && manager.isFocused) {

packages/@react-aria/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export {useGlobalListeners} from './useGlobalListeners';
2424
export {useLabels} from './useLabels';
2525
export {useObjectRef} from './useObjectRef';
2626
export {useUpdateEffect} from './useUpdateEffect';
27+
export {useUpdateLayoutEffect} from './useUpdateLayoutEffect';
2728
export {useLayoutEffect} from './useLayoutEffect';
2829
export {useResizeObserver} from './useResizeObserver';
2930
export {useSyncRef} from './useSyncRef';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2020 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {EffectCallback, useRef} from 'react';
14+
import {useLayoutEffect} from './useLayoutEffect';
15+
16+
// Like useLayoutEffect, but only called for updates after the initial render.
17+
export function useUpdateLayoutEffect(effect: EffectCallback, dependencies: any[]) {
18+
const isInitialMount = useRef(true);
19+
const lastDeps = useRef<any[] | null>(null);
20+
21+
useLayoutEffect(() => {
22+
isInitialMount.current = true;
23+
return () => {
24+
isInitialMount.current = false;
25+
};
26+
}, []);
27+
28+
useLayoutEffect(() => {
29+
if (isInitialMount.current) {
30+
isInitialMount.current = false;
31+
} else if (!lastDeps.current || dependencies.some((dep, i) => !Object.is(dep, lastDeps[i]))) {
32+
effect();
33+
}
34+
lastDeps.current = dependencies;
35+
// eslint-disable-next-line react-hooks/exhaustive-deps
36+
}, dependencies);
37+
}

packages/react-aria-components/stories/Autocomplete.stories.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ for (let i = 0; i < 50; i++) {
410410
// ]
411411
// });
412412
// } else {
413-
children.push({name: `Section ${i}, Item ${j}`, id: `item_${i}${j}`});
413+
children.push({name: `Section ${i}, Item ${j}`, id: `item_${i}_${j}`});
414414
// }
415415
}
416416

@@ -534,6 +534,11 @@ export const AutocompleteInPopover = {
534534
disable: true
535535
}
536536
}
537+
},
538+
parameters: {
539+
description: {
540+
data: 'Menu is single selection so only the latest selected option will show the selected style'
541+
}
537542
}
538543
};
539544

@@ -578,5 +583,10 @@ export const AutocompleteInPopoverDialogTrigger = {
578583
disable: true
579584
}
580585
}
586+
},
587+
parameters: {
588+
description: {
589+
data: 'Menu is single selection so only the latest selected option will show the selected style'
590+
}
581591
}
582592
};

0 commit comments

Comments
 (0)