Skip to content

Commit 3a8301e

Browse files
committed
make node param in autocomplete non breaking
1 parent 90c2056 commit 3a8301e

File tree

9 files changed

+53
-35
lines changed

9 files changed

+53
-35
lines changed

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ export interface CollectionOptions extends DOMProps, AriaLabelingProps {
2727
/** Whether typeahead is disabled. */
2828
disallowTypeAhead: boolean
2929
}
30+
31+
// TODO: is in beta so technically could replace textValue with Node if we are comfortable with that
3032
export interface AriaAutocompleteProps extends AutocompleteProps {
3133
/**
3234
* An optional filter function used to determine if a option should be included in the autocomplete list.
3335
* Include this if the items you are providing to your wrapped collection aren't filtered by default.
3436
*/
35-
filter?: (textValue: string, inputValue: string) => boolean,
37+
filter?: (textValue: string, inputValue: string, node: Node<unknown>) => boolean,
3638

3739
/**
3840
* Whether or not to focus the first item in the collection after a filter is performed.
@@ -55,8 +57,9 @@ export interface AutocompleteAria {
5557
collectionProps: CollectionOptions,
5658
/** Ref to attach to the wrapped collection. */
5759
collectionRef: RefObject<HTMLElement | null>,
60+
// TODO: same as above, replace nodeTextValue?
5861
/** A filter function that returns if the provided collection node should be filtered out of the collection. */
59-
filter?: (node: Node<unknown>) => boolean
62+
filter?: (nodeTextValue: string, node: Node<unknown>) => boolean
6063
}
6164

6265
/**
@@ -316,9 +319,9 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl
316319
'aria-label': stringFormatter.format('collectionLabel')
317320
});
318321

319-
let filterFn = useCallback((node: Node<unknown>) => {
322+
let filterFn = useCallback((nodeTextValue: string, node: Node<unknown>) => {
320323
if (filter) {
321-
return filter(node.textValue, state.inputValue);
324+
return filter(nodeTextValue, state.inputValue, node);
322325
}
323326

324327
return true;

packages/@react-aria/collections/src/BaseCollection.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export type Mutable<T> = {
1717
-readonly[P in keyof T]: T[P]
1818
}
1919

20+
type FilterFn<T> = (textValue: string, node: Node<T>) => boolean;
21+
2022
/** An immutable object representing a Node in a Collection. */
2123
export class CollectionNode<T> implements Node<T> {
2224
readonly type: string;
@@ -69,7 +71,7 @@ export class CollectionNode<T> implements Node<T> {
6971
return node;
7072
}
7173

72-
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (node: Node<T>) => boolean): CollectionNode<T> | null {
74+
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: FilterFn<T>): CollectionNode<T> | null {
7375
let [firstKey, lastKey] = filterChildren(collection, newCollection, this.firstChildKey, filterFn);
7476
let newNode: Mutable<CollectionNode<T>> = this.clone();
7577
newNode.firstChildKey = firstKey;
@@ -82,7 +84,7 @@ export class CollectionNode<T> implements Node<T> {
8284
// Perhaps this filter logic should be in CollectionNode instead and the current logic of CollectionNode's filter should move to Table
8385
export class FilterLessNode<T> extends CollectionNode<T> {
8486
// eslint-disable-next-line @typescript-eslint/no-unused-vars
85-
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (node: Node<T>) => boolean): FilterLessNode<T> | null {
87+
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: FilterFn<T>): FilterLessNode<T> | null {
8688
return this.clone();
8789
}
8890
}
@@ -94,8 +96,8 @@ export class ItemNode<T> extends CollectionNode<T> {
9496
super(ItemNode.type, key);
9597
}
9698

97-
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (node: Node<T>) => boolean): ItemNode<T> | null {
98-
if (filterFn(this)) {
99+
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: FilterFn<T>): ItemNode<T> | null {
100+
if (filterFn(this.textValue, this)) {
99101
return this.clone();
100102
}
101103

@@ -110,7 +112,7 @@ export class SectionNode<T> extends CollectionNode<T> {
110112
super(SectionNode.type, key);
111113
}
112114

113-
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (node: Node<T>) => boolean): SectionNode<T> | null {
115+
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: FilterFn<T>): SectionNode<T> | null {
114116
let filteredSection = super.filter(collection, newCollection, filterFn);
115117
if (filteredSection) {
116118
if (filteredSection.lastChildKey !== null) {
@@ -280,7 +282,7 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
280282
this.frozen = !isSSR;
281283
}
282284

283-
filter(filterFn: (node: Node<T>) => boolean, newCollection?: BaseCollection<T>): BaseCollection<T> {
285+
filter(filterFn: FilterFn, newCollection?: BaseCollection<T>): BaseCollection<T> {
284286
if (newCollection == null) {
285287
newCollection = new BaseCollection<T>();
286288
}
@@ -292,7 +294,7 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
292294
}
293295
}
294296

295-
function filterChildren<T>(collection: BaseCollection<T>, newCollection: BaseCollection<T>, firstChildKey: Key | null, filterFn: (node: Node<T>) => boolean): [Key | null, Key | null] {
297+
function filterChildren<T>(collection: BaseCollection<T>, newCollection: BaseCollection<T>, firstChildKey: Key | null, filterFn: FilterFn): [Key | null, Key | null] {
296298
// loop over the siblings for firstChildKey
297299
// create new nodes based on calling node.filter for each child
298300
// if it returns null then don't include it, otherwise update its prev/next keys

packages/@react-stately/list/src/useListState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function useListState<T extends object>(props: ListProps<T>): ListState<T
7373
/**
7474
* Filters a collection using the provided filter function and returns a new ListState.
7575
*/
76-
export function UNSTABLE_useFilteredListState<T extends object>(state: ListState<T>, filterFn: ((node: Node<T>) => boolean) | null | undefined): ListState<T> {
76+
export function UNSTABLE_useFilteredListState<T extends object>(state: ListState<T>, filterFn: ((nodeValue: string, node: Node<T>) => boolean) | null | undefined): ListState<T> {
7777
let collection = useMemo(() => filterFn ? state.collection.filter!(filterFn) : state.collection, [state.collection, filterFn]);
7878
let selectionManager = state.selectionManager.withCollection(collection);
7979
useFocusedKeyReset(collection, selectionManager);

packages/@react-stately/table/src/useTableState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function useTableState<T extends object>(props: TableStateProps<T>): Tabl
111111
/**
112112
* Filters a collection using the provided filter function and returns a new TableState.
113113
*/
114-
export function UNSTABLE_useFilteredTableState<T extends object>(state: TableState<T>, filterFn: ((node: Node<T>) => boolean) | null | undefined): TableState<T> {
114+
export function UNSTABLE_useFilteredTableState<T extends object>(state: TableState<T>, filterFn: ((nodeValue: string, node: Node<T>) => boolean) | null | undefined): TableState<T> {
115115
let collection = useMemo(() => filterFn ? state.collection.filter!(filterFn) : state.collection, [state.collection, filterFn]) as ITableCollection<T>;
116116
let selectionManager = state.selectionManager.withCollection(collection);
117117
// TODO: handle focus key reset? That logic is in useGridState

packages/@react-types/shared/src/collections.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export interface Collection<T> extends Iterable<T> {
183183
getTextValue?(key: Key): string,
184184

185185
/** Filters the collection using the given function. */
186-
filter?(filterFn: (node: T) => boolean): Collection<T>
186+
filter?(filterFn: (nodeValue: string, node: T) => boolean): Collection<T>
187187
}
188188

189189
export interface Node<T> {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {TextFieldContext} from './TextField';
2323
export interface AutocompleteProps extends AriaAutocompleteProps, SlotProps {}
2424

2525
interface InternalAutocompleteContextValue {
26-
filter?: (node: Node<unknown>) => boolean,
26+
filter?: (nodeTextValue: string, node: Node<unknown>) => boolean,
2727
collectionProps: CollectionOptions,
2828
collectionRef: RefObject<HTMLElement | null>
2929
}

packages/react-aria-components/src/Menu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ class SubmenuTriggerNode<T> extends CollectionNode<T> {
114114
super(SubmenuTriggerNode.type, key);
115115
}
116116

117-
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (node: Node<T>) => boolean): CollectionNode<T> | null {
117+
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (textValue: string, node: Node<T>) => boolean): CollectionNode<T> | null {
118118
let triggerNode = collection.getItem(this.firstChildKey!);
119-
if (triggerNode && filterFn(triggerNode)) {
119+
if (triggerNode && filterFn(triggerNode.textValue, triggerNode)) {
120120
// TODO: perhaps should call super.filter for correctness, but basically add the menu item child of the submenutrigger
121121
// to the keymap so it renders
122122
newCollection.addNode(triggerNode as CollectionNode<T>);

packages/react-aria-components/src/Table.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ class TableCollection<T> extends BaseCollection<T> implements ITableCollection<T
197197
return text.join(' ');
198198
}
199199

200-
filter(filterFn: (node: Node<T>) => boolean): TableCollection<T> {
200+
filter(filterFn: (textValue: string, node: Node<T>) => boolean): TableCollection<T> {
201201
let clone = this.clone();
202202
return super.filter(filterFn, clone) as TableCollection<T>;
203203

@@ -1063,10 +1063,10 @@ class TableRowNode<T> extends CollectionNode<T> {
10631063
super(TableRowNode.type, key);
10641064
}
10651065

1066-
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (node: Node<T>) => boolean): TableRowNode<T> | null {
1066+
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (textValue: string, node: Node<T>) => boolean): TableRowNode<T> | null {
10671067
let cells = collection.getChildren(this.key);
10681068
for (let cell of cells) {
1069-
if (filterFn(cell)) {
1069+
if (filterFn(cell.textValue, cell)) {
10701070
return this.clone();
10711071
}
10721072
}

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

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -944,10 +944,6 @@ AutocompleteWithAsyncListBox.story = {
944944
}
945945
};
946946

947-
// TODO: I'm skipping Breadcrumbs, Tabs for now, not sure it makes sense to filter that via Autocomplete
948-
// Filtering the Taggroup might make sense
949-
// TODO make all of the below examples async loading as well?
950-
951947
export const AutocompleteWithGridList = () => {
952948
return (
953949
<AutocompleteWrapper>
@@ -987,7 +983,8 @@ export const AutocompleteWithTable = () => {
987983
layout={TableLayout}
988984
layoutOptions={{
989985
rowHeight: 25,
990-
headingHeight: 25
986+
headingHeight: 25,
987+
padding: 10
991988
}}>
992989
<Table aria-label="Files" selectionMode="multiple" style={{height: 400, width: 400, overflow: 'auto', scrollPaddingTop: 25}}>
993990
<TableHeader style={{background: 'var(--spectrum-gray-100)', width: '100%', height: '100%'}}>
@@ -999,31 +996,31 @@ export const AutocompleteWithTable = () => {
999996
<Column>Date Modified</Column>
1000997
</TableHeader>
1001998
<TableBody>
1002-
<Row id="1">
999+
<Row id="1" style={{width: 'inherit', height: 'inherit'}}>
10031000
<Cell>
10041001
<MyCheckbox slot="selection" />
10051002
</Cell>
10061003
<Cell>Games</Cell>
10071004
<Cell>File folder</Cell>
10081005
<Cell>6/7/2020</Cell>
10091006
</Row>
1010-
<Row id="2">
1007+
<Row id="2" style={{width: 'inherit', height: 'inherit'}}>
10111008
<Cell>
10121009
<MyCheckbox slot="selection" />
10131010
</Cell>
10141011
<Cell>Program Files</Cell>
10151012
<Cell>File folder</Cell>
10161013
<Cell>4/7/2021</Cell>
10171014
</Row>
1018-
<Row id="3">
1015+
<Row id="3" style={{width: 'inherit', height: 'inherit'}}>
10191016
<Cell>
10201017
<MyCheckbox slot="selection" />
10211018
</Cell>
10221019
<Cell>bootmgr</Cell>
10231020
<Cell>System file</Cell>
10241021
<Cell>11/20/2010</Cell>
10251022
</Row>
1026-
<Row id="4">
1023+
<Row id="4" style={{width: 'inherit', height: 'inherit'}}>
10271024
<Cell>
10281025
<MyCheckbox slot="selection" />
10291026
</Cell>
@@ -1079,16 +1076,32 @@ export const AutocompleteWithTagGroup = () => {
10791076
);
10801077
};
10811078

1082-
export const AutocompleteWithTree = () => {
1079+
function AutocompletePreserveFirstSection(args) {
1080+
let {contains} = useFilter({sensitivity: 'base'});
1081+
let filter = (textValue, inputValue, node) => {
1082+
if (node.parentKey === 'Section 1') {
1083+
return true;
1084+
}
1085+
return contains(textValue, inputValue);
1086+
};
1087+
10831088
return (
1084-
<AutocompleteWrapper>
1089+
<Autocomplete filter={filter}>
10851090
<div>
1086-
<TextField autoFocus data-testid="autocomplete-example">
1091+
<SearchField autoFocus>
10871092
<Label style={{display: 'block'}}>Test</Label>
10881093
<Input />
1089-
</TextField>
1090-
<TreeExampleStaticRender />
1094+
<Text style={{display: 'block'}} slot="description">Please select an option below.</Text>
1095+
</SearchField>
1096+
<Menu className={styles.menu} items={dynamicAutocompleteSubdialog} {...args}>
1097+
{item => dynamicRenderFuncSections(item)}
1098+
</Menu>
10911099
</div>
1092-
</AutocompleteWrapper>
1100+
</Autocomplete>
10931101
);
1102+
}
1103+
1104+
export const AutocompletePreserveFirstSectionStory: AutocompleteStory = {
1105+
render: (args) => <AutocompletePreserveFirstSection {...args} />,
1106+
name: 'Autocomplete, never filter first section'
10941107
};

0 commit comments

Comments
 (0)