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 31 commits into
base: main
Choose a base branch
from

Conversation

LFDanLu
Copy link
Member

@LFDanLu LFDanLu commented Jul 26, 2025

Closes

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

In the RAC storybook, test that filtering still works as expected for Autocomplete wrapped Menu/Listbox. Also test the Autocomplete GridList/Table/TagGroup/custom node filter stories work as expected (aka contents are filtered when the user types in the field). Note that virtual focus isn't supported for these grid collection components since Left/Right arrow is overloaded if so (would navigate the collection and move the text input cursor)

Some things to look out for is that loading spinners shouldn't be filtered out, keyboard navigation should still all work as expected (especially in nested menus), and that sections/dividers shouldn't stick around if they aren't needed (e.g. sections shouldn't remain if all of their contents are filtered out, and dividers shouldn't remain if they don't have content before/after them)

🧢 Your Project:

RSP

Comment on lines 237 to 243
let newCollection = new BaseCollection<T>();
// This tracks the absolute last node we've visited in the collection when filtering, used for setting up the filteredCollection's lastKey and
// for updating the next/prevKey for every non-filtered node.
let lastNode: Mutable<CollectionNode<T>> | null = null;

for (let node of this) {
if (node.type === 'section' && node.hasChildNodes) {
let clonedSection: Mutable<CollectionNode<T>> = (node as CollectionNode<T>).clone();
let lastChildInSection: Mutable<CollectionNode<T>> | null = null;
for (let child of this.getChildren(node.key)) {
if (shouldKeepNode(child, filterFn, this, newCollection)) {
let clonedChild: Mutable<CollectionNode<T>> = (child as CollectionNode<T>).clone();
// eslint-disable-next-line max-depth
if (lastChildInSection == null) {
clonedSection.firstChildKey = clonedChild.key;
}

// eslint-disable-next-line max-depth
if (newCollection.firstKey == null) {
newCollection.firstKey = clonedSection.key;
}

// eslint-disable-next-line max-depth
if (lastChildInSection && lastChildInSection.parentKey === clonedChild.parentKey) {
lastChildInSection.nextKey = clonedChild.key;
clonedChild.prevKey = lastChildInSection.key;
} else {
clonedChild.prevKey = null;
}

clonedChild.nextKey = null;
newCollection.addNode(clonedChild);
lastChildInSection = clonedChild;
}
}
let [firstKey, lastKey] = filterChildren(this, newCollection, this.firstKey, filterFn);
newCollection.firstKey = firstKey;
newCollection.lastKey = lastKey;
return newCollection;
}
}
Copy link
Contributor

@nwidynski nwidynski Jul 26, 2025

Choose a reason for hiding this comment

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

I was wondering whether we could introduce something like a parent prop here to track relationship between the filtered outcome and the original collection. Currently, filtered collections generate a different data-collection id every time the filter changes.

Copy link
Member Author

Choose a reason for hiding this comment

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

I see, I'll play around with it a bit. Wonder if the data-collection id can just come from the collection directly

Copy link
Contributor

@nwidynski nwidynski Jul 28, 2025

Choose a reason for hiding this comment

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

Surely, although this would mean a merge of the id would likely trigger a rerender in the builder instead of the collection inner component. Not sure whether it was intentional to hook up useCollectionId with useId instead of useSSRId anyways?

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought the rerender from merging ids only happened for id and not for for say the data-collectionId but its been a while since I've dug through that code. As for the usage of useId in useCollectionId, is the concern around the rerendering? If so, I think its fine, just used to get a unique value but shouldn't be affected by the mergeProps id rerendering behavior I think as mentioned before

Copy link
Contributor

@nwidynski nwidynski Jul 28, 2025

Choose a reason for hiding this comment

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

No, you are good. I meant an explicit merge through mergeIds 👍 Or it happens someone for some reason decides to map data-collectionid to an id prop. Just wanted to make sure we are aware of what could happen, since this may lead to hard to debug issues real quick.

Copy link
Member Author

Choose a reason for hiding this comment

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

Just a heads up, but the team discussed this a bit and would like to hold off on adding it until we discuss all the requirements/needs for these collection ids (and how it meshes with your other PRs) in your Carousel RFC

Copy link
Contributor

Choose a reason for hiding this comment

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

got it, been working on the rfc, but its a lot of work. i expect it to land early next week 🙏

Copy link
Member Author

Choose a reason for hiding this comment

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

sounds good, and thank you so much for going the extra mile!

// TODO: an alternative is to simply walk the collection and add all item nodes that match the filter and any sections/separators we encounter
// to an array, then walk that new array and fix all the next/Prev keys while adding them to the new collection
UNSTABLE_filter(filterFn: (nodeValue: string) => boolean): BaseCollection<T> {
filter(filterFn: (textValue: string) => boolean): BaseCollection<T> {
Copy link
Contributor

@nwidynski nwidynski Jul 27, 2025

Choose a reason for hiding this comment

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

Any chance we could pass the entire node into filterFn and make it backwards compatible for the components using this? For more context, see https://github.com/adobe/react-spectrum/pull/8553/files#diff-87a6705385357783c7c3d863c9d0d740366e54297b558ba5bd676148afc41492

Copy link
Member Author

Choose a reason for hiding this comment

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

so I was looking, but it seems like this filter function has always taken a string (at least when looking at the history of BaseCollection)? I see in the tests you linked that it is referencing a signature of UNSTABLE_filter that expects a node, where did that come from?

Copy link
Contributor

@nwidynski nwidynski Jul 28, 2025

Choose a reason for hiding this comment

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

The feature of the PR is to sync multiple collections through a common parent builder, which is done by hoisting the collections, then filtering each other based on node keys. Additionally, I would like to attach the "primary" collection node in a context value on the "secondary" node.

Regardless, I would need access to the entire node to skip having to re-implement filtering - since this PR is migrating the filter API to stable status I would love to squeeze in this change now. Passing the entire node would also be more in line with iterables in general.

PS: Just noticed I named the textValue arg item, which I guess caused the confusion 😅

@rspbot
Copy link

rspbot commented Jul 28, 2025

@LFDanLu LFDanLu changed the title feat: (React Aria) (WIP) Implement filtering on a per CollectionNode basis feat: (React Aria) Implement filtering on a per CollectionNode basis Aug 1, 2025
@rspbot
Copy link

rspbot commented Aug 1, 2025

@@ -27,12 +27,14 @@ export interface CollectionOptions extends DOMProps, AriaLabelingProps {
/** Whether typeahead is disabled. */
disallowTypeAhead: boolean
}

// 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.

Comment on lines +83 to +85
// TODO: naming, but essentially these nodes shouldn't be affected by filtering (BaseNode)?
// Perhaps this filter logic should be in CollectionNode instead and the current logic of CollectionNode's filter should move to Table
export class FilterLessNode<T> extends CollectionNode<T> {
Copy link
Member Author

Choose a reason for hiding this comment

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

open to opinions on the below nodes (naming and if BaseCollection's filter should instead be "do nothing")

Copy link
Member

Choose a reason for hiding this comment

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

PersistentNode, matches our other name for virtualised items which stick around when others do not

StaticNode, maybe misleading?

Copy link
Member Author

Choose a reason for hiding this comment

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

PersistentNode does sound better, but I'm a bit worried that it will be confusing with virtualized behavior like you mentioned (it only sticks around when a filter operation happens, not really virtualizer specific).

Comment on lines +175 to +176
// TODO: could just make this a div perhaps, but keep it in line with how it used to work
return <CollectionNodeClass.type ref={itemRef}>{children}</CollectionNodeClass.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.

I think it would be ok to make these divs?

Copy link
Member Author

Choose a reason for hiding this comment

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

The todos in this file highlight some of my findings when tinkering with the refactor here. I believe the current flow is fine

export function UNSTABLE_useFilteredTableState<T extends object>(state: TableState<T>, filterFn: ((nodeValue: string, node: Node<T>) => boolean) | null | undefined): TableState<T> {
let collection = useMemo(() => filterFn ? state.collection.filter!(filterFn) : state.collection, [state.collection, filterFn]) as ITableCollection<T>;
let selectionManager = state.selectionManager.withCollection(collection);
// TODO: handle focus key reset? That logic is in useGridState
Copy link
Member Author

Choose a reason for hiding this comment

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

Can copy over the focus key reset logic but you have to tab out of the table to filter it so maybe not too bad

Copy link
Member

Choose a reason for hiding this comment

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

is that true? or can something else "control" the filtering and therefor trigger it as an async functionality while a user is inside the table?
or might it be that there's a network request that needs to complete before the filter and while waiting for that to happen, the user navigates into the table?

Copy link
Member Author

Choose a reason for hiding this comment

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

fair point, I'll probably look to pull in some fashion of focused key handling in here when we handle the Autocomplete + grid navigation + virtual focus. The logic will probably be much simpler, perhaps might be best to reset to the first key if we can't find the original key since the contents of the newly filtered table may not even be related to the previous state

Copy link
Member

Choose a reason for hiding this comment

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

Found an interesting related situation, in the case where the item was moved in the autocomplete from somewhere in the middle of the collection to the "recently selected" section at the top, virtual focus got lost and we never scrolled up to the new location

Copy link
Member Author

Choose a reason for hiding this comment

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

good catch, looks like this actually used to happen: https://reactspectrum.blob.core.windows.net/reactspectrum/2ee02a0afccfcbb4611e3f4085739ce80852d133/storybook/index.html?path=/story/react-aria-components-autocomplete--autocomplete-in-popover-dialog-trigger&providerSwitcher-express=false. Seems like the problem is actually just the scrolling since the input still has the proper active descendant and the moved item is still in the DOM it seems.

Comment on lines +107 to +109
// TODO: for now, don't grab collection ref and collectionProps from the autocomplete, rely on the user tabbing to the gridlist
// figure out if we want to support virtual focus for grids when wrapped in an autocomplete
let {filter, collectionProps} = useContext(UNSTABLE_InternalAutocompleteContext) || {};
Copy link
Member Author

Choose a reason for hiding this comment

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

To discuss, as mentioned ArrowLeft/Right become overloaded when virtual focus is enabled since it would move the cursor at the same time as moving virtual focus in the collection.

Copy link
Member

Choose a reason for hiding this comment

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

maybe only move it if the selection cursor is at the start or end of the input? I'm planning to do something similar inside grid edit mode

Comment on lines +526 to +527
// TODO: maybe make a general loader node
class GridListLoaderNode extends FilterLessNode<any> {
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 now just using FilterLessNode, but can make a general LoaderNode if the logic diverges in the future

@rspbot
Copy link

rspbot commented Aug 1, 2025

Copy link
Member

@snowystinger snowystinger left a comment

Choose a reason for hiding this comment

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

first set of review comments, mostly confusion around these classes

Comment on lines +83 to +85
// TODO: naming, but essentially these nodes shouldn't be affected by filtering (BaseNode)?
// Perhaps this filter logic should be in CollectionNode instead and the current logic of CollectionNode's filter should move to Table
export class FilterLessNode<T> extends CollectionNode<T> {
Copy link
Member

Choose a reason for hiding this comment

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

PersistentNode, matches our other name for virtualised items which stick around when others do not

StaticNode, maybe misleading?

import React, {createContext, ForwardedRef, HTMLAttributes} from 'react';

export const HeaderContext = createContext<ContextValue<HTMLAttributes<HTMLElement>, HTMLElement>>({});

export const Header = /*#__PURE__*/ createLeafComponent('header', function Header(props: HTMLAttributes<HTMLElement>, ref: ForwardedRef<HTMLElement>) {
class HeaderNode extends FilterLessNode<unknown> {
Copy link
Member

Choose a reason for hiding this comment

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

seems opinionated?

Copy link
Member Author

Choose a reason for hiding this comment

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

yep, in the same vein as https://github.com/adobe/react-spectrum/pull/8641/files#r2252805272, will most likely let the user customize the desired filter behavior here in the future

Copy link
Member

Choose a reason for hiding this comment

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

ok, so long as the intention is more of a default i guess

@@ -60,7 +60,7 @@ export {ProgressBar, ProgressBarContext} from './ProgressBar';
export {RadioGroup, Radio, RadioGroupContext, RadioContext, RadioGroupStateContext} from './RadioGroup';
export {SearchField, SearchFieldContext} from './SearchField';
export {Select, SelectValue, SelectContext, SelectValueContext, SelectStateContext} from './Select';
export {Separator, SeparatorContext} from './Separator';
export {Separator, SeparatorContext, SeparatorNode} from './Separator';
Copy link
Member

Choose a reason for hiding this comment

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

why is SeparatorNode exported, but HeaderNode isn't? I assume adding this here was an accident?

Copy link
Member Author

Choose a reason for hiding this comment

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

needed in S2 Combobox's Divider implementation

Copy link
Member

@snowystinger snowystinger Aug 5, 2025

Choose a reason for hiding this comment

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

hmmm torn on exporting it vs recreating it since we recreated the Separator anyways, we're not using the RAC one

@@ -99,6 +99,7 @@ interface SectionContextValue {

export const SectionContext = createContext<SectionContextValue | null>(null);

// TODO: should I update this since it is deprecated?
Copy link
Member

Choose a reason for hiding this comment

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

I think I'm ok not updating it

@@ -127,22 +127,40 @@ function useCollectionDocument<T extends object, C extends BaseCollection<T>>(cr

const SSRContext = createContext<BaseNode<any> | null>(null);

function useSSRCollectionNode<T extends Element>(Type: string, props: object, ref: ForwardedRef<T>, rendered?: any, children?: ReactNode, render?: (node: Node<T>) => ReactElement) {
export type CollectionNodeClass<T> = {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
export type CollectionNodeClass<T> = {
export type CollectionNodeClass<T extends object> = {
new (key: Key): CollectionNode<T>,
readonly type: string
};

should this extend or have a default? like other collections we have

any reason you didn't use interface? or the CollectionNode directly (as i commented on L135?
Also, why not declare this type/interface in the same file as the CollectionNode and have

export class CollectionNode<T> implements CollectionNodeClass<T>

?

Copy link
Member Author

Choose a reason for hiding this comment

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

The above wouldn't work right? CollectionNode should have a constructor that accepts a user provided type whereas useSSRCollectionNode/createLeaf/etc should take a class that already has a type

readonly type: string
};

function createCollectionNodeClass(type: string): CollectionNodeClass<any> {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
function createCollectionNodeClass(type: string): CollectionNodeClass<any> {
function createCollectionNodeClass(type: string): CollectionNode<any> {

Copy link
Member Author

Choose a reason for hiding this comment

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

This is not producing a CollectionNode since you can't set the type after calling createCollectionNodeClass

function useSSRCollectionNode<T extends Element>(Type: string, props: object, ref: ForwardedRef<T>, rendered?: any, children?: ReactNode, render?: (node: Node<T>) => ReactElement) {
export type CollectionNodeClass<T> = {
new (key: Key): CollectionNode<T>,
readonly type: string
Copy link
Member

Choose a reason for hiding this comment

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

this is readonly? but it's not readonly because you assign to it just below?

Copy link
Member Author

Choose a reason for hiding this comment

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

it is readonly, the createCollectionNodeClass below basically creates a CollectionNodeClass when called since you can't set the type after that call

Copy link
Member

Choose a reason for hiding this comment

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

ah, confusing...
Something is still tripping me up here, like inheritance isn't quite how I would expect or something

Copy link
Member Author

@LFDanLu LFDanLu Aug 5, 2025

Choose a reason for hiding this comment

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

essentially the useSSRCollectionNode/leaf/branch functions expect a class similar to CollectionNode, albeit with a type already defined, thus only needing a constructor that accepts just a key. The type needs to be static since I use it in useSSRCollection node without instantiating the class.

the weird part is definitely the createCollectionNodeClass since I also need to create a similar CollectionNodeClass if given just a string

Comment on lines +93 to +97
static readonly type = 'item';

constructor(key: Key) {
super(ItemNode.type, key);
}
Copy link
Member

Choose a reason for hiding this comment

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

why static? I don't think we need this as a class field?
this should be enough if it's not overridable

Suggested change
static readonly type = 'item';
constructor(key: Key) {
super(ItemNode.type, key);
}
constructor(key: Key) {
super('item', key);
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I originally just had them like the above, but typescript was giving me all kinds of hell in CollectionBuilder as a result due to the overrides

Copy link
Member

Choose a reason for hiding this comment

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

hmmm... because it only knew 'type = item'?
or what were the errors?

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, so changing ItemNode in such a way then conflicts with the CollectionNodeClass definition since then it doesn't have a static type property, only an instance type property. I access CollectionNodeClass.type in useSSRCollectionNode without instantiating it hence why I need the static type

Copy link
Member

Choose a reason for hiding this comment

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

Must've missed that usage, apologies. I thought it was only being used here. I see the other now so this makes more sense

Comment on lines +136 to +140
let NodeClass = function (key: Key) {
return new CollectionNode(type, key);
} as any;
NodeClass.type = type;
return NodeClass;
Copy link
Member

Choose a reason for hiding this comment

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

if we address the above, maybe we can remove the need for this
or simplify it, i'm not sure why it's wrapped in a function

filter(collection: BaseCollection<any>, newCollection: BaseCollection<any>): CollectionNode<any> | null {
if (newCollection.getItem(this.prevKey!)) {
return this.clone();
}
Copy link
Member

Choose a reason for hiding this comment

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

if you have parentKey or nextKey, could you decide to return null here? saw you had a question of if you could move that logic in here

Copy link
Member Author

Choose a reason for hiding this comment

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

the problem is that at this point where you are calling filter on the separator, we haven't performed filter for the nodes after it so we don't know if the nextKey will still be in the filtered collection

Copy link
Member

Choose a reason for hiding this comment

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

ahh... no look ahead, got it
could add a callback to check it after the collection is complete? almost like a second pass, but not over the entire collection, so shouldn't be a performance hit

I was also thinking, might be able to do this with css, just leave all the separators in the collection, assign them some data-attribute that matches a section/items and check in the css if it has any siblings matching that data attribute, otherwise hide it

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess I could do the callback, trying to think about how filterChildren would go about calling that callback before it returns the first/last node (maybe filter would return a callback alongside the CollectionNode)? Still feels easier to handle this logic in filterChildren though...

As for the css approach, that would mean the user would need to implement that no? Wouldn't be too hard I guess, but kinda annoying that they would need to know to do so.

Copy link
Member

Choose a reason for hiding this comment

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

yeah, not ideal
It would be nice if we could queue additional checks, will think on it some more

@rspbot
Copy link

rspbot commented Aug 4, 2025

@LFDanLu LFDanLu force-pushed the baseCollection_filter branch from d3f22fb to d2b5e51 Compare August 4, 2025 22:05
@rspbot
Copy link

rspbot commented Aug 4, 2025

@rspbot
Copy link

rspbot commented Aug 5, 2025

@rspbot
Copy link

rspbot commented Aug 5, 2025

@rspbot
Copy link

rspbot commented Aug 5, 2025

@rspbot
Copy link

rspbot commented Aug 7, 2025

@rspbot
Copy link

rspbot commented Aug 7, 2025

## API Changes

react-aria-components

/react-aria-components:Autocomplete

-Autocomplete {
+Autocomplete <T extends {}> {
   children: ReactNode
   defaultInputValue?: string
   disableAutoFocusFirst?: boolean = false
-  filter?: (string, string) => boolean
+  filter?: (string, string, Node<{}>) => boolean
   inputValue?: string
   onInputChange?: (string) => void
   slot?: string | null
 }

/react-aria-components:UNSTABLE_createLeafComponent

 UNSTABLE_createLeafComponent <E extends Element, P extends {}> {
-  type: string
+  CollectionNodeClass: {}<any> | string
   render: (P, ForwardedRef<E>, any) => ReactElement | null
   returnVal: undefined
 }

/react-aria-components:UNSTABLE_createBranchComponent

 UNSTABLE_createBranchComponent <E extends Element, P extends {
     children?: any
 }, T extends {}> {
-  type: string
+  CollectionNodeClass: {}<any> | string
   render: (P, ForwardedRef<E>, Node<T>) => ReactElement | null
   useChildren: (P) => ReactNode
   returnVal: undefined
 }

/react-aria-components:VisuallyHidden

-VisuallyHidden {
-  children?: ReactNode
-  className?: string | undefined
-  elementType?: string | JSXElementConstructor<any> = 'div'
-  id?: string | undefined
-  isFocusable?: boolean
-  role?: AriaRole | undefined
-  style?: CSSProperties | undefined
-  tabIndex?: number | undefined
-}

/react-aria-components:AutocompleteProps

-AutocompleteProps {
+AutocompleteProps <T> {
   children: ReactNode
   defaultInputValue?: string
   disableAutoFocusFirst?: boolean = false
-  filter?: (string, string) => boolean
+  filter?: (string, string, Node<T>) => boolean
   inputValue?: string
   onInputChange?: (string) => void
   slot?: string | null
 }

/react-aria-components:GridLayoutOptions

 GridLayoutOptions {
   dropIndicatorThickness?: number = 2
   maxColumns?: number = Infinity
-  maxHorizontalSpace?: number = Infinity
   maxItemSize?: Size = Infinity
   minItemSize?: Size = 200 x 200
   minSpace?: Size = 18 x 18
   preserveAspectRatio?: boolean = false

/react-aria-components:SeparatorNode

+SeparatorNode {
+  aria-label?: string
+  childNodes: Iterable<Node<T>>
+  clone: () => CollectionNode<T>
+  colIndex: number | null
+  colSpan: number | null
+  constructor: (Key) => void
+  filter: (BaseCollection<any>, BaseCollection<any>) => CollectionNode<any> | null
+  firstChildKey: Key | null
+  hasChildNodes: boolean
+  index: number
+  key: Key
+  lastChildKey: Key | null
+  level: number
+  nextKey: Key | null
+  parentKey: Key | null
+  prevKey: Key | null
+  props: any
+  render?: (Node<any>) => ReactElement
+  rendered: ReactNode
+  textValue: string
+  type: any
+  value: T | null
+}

@internationalized/date

/@internationalized/date:setLocalTimeZone

-setLocalTimeZone {
-  timeZone: string
-  returnVal: undefined
-}

/@internationalized/date:resetLocalTimeZone

-resetLocalTimeZone {
-  returnVal: undefined
-}

@react-aria/autocomplete

/@react-aria/autocomplete:useAutocomplete

-useAutocomplete {
+useAutocomplete <T> {
-  props: AriaAutocompleteOptions
+  props: AriaAutocompleteOptions<T>
   state: AutocompleteState
   returnVal: undefined
 }

/@react-aria/autocomplete:AriaAutocompleteProps

-AriaAutocompleteProps {
+AriaAutocompleteProps <T> {
   children: ReactNode
   defaultInputValue?: string
   disableAutoFocusFirst?: boolean = false
-  filter?: (string, string) => boolean
+  filter?: (string, string, Node<T>) => boolean
   inputValue?: string
   onInputChange?: (string) => void
 }

/@react-aria/autocomplete:AriaAutocompleteOptions

-AriaAutocompleteOptions {
+AriaAutocompleteOptions <T> {
   collectionRef: RefObject<HTMLElement | null>
   defaultInputValue?: string
   disableAutoFocusFirst?: boolean = false
-  filter?: (string, string) => boolean
+  filter?: (string, string, Node<T>) => boolean
   inputRef: RefObject<HTMLInputElement | null>
   inputValue?: string
   onInputChange?: (string) => void
 }

/@react-aria/autocomplete:AutocompleteAria

-AutocompleteAria {
+AutocompleteAria <T> {
   collectionProps: CollectionOptions
   collectionRef: RefObject<HTMLElement | null>
-  filter?: (string) => boolean
+  filter?: (string, Node<T>) => boolean
   textFieldProps: AriaTextFieldProps
 }

@react-aria/collections

/@react-aria/collections:createLeafComponent

 createLeafComponent <E extends Element, P extends {}> {
-  type: string
+  CollectionNodeClass: {}<any> | string
   render: (P, ForwardedRef<E>, any) => ReactElement | null
   returnVal: undefined
 }

/@react-aria/collections:createBranchComponent

 createBranchComponent <E extends Element, P extends {
     children?: any
 }, T extends {}> {
-  type: string
+  CollectionNodeClass: {}<any> | string
   render: (P, ForwardedRef<E>, Node<T>) => ReactElement | null
   useChildren: (P) => ReactNode
   returnVal: undefined
 }

/@react-aria/collections:BaseCollection

 BaseCollection <T> {
-  UNSTABLE_filter: ((string) => boolean) => BaseCollection<T>
   addNode: (CollectionNode<T>) => void
   at: () => Node<T>
   clone: () => this
   commit: (Key | null, Key | null, any) => void
+  filter: (FilterFn<T>, BaseCollection<T>) => BaseCollection<T>
   getChildren: (Key) => Iterable<Node<T>>
   getFirstKey: () => Key | null
   getItem: (Key) => Node<T> | null
   getKeyAfter: (Key) => Key | null
   getKeys: () => IterableIterator<Key>
   getLastKey: () => Key | null
   removeNode: (Key) => void
   size: number
   undefined: () => IterableIterator<Node<T>>
 }

/@react-aria/collections:CollectionNode

 CollectionNode <T> {
   aria-label?: string
   childNodes: Iterable<Node<T>>
   clone: () => CollectionNode<T>
   colIndex: number | null
   colSpan: number | null
   constructor: (string, Key) => void
+  filter: (BaseCollection<T>, BaseCollection<T>, FilterFn<T>) => CollectionNode<T> | null
   firstChildKey: Key | null
   hasChildNodes: boolean
   index: number
   key: Key
   level: number
   nextKey: Key | null
   parentKey: Key | null
   prevKey: Key | null
   props: any
   render?: (Node<any>) => ReactElement
   rendered: ReactNode
   textValue: string
   type: string
   value: T | null
 }

/@react-aria/collections:ItemNode

+ItemNode <T> {
+  aria-label?: string
+  childNodes: Iterable<Node<T>>
+  clone: () => CollectionNode<T>
+  colIndex: number | null
+  colSpan: number | null
+  constructor: (Key) => void
+  filter: (BaseCollection<T>, BaseCollection<T>, FilterFn<T>) => ItemNode<T> | null
+  firstChildKey: Key | null
+  hasChildNodes: boolean
+  index: number
+  key: Key
+  lastChildKey: Key | null
+  level: number
+  nextKey: Key | null
+  parentKey: Key | null
+  prevKey: Key | null
+  props: any
+  render?: (Node<any>) => ReactElement
+  rendered: ReactNode
+  textValue: string
+  type: any
+  value: T | null
+}

/@react-aria/collections:SectionNode

+SectionNode <T> {
+  aria-label?: string
+  childNodes: Iterable<Node<T>>
+  clone: () => CollectionNode<T>
+  colIndex: number | null
+  colSpan: number | null
+  constructor: (Key) => void
+  filter: (BaseCollection<T>, BaseCollection<T>, FilterFn<T>) => SectionNode<T> | null
+  firstChildKey: Key | null
+  hasChildNodes: boolean
+  index: number
+  key: Key
+  lastChildKey: Key | null
+  level: number
+  nextKey: Key | null
+  parentKey: Key | null
+  prevKey: Key | null
+  props: any
+  render?: (Node<any>) => ReactElement
+  rendered: ReactNode
+  textValue: string
+  type: any
+  value: T | null
+}

/@react-aria/collections:FilterLessNode

+FilterLessNode <T> {
+  aria-label?: string
+  childNodes: Iterable<Node<T>>
+  clone: () => CollectionNode<T>
+  colIndex: number | null
+  colSpan: number | null
+  constructor: (string, Key) => void
+  filter: (BaseCollection<T>, BaseCollection<T>, FilterFn<T>) => FilterLessNode<T> | null
+  firstChildKey: Key | null
+  hasChildNodes: boolean
+  index: number
+  key: Key
+  lastChildKey: Key | null
+  level: number
+  nextKey: Key | null
+  parentKey: Key | null
+  prevKey: Key | null
+  props: any
+  render?: (Node<any>) => ReactElement
+  rendered: ReactNode
+  textValue: string
+  type: string
+  value: T | null
+}

@react-spectrum/s2

/@react-spectrum/s2:Autocomplete

-Autocomplete {
+Autocomplete <T extends {}> {
   children: ReactNode
   defaultInputValue?: string
   disableAutoFocusFirst?: boolean = false
-  filter?: (string, string) => boolean
+  filter?: (string, string, Node<{}>) => boolean
   inputValue?: string
   onInputChange?: (string) => void
   slot?: string | null
 }

/@react-spectrum/s2:AutocompleteProps

-AutocompleteProps {
+AutocompleteProps <T> {
   children: ReactNode
   defaultInputValue?: string
   disableAutoFocusFirst?: boolean = false
-  filter?: (string, string) => boolean
+  filter?: (string, string, Node<T>) => boolean
   inputValue?: string
   onInputChange?: (string) => void
   slot?: string | null
 }

@react-stately/data

/@react-stately/data:ListData

 ListData <T> {
-  addKeysToSelection: (Selection) => void
   append: (Array<T>) => void
   filterText: string
   getItem: (Key) => T | undefined
   insert: (number, Array<T>) => void
   insertAfter: (Key, Array<T>) => void
   insertBefore: (Key, Array<T>) => void
   items: Array<T>
   move: (Key, number) => void
   moveAfter: (Key, Iterable<Key>) => void
   moveBefore: (Key, Iterable<Key>) => void
   prepend: (Array<T>) => void
   remove: (Array<Key>) => void
-  removeKeysFromSelection: (Selection) => void
   removeSelectedItems: () => void
   selectedKeys: Selection
   setFilterText: (string) => void
   setSelectedKeys: (Selection) => void
 }

/@react-stately/data:AsyncListData

 AsyncListData <T> {
-  addKeysToSelection: (Selection) => void
   append: (Array<T>) => void
   error?: Error
   filterText: string
   getItem: (Key) => T | undefined
   insert: (number, Array<T>) => void
   insertAfter: (Key, Array<T>) => void
   insertBefore: (Key, Array<T>) => void
   isLoading: boolean
   items: Array<T>
   loadMore: () => void
   loadingState: LoadingState
   move: (Key, number) => void
   moveAfter: (Key, Iterable<Key>) => void
   moveBefore: (Key, Iterable<Key>) => void
   prepend: (Array<T>) => void
   reload: () => void
   remove: (Array<Key>) => void
-  removeKeysFromSelection: (Selection) => void
   removeSelectedItems: () => void
   selectedKeys: Selection
   setFilterText: (string) => void
   setSelectedKeys: (Selection) => void
   sortDescriptor?: SortDescriptor
   update: (Key, T) => void
 }

@react-stately/layout

/@react-stately/layout:GridLayoutOptions

 GridLayoutOptions {
   dropIndicatorThickness?: number = 2
   maxColumns?: number = Infinity
-  maxHorizontalSpace?: number = Infinity
   maxItemSize?: Size = Infinity
   minItemSize?: Size = 200 x 200
   minSpace?: Size = 18 x 18
   preserveAspectRatio?: boolean = false

@react-stately/list

/@react-stately/list:UNSTABLE_useFilteredListState

 UNSTABLE_useFilteredListState <T extends {}> {
   state: ListState<T>
-  filter: (string) => boolean | null | undefined
+  filterFn: (string, Node<T>) => boolean | null | undefined
   returnVal: undefined
 }

@react-stately/table

/@react-stately/table:UNSTABLE_useFilteredTableState

+UNSTABLE_useFilteredTableState <T extends {}> {
+  state: TableState<T>
+  filterFn: (string, Node<T>) => boolean | null | undefined
+  returnVal: undefined
+}

@@ -65,15 +67,15 @@ 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> {
Copy link
Member

Choose a reason for hiding this comment

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

do we have any stories or tests written in TS that make use of the generic?

};

return (
<Autocomplete filter={filter}>
Copy link
Member

Choose a reason for hiding this comment

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

for instance, is this picking things up correctly? I would've thought that you'd need to specify the <Autocomplete<{name: string, isSection?: boolean, children?: ReactNode}> to match the types of items
I don't think Autocomplete will be able to infer this, so I would expect the generic to be required. Then filter can be typed as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants