Skip to content

feat: (React Aria) Implement filtering on a per CollectionNode basis #8641

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 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
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
10 changes: 5 additions & 5 deletions packages/@react-aria/autocomplete/src/useAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ export interface CollectionOptions extends DOMProps, AriaLabelingProps {
}

// TODO; For now go with Node here, but maybe pare it down to just the essentials? Value, key, and maybe type?
Copy link
Member Author

Choose a reason for hiding this comment

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

For discussion, might be enough to just provide a subset of node information as mentioned above

Copy link
Contributor

@nwidynski nwidynski Aug 1, 2025

Choose a reason for hiding this comment

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

Hm, would also ask to hold off with this until the RFC. While it is definitely enough to sync collections, I do fancy the idea of being able to attach a node as context on another node - doing it all in one iteration would be great.

PS: I guess key in the end is always enough though since one can just retrieve the node.

Copy link
Member Author

Choose a reason for hiding this comment

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

happy to hold off on paring it down for now, but on the flip side it is easier to go from exposing object containing a subset of the Node's values back to a Node if the need arises.

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, node: Node<unknown>) => 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.
Expand All @@ -43,7 +43,7 @@ export interface AriaAutocompleteProps extends AutocompleteProps {
disableAutoFocusFirst?: 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. */
Expand All @@ -67,7 +67,7 @@ export interface AutocompleteAria<T> {
* @param props - Props for the autocomplete.
* @param state - State for the autocomplete, as returned by `useAutocompleteState`.
*/
export function useAutocomplete<T>(props: AriaAutocompleteOptions, state: AutocompleteState): AutocompleteAria<T> {
export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: AutocompleteState): AutocompleteAria<T> {
let {
inputRef,
collectionRef,
Expand Down Expand Up @@ -318,7 +318,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions, state: Autoco
'aria-label': stringFormatter.format('collectionLabel')
});

let filterFn = useCallback((nodeTextValue: string, node: Node<unknown>) => {
let filterFn = useCallback((nodeTextValue: string, node: Node<T>) => {
if (filter) {
return filter(nodeTextValue, state.inputValue, node);
}
Expand Down
16 changes: 8 additions & 8 deletions packages/dev/s2-docs/src/SearchMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const getCurrentLibrary = (currentPage: Page) => {

export default function SearchMenu(props: SearchMenuProps) {
let {pages, currentPage, toggleShowSearchMenu, closeSearchMenu, isSearchOpen} = props;

const currentLibrary = getCurrentLibrary(currentPage);
let [selectedLibrary, setSelectedLibrary] = useState<'react-spectrum' | 'react-aria' | 'internationalized'>(currentLibrary);
let [searchValue, setSearchValue] = useState('');
Expand All @@ -140,14 +140,14 @@ export default function SearchMenu(props: SearchMenuProps) {
},
{
id: 'react-aria',
label: 'React Aria',
label: 'React Aria',
description: 'Style-free components and hooks for building accessible UIs',
icon: <ReactAriaLogo />
},
{
id: 'internationalized',
label: 'Internationalized',
description: 'Framework-agnostic internationalization utilities',
description: 'Framework-agnostic internationalization utilities',
icon: <InternationalizedLogo />
}
];
Expand All @@ -158,7 +158,7 @@ export default function SearchMenu(props: SearchMenuProps) {
const currentTab = allTabs.splice(currentTabIndex, 1)[0];
allTabs.unshift(currentTab);
}

return allTabs;
};

Expand All @@ -184,13 +184,13 @@ export default function SearchMenu(props: SearchMenuProps) {
} else if (page.url.includes('react-internationalized')) {
library = 'internationalized';
}

return library === selectedLibrary;
})
.map(page => {
const name = page.url.replace(/^\//, '').replace(/\.html$/, '');
const title = page.tableOfContents?.[0]?.title || name;

return {
id: name,
name: title,
Expand Down Expand Up @@ -257,7 +257,7 @@ export default function SearchMenu(props: SearchMenuProps) {

let {contains} = useFilter({sensitivity: 'base'});

let filter: AutocompleteProps['filter'] = (textValue, inputValue) => {
let filter: AutocompleteProps<any>['filter'] = (textValue, inputValue) => {
return textValue != null && contains(textValue, inputValue);
};

Expand Down Expand Up @@ -294,7 +294,7 @@ export default function SearchMenu(props: SearchMenuProps) {

return (
<div
className={style({
className={style({
display: 'grid',
gridTemplateColumns: 'auto 1fr',
alignItems: 'center',
Expand Down
4 changes: 2 additions & 2 deletions packages/dev/s2-docs/src/SearchResultsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface SearchResultsMenuProps {
searchRef: React.RefObject<TextFieldRef<HTMLInputElement> | null>,
showCards: boolean,
renderCardList: () => React.ReactNode,
filter?: AutocompleteProps['filter'],
filter?: AutocompleteProps<any>['filter'],
noResultsText?: (value: string) => string,
closeSearchMenu: () => void,
isPrimary?: boolean
Expand All @@ -48,7 +48,7 @@ function CloseButton({closeSearchMenu}: {closeSearchMenu: () => void}) {
<Close />
</ActionButton>
</Provider>
</div>
</div>
);
}

Expand Down
14 changes: 7 additions & 7 deletions packages/react-aria-components/src/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,24 @@ import React, {createContext, JSX, RefObject, useRef} from 'react';
import {SearchFieldContext} from './SearchField';
import {TextFieldContext} from './TextField';

export interface AutocompleteProps extends AriaAutocompleteProps, SlotProps {}
export interface AutocompleteProps<T> extends AriaAutocompleteProps<T>, SlotProps {}

interface InternalAutocompleteContextValue {
filter?: (nodeTextValue: string, node: Node<unknown>) => boolean,
interface InternalAutocompleteContextValue<T> {
filter?: (nodeTextValue: string, node: Node<T>) => boolean,
collectionProps: CollectionOptions,
collectionRef: RefObject<HTMLElement | null>
}

export const AutocompleteContext = createContext<SlottedContextValue<Partial<AutocompleteProps>>>(null);
export const AutocompleteContext = createContext<SlottedContextValue<Partial<AutocompleteProps<any>>>>(null);
export const AutocompleteStateContext = createContext<AutocompleteState | null>(null);
// This context is to pass the register and filter down to whatever collection component is wrapped by the Autocomplete
// TODO: export from RAC, but rename to something more appropriate
export const UNSTABLE_InternalAutocompleteContext = createContext<InternalAutocompleteContextValue | null>(null);
export const UNSTABLE_InternalAutocompleteContext = createContext<InternalAutocompleteContextValue<any> | null>(null);

/**
* An autocomplete combines a TextField or SearchField with a Menu or ListBox, allowing users to search or filter a list of suggestions.
*/
export function Autocomplete(props: AutocompleteProps): JSX.Element {
export function Autocomplete<T>(props: AutocompleteProps<T>): JSX.Element {
let ctx = useSlottedContext(AutocompleteContext, props.slot);
props = mergeProps(ctx, props);
let {filter, disableAutoFocusFirst} = props;
Expand Down Expand Up @@ -65,7 +65,7 @@ export function Autocomplete(props: AutocompleteProps): JSX.Element {
[TextFieldContext, textFieldProps],
[InputContext, {ref: inputRef}],
[UNSTABLE_InternalAutocompleteContext, {
filter: filterFn,
filter: filterFn as (nodeTextValue: string, node: Node<T>) => boolean,
collectionProps,
collectionRef: mergedCollectionRef
}]
Expand Down