Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c9ee11d
account for loaders in base collection filter
LFDanLu Jun 27, 2025
ac323e9
rough implementation for listbox
LFDanLu Jun 27, 2025
290514a
replace other instances of createLeaf/createBranch to use node classes
LFDanLu Jul 8, 2025
2969511
fix bugs with subdialog filtering, arrow nav, dividers, etc
LFDanLu Jul 8, 2025
d8a6f06
fix case where arrow nav wasnt working post filter
LFDanLu Jul 9, 2025
639acd0
Merge branch 'main' of github.com:adobe/react-spectrum into baseColle…
LFDanLu Jul 22, 2025
6eb1753
update types and class node structure
LFDanLu Jul 22, 2025
d1efa8d
prep stories
LFDanLu Jul 23, 2025
77936ca
fix
LFDanLu Jul 23, 2025
7991977
add autocomplete gridlist filtering
LFDanLu Jul 24, 2025
e615835
taglist filter support
LFDanLu Jul 25, 2025
d02197e
fixing lint
LFDanLu Jul 25, 2025
361286b
fix tag group keyboard nav and lint
LFDanLu Jul 25, 2025
432a43c
adding support for table filtering
LFDanLu Jul 26, 2025
3ec3fd6
fix tableCollection filter so it doesnt need to call filterChildren d…
LFDanLu Jul 28, 2025
4a69d50
create common use nodes for specific filtering patterns
LFDanLu Jul 28, 2025
73a1971
fix ssr
LFDanLu Jul 28, 2025
1ead59b
refactor to accept a node rather than a string in the filter function
LFDanLu Jul 28, 2025
90c2056
fix lint
LFDanLu Jul 28, 2025
3a8301e
make node param in autocomplete non breaking
LFDanLu Jul 31, 2025
45a39c1
Merge branch 'main' of github.com:adobe/react-spectrum into baseColle…
LFDanLu Jul 31, 2025
9d65d5b
adding tests, make sure we only apply autocomplete attributes if the …
LFDanLu Jul 31, 2025
19b695e
prevent breaking change in CollectionBuilder by still accepting strin…
LFDanLu Aug 1, 2025
6066c6c
fix tests and pass submenutrigger node to filterFn
LFDanLu Aug 1, 2025
d2b5e51
small clean up
LFDanLu Aug 1, 2025
2c89783
small fixes
LFDanLu Aug 5, 2025
739e93f
addressing more review comments
LFDanLu Aug 5, 2025
3c2e92c
simplifying setProps logic since we have already have id when calling it
LFDanLu Aug 5, 2025
35b627e
forgot to use generic for autocomplete filter
LFDanLu Aug 5, 2025
9408aa9
ugh docs typescript
LFDanLu Aug 5, 2025
8e75339
review comments
LFDanLu Aug 7, 2025
57e57e0
add example testing the Autocomplete generic
LFDanLu Aug 12, 2025
4d5fde2
fix: Autocomplete context refactor (#8695)
LFDanLu Aug 18, 2025
a2925e4
Merge branch 'main' of github.com:adobe/react-spectrum into baseColle…
LFDanLu Aug 18, 2025
058f820
Merge branch 'baseCollection_filter' of github.com:adobe/react-spectr…
LFDanLu Aug 18, 2025
9b3aadf
support filtering when there are sections in gridlist
LFDanLu Aug 18, 2025
8fffdf8
fix mobile screen reader detection for disabling virtual focus
LFDanLu Aug 18, 2025
113b259
review comments
LFDanLu Aug 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 65 additions & 38 deletions packages/@react-aria/autocomplete/src/useAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
* governing permissions and limitations under the License.
*/

import {AriaLabelingProps, BaseEvent, DOMProps, RefObject} from '@react-types/shared';
import {AriaLabelingProps, BaseEvent, DOMProps, FocusableElement, Node, RefObject} from '@react-types/shared';
import {AriaTextFieldProps} from '@react-aria/textfield';
import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete';
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isCtrlKeyPressed, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useObjectRef} from '@react-aria/utils';
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useLabels, useObjectRef, useSlotId} from '@react-aria/utils';
import {dispatchVirtualBlur, dispatchVirtualFocus, getVirtuallyFocusedElement, moveVirtualFocus} from '@react-aria/focus';
import {getInteractionModality} from '@react-aria/interactions';
// @ts-ignore
Expand All @@ -27,36 +27,44 @@ export interface CollectionOptions extends DOMProps, AriaLabelingProps {
/** Whether typeahead is disabled. */
disallowTypeAhead: boolean
}
export interface AriaAutocompleteProps extends AutocompleteProps {

export interface AriaAutocompleteProps<T> extends AutocompleteProps {
/**
* An optional filter function used to determine if a option should be included in the autocomplete list.
* Include this if the items you are providing to your wrapped collection aren't filtered by default.
*/
filter?: (textValue: string, inputValue: string) => boolean,
filter?: (textValue: string, inputValue: string, node: Node<T>) => boolean,

/**
* Whether or not to focus the first item in the collection after a filter is performed. Note this is only applicable
* if virtual focus behavior is not turned off via `disableVirtualFocus`.
* @default false
*/
disableAutoFocusFirst?: boolean,

/**
* Whether or not to focus the first item in the collection after a filter is performed.
* Whether the autocomplete should disable virtual focus, instead making the wrapped collection directly tabbable.
* @default false
*/
disableAutoFocusFirst?: boolean
disableVirtualFocus?: boolean
}

export interface AriaAutocompleteOptions extends Omit<AriaAutocompleteProps, 'children'> {
export interface AriaAutocompleteOptions<T> extends Omit<AriaAutocompleteProps<T>, 'children'> {
/** The ref for the wrapped collection element. */
inputRef: RefObject<HTMLInputElement | null>,
/** The ref for the wrapped collection element. */
collectionRef: RefObject<HTMLElement | null>
}

export interface AutocompleteAria {
export interface AutocompleteAria<T> {
/** Props for the autocomplete textfield/searchfield element. These should be passed to the textfield/searchfield aria hooks respectively. */
textFieldProps: AriaTextFieldProps,
textFieldProps: AriaTextFieldProps<FocusableElement>,
/** Props for the collection, to be passed to collection's respective aria hook (e.g. useMenu). */
collectionProps: CollectionOptions,
/** Ref to attach to the wrapped collection. */
collectionRef: RefObject<HTMLElement | null>,
/** A filter function that returns if the provided collection node should be filtered out of the collection. */
filter?: (nodeTextValue: string) => boolean
filter?: (nodeTextValue: string, node: Node<T>) => boolean
}

/**
Expand All @@ -65,24 +73,25 @@ export interface AutocompleteAria {
* @param props - Props for the autocomplete.
* @param state - State for the autocomplete, as returned by `useAutocompleteState`.
*/
export function useAutocomplete(props: AriaAutocompleteOptions, state: AutocompleteState): AutocompleteAria {
export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: AutocompleteState): AutocompleteAria<T> {
let {
inputRef,
collectionRef,
filter,
disableAutoFocusFirst = false
disableAutoFocusFirst = false,
disableVirtualFocus = false
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bringing back this discussion from #8695:
is it a problem that virtual focus doesn't work with grid collection just yet but will be enabled by default later via #8728? Feels like perhaps we should hold off on RC until I can get all the work in that PR finished

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's another issue here, what does virtual focus look like for grid collections? what if someone puts an editable field in it? Maybe it's better that it's not virtual by default?

} = props;

let collectionId = useId();
let collectionId = useSlotId();
let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
let delayNextActiveDescendant = useRef(false);
let queuedActiveDescendant = useRef<string | null>(null);
let lastCollectionNode = useRef<HTMLElement>(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 shouldUseVirtualFocus = getInteractionModality() !== 'virtual';

let isMobileScreenReader = getInteractionModality() === 'virtual' && (isIOS() || isAndroid());
let shouldUseVirtualFocus = !isMobileScreenReader && !disableVirtualFocus;
useEffect(() => {
return () => clearTimeout(timeout.current);
}, []);
Expand Down Expand Up @@ -252,15 +261,17 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl
}

let shouldPerformDefaultAction = true;
if (focusedNodeId == null) {
shouldPerformDefaultAction = collectionRef.current?.dispatchEvent(
new KeyboardEvent(e.nativeEvent.type, e.nativeEvent)
) || false;
} else {
let item = document.getElementById(focusedNodeId);
shouldPerformDefaultAction = item?.dispatchEvent(
new KeyboardEvent(e.nativeEvent.type, e.nativeEvent)
) || false;
if (collectionRef.current !== null) {
if (focusedNodeId == null) {
shouldPerformDefaultAction = collectionRef.current?.dispatchEvent(
new KeyboardEvent(e.nativeEvent.type, e.nativeEvent)
) || false;
} else {
let item = document.getElementById(focusedNodeId);
shouldPerformDefaultAction = item?.dispatchEvent(
new KeyboardEvent(e.nativeEvent.type, e.nativeEvent)
) || false;
}
}

if (shouldPerformDefaultAction) {
Expand All @@ -280,6 +291,9 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl
}
break;
}
} else {
// TODO: check if we can do this, want to stop textArea from using its default Enter behavior so items are properly triggered
e.preventDefault();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only do it for Enter?
also, maybe allow Opt+Enter? or essentially only if no modifies are pressed?

}
};

Expand Down Expand Up @@ -316,9 +330,9 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl
'aria-label': stringFormatter.format('collectionLabel')
});

let filterFn = useCallback((nodeTextValue: string) => {
let filterFn = useCallback((nodeTextValue: string, node: Node<T>) => {
if (filter) {
return filter(nodeTextValue, state.inputValue);
return filter(nodeTextValue, state.inputValue, node);
}

return true;
Expand Down Expand Up @@ -352,25 +366,38 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl
}
};

return {
textFieldProps: {
value: state.inputValue,
onChange,
onKeyDown,
autoComplete: 'off',
'aria-haspopup': 'listbox',
// Only apply the autocomplete specific behaviors if the collection component wrapped by it is actually
// being filtered/allows filtering by the Autocomplete.
let textFieldProps = {
value: state.inputValue,
onChange
} as AriaTextFieldProps<FocusableElement>;

let virtualFocusProps = {
onKeyDown,
'aria-activedescendant': state.focusedNodeId ?? undefined,
onBlur,
onFocus
};

if (collectionId) {
textFieldProps = {
...textFieldProps,
...(shouldUseVirtualFocus && virtualFocusProps),
enterKeyHint: 'go',
'aria-controls': collectionId,
// TODO: readd proper logic for completionMode = complete (aria-autocomplete: both)
'aria-autocomplete': 'list',
'aria-activedescendant': state.focusedNodeId ?? undefined,
// This disable's iOS's autocorrect suggestions, since the autocomplete provides its own suggestions.
autoCorrect: 'off',
// This disable's the macOS Safari spell check auto corrections.
spellCheck: 'false',
enterKeyHint: 'go',
onBlur,
onFocus
},
autoComplete: 'off'
};
}

return {
textFieldProps,
collectionProps: mergeProps(collectionProps, {
shouldUseVirtualFocus,
disallowTypeAhead: true
Expand Down
Loading