Skip to content

Commit 4a69d50

Browse files
committed
create common use nodes for specific filtering patterns
1 parent 3ec3fd6 commit 4a69d50

File tree

11 files changed

+79
-181
lines changed

11 files changed

+79
-181
lines changed

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,56 @@ export class CollectionNode<T> implements Node<T> {
7878
}
7979
}
8080

81+
// TODO: naming, but essentially these nodes shouldn't be affected by filtering (BaseNode)?
82+
// Perhaps this filter logic should be in CollectionNode instead and the current logic of CollectionNode's filter should move to Table
83+
export class FilterLessNode<T> extends CollectionNode<T> {
84+
// TODO: resolve this
85+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-unused-vars
86+
filter(_, __, ___): FilterLessNode<T> | null {
87+
return this.clone();
88+
}
89+
}
90+
91+
export class ItemNode<T> extends CollectionNode<T> {
92+
static readonly type = 'item';
93+
94+
constructor(key: Key) {
95+
super(ItemNode.type, key);
96+
}
97+
98+
// TODO: resolve this
99+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
100+
filter(_, __, filterFn: (textValue: string) => boolean): ItemNode<T> | null {
101+
if (filterFn(this.textValue)) {
102+
return this.clone();
103+
}
104+
105+
return null;
106+
}
107+
}
108+
109+
export class SectionNode<T> extends CollectionNode<T> {
110+
static readonly type = 'section';
111+
112+
constructor(key: Key) {
113+
super(SectionNode.type, key);
114+
}
115+
116+
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (textValue: string) => boolean): SectionNode<T> | null {
117+
let filteredSection = super.filter(collection, newCollection, filterFn);
118+
if (filteredSection) {
119+
if (filteredSection.lastChildKey !== null) {
120+
let lastChild = collection.getItem(filteredSection.lastChildKey);
121+
if (lastChild && lastChild.type !== 'header') {
122+
return filteredSection;
123+
}
124+
}
125+
}
126+
127+
return null;
128+
}
129+
}
130+
81131
/**
82132
* An immutable Collection implementation. Updates are only allowed
83133
* when it is not marked as frozen. This can be subclassed to implement

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
export {CollectionBuilder, Collection, createLeafComponent, createBranchComponent} from './CollectionBuilder';
1414
export {createHideableComponent, useIsHidden} from './Hidden';
1515
export {useCachedChildren} from './useCachedChildren';
16-
export {BaseCollection, CollectionNode} from './BaseCollection';
16+
export {BaseCollection, CollectionNode, ItemNode, SectionNode, FilterLessNode} from './BaseCollection';
1717

1818
export type {CollectionBuilderProps, CollectionProps} from './CollectionBuilder';
1919
export type {CachedChildrenOptions} from './useCachedChildren';

packages/@react-spectrum/s2/src/SkeletonCollection.tsx

Lines changed: 2 additions & 6 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 {CollectionNode, createLeafComponent} from '@react-aria/collections';
13+
import {createLeafComponent, FilterLessNode} from '@react-aria/collections';
1414
import {Key} from '@react-types/shared';
1515
import {ReactNode} from 'react';
1616
import {Skeleton} from './Skeleton';
@@ -21,16 +21,12 @@ export interface SkeletonCollectionProps {
2121

2222
let cache = new WeakMap();
2323

24-
class SkeletonNode<T> extends CollectionNode<T> {
24+
class SkeletonNode extends FilterLessNode<unknown> {
2525
static readonly type = 'skeleton';
2626

2727
constructor(key: Key) {
2828
super(SkeletonNode.type, key);
2929
}
30-
31-
filter(): CollectionNode<any> | null {
32-
return this.clone();
33-
}
3430
}
3531

3632
/**

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212
import {AriaBreadcrumbsProps, useBreadcrumbs} from 'react-aria';
13-
import {Collection, CollectionBuilder, CollectionNode, createLeafComponent} from '@react-aria/collections';
13+
import {Collection, CollectionBuilder, createLeafComponent, FilterLessNode} from '@react-aria/collections';
1414
import {CollectionProps, CollectionRendererContext} from './Collection';
1515
import {ContextValue, RenderProps, SlotProps, StyleProps, useContextProps, useRenderProps, useSlottedContext} from './utils';
1616
import {filterDOMProps, mergeProps} from '@react-aria/utils';
@@ -73,17 +73,12 @@ export interface BreadcrumbProps extends RenderProps<BreadcrumbRenderProps>, Glo
7373
id?: Key
7474
}
7575

76-
class BreadcrumbNode extends CollectionNode<any> {
76+
class BreadcrumbNode extends FilterLessNode<unknown> {
7777
static readonly type = 'item';
7878

7979
constructor(key: Key) {
8080
super(BreadcrumbNode.type, key);
8181
}
82-
83-
// For, don't support Breadcrumb filtering
84-
filter(): CollectionNode<any> | null {
85-
return this.clone();
86-
}
8782
}
8883

8984
/**

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

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import {AriaGridListProps, DraggableItemResult, DragPreviewRenderer, DropIndicatorAria, DroppableCollectionResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useGridList, useGridListItem, useGridListSelectionCheckbox, useHover, useLocale, useVisuallyHidden} from 'react-aria';
1313
import {ButtonContext} from './Button';
1414
import {CheckboxContext} from './RSPContexts';
15-
import {Collection, CollectionBuilder, CollectionNode, createLeafComponent} from '@react-aria/collections';
15+
import {Collection, CollectionBuilder, createLeafComponent, FilterLessNode, ItemNode} from '@react-aria/collections';
1616
import {CollectionProps, CollectionRendererContext, DefaultCollectionRenderer, ItemRenderProps} from './Collection';
1717
import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps} from './utils';
1818
import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop';
@@ -284,20 +284,7 @@ export interface GridListItemProps<T = object> extends RenderProps<GridListItemR
284284
onAction?: () => void
285285
}
286286

287-
class GridListNode extends CollectionNode<any> {
288-
static readonly type = 'item';
289-
constructor(key: Key) {
290-
super(GridListNode.type, key);
291-
}
292-
293-
filter(_, __, filterFn: (textValue: string) => boolean): CollectionNode<any> | null {
294-
if (filterFn(this.textValue)) {
295-
return this.clone();
296-
}
297-
298-
return null;
299-
}
300-
}
287+
class GridListNode<T> extends ItemNode<T> {}
301288

302289
/**
303290
* A GridListItem represents an individual item in a GridList.
@@ -533,16 +520,13 @@ export interface GridListLoadMoreItemProps extends Omit<LoadMoreSentinelProps, '
533520
isLoading?: boolean
534521
}
535522

536-
class GridListLoaderNode extends CollectionNode<any> {
523+
// TODO: maybe make a general loader node
524+
class GridListLoaderNode extends FilterLessNode<any> {
537525
static readonly type = 'loader';
538526

539527
constructor(key: Key) {
540528
super(GridListLoaderNode.type, key);
541529
}
542-
543-
filter(): CollectionNode<any> | null {
544-
return this.clone();
545-
}
546530
}
547531

548532
export const GridListLoadMoreItem = createLeafComponent(GridListLoaderNode, function GridListLoadingIndicator(props: GridListLoadMoreItemProps, ref: ForwardedRef<HTMLDivElement>, item: Node<object>) {

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

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

13-
import {CollectionNode, createLeafComponent} from '@react-aria/collections';
1413
import {ContextValue, useContextProps} from './utils';
14+
import {createLeafComponent, FilterLessNode} from '@react-aria/collections';
1515
import {Key} from '@react-types/shared';
1616
import React, {createContext, ForwardedRef, HTMLAttributes} from 'react';
1717

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

20-
class HeaderNode extends CollectionNode<any> {
20+
class HeaderNode extends FilterLessNode<unknown> {
2121
static readonly type = 'header';
2222

2323
constructor(key: Key) {
2424
super(HeaderNode.type, key);
2525
}
26-
27-
filter(): CollectionNode<any> {
28-
return this.clone();
29-
}
3026
}
3127

3228
export const Header = /*#__PURE__*/ createLeafComponent(HeaderNode, function Header(props: HTMLAttributes<HTMLElement>, ref: ForwardedRef<HTMLElement>) {

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

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {AriaListBoxOptions, AriaListBoxProps, DraggableItemResult, DragPreviewRenderer, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useHover, useListBox, useListBoxSection, useLocale, useOption} from 'react-aria';
14-
import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent} from '@react-aria/collections';
14+
import {Collection, CollectionBuilder, createBranchComponent, createLeafComponent, FilterLessNode, ItemNode, SectionNode} from '@react-aria/collections';
1515
import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps} from './Collection';
1616
import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils';
1717
import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop';
@@ -305,27 +305,7 @@ function ListBoxSectionInner<T extends object>(props: ListBoxSectionProps<T>, re
305305
);
306306
}
307307

308-
export class ListBoxSectionNode<T> extends CollectionNode<T> {
309-
static readonly type = 'section';
310-
311-
constructor(key: Key) {
312-
super(ListBoxSectionNode.type, key);
313-
}
314-
315-
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (textValue: string) => boolean): CollectionNode<T> | null {
316-
let filteredSection = super.filter(collection, newCollection, filterFn);
317-
if (filteredSection) {
318-
if (filteredSection.lastChildKey !== null) {
319-
let lastChild = collection.getItem(filteredSection.lastChildKey);
320-
if (lastChild && lastChild.type !== 'header') {
321-
return filteredSection;
322-
}
323-
}
324-
}
325-
326-
return null;
327-
}
328-
}
308+
export class ListBoxSectionNode<T> extends SectionNode<T> {}
329309

330310
/**
331311
* A ListBoxSection represents a section within a ListBox.
@@ -352,21 +332,7 @@ export interface ListBoxItemProps<T = object> extends RenderProps<ListBoxItemRen
352332
onAction?: () => void
353333
}
354334

355-
class ListBoxItemNode<T> extends CollectionNode<T> {
356-
static readonly type = 'item';
357-
358-
constructor(key: Key) {
359-
super(ListBoxItemNode.type, key);
360-
}
361-
362-
filter(_, __, filterFn: (textValue: string) => boolean): CollectionNode<T> | null {
363-
if (filterFn(this.textValue)) {
364-
return this.clone();
365-
}
366-
367-
return null;
368-
}
369-
}
335+
class ListBoxItemNode<T> extends ItemNode<T> {}
370336

371337
/**
372338
* A ListBoxItem represents an individual option in a ListBox.
@@ -508,16 +474,12 @@ function ListBoxDropIndicator(props: ListBoxDropIndicatorProps, ref: ForwardedRe
508474
);
509475
}
510476

511-
class ListBoxLoaderNode extends CollectionNode<any> {
477+
class ListBoxLoaderNode extends FilterLessNode<any> {
512478
static readonly type = 'loader';
513479

514480
constructor(key: Key) {
515481
super(ListBoxLoaderNode.type, key);
516482
}
517-
518-
filter(): CollectionNode<any> | null {
519-
return this.clone();
520-
}
521483
}
522484

523485
const ListBoxDropIndicatorForwardRef = forwardRef(ListBoxDropIndicator);

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

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {AriaMenuProps, FocusScope, mergeProps, useHover, useMenu, useMenuItem, useMenuSection, useMenuTrigger, useSubmenuTrigger} from 'react-aria';
14-
import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent} from '@react-aria/collections';
14+
import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent, ItemNode, SectionNode} from '@react-aria/collections';
1515
import {MenuTriggerProps as BaseMenuTriggerProps, Collection as ICollection, Node, RootMenuTriggerState, TreeState, useMenuTriggerState, useSubmenuTriggerState, useTreeState} from 'react-stately';
1616
import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps, usePersistedKeys} from './Collection';
1717
import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
@@ -339,29 +339,7 @@ function MenuSectionInner<T extends object>(props: MenuSectionProps<T>, ref: For
339339
);
340340
}
341341

342-
// TODO: can probably reuse the SectionNode from ListBox? Do this last in case there is something different in the implementation? Or maybe keep them unique in case
343-
// down the line we need to differentiate the two?
344-
class MenuSectionNode<T> extends CollectionNode<T> {
345-
static readonly type = 'section';
346-
347-
constructor(key: Key) {
348-
super(MenuSectionNode.type, key);
349-
}
350-
351-
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (textValue: string) => boolean): CollectionNode<T> | null {
352-
let filteredSection = super.filter(collection, newCollection, filterFn);
353-
if (filteredSection) {
354-
if (filteredSection.lastChildKey !== null) {
355-
let lastChild = collection.getItem(filteredSection.lastChildKey);
356-
if (lastChild && lastChild.type !== 'header') {
357-
return filteredSection;
358-
}
359-
}
360-
}
361-
362-
return null;
363-
}
364-
}
342+
class MenuSectionNode<T> extends SectionNode<T> {}
365343

366344
/**
367345
* A MenuSection represents a section within a Menu.
@@ -400,23 +378,7 @@ export interface MenuItemProps<T = object> extends RenderProps<MenuItemRenderPro
400378

401379
const MenuItemContext = createContext<ContextValue<MenuItemProps, HTMLDivElement>>(null);
402380

403-
// TODO maybe this needs to be a separate node type? Or maybe it should just reuse the ItemNode from ListBox (reuse later if need be)
404-
// There is probably some merit to separating it like we already do for ListBoxItem/MenuItem/etc
405-
class MenuItemNode<T> extends CollectionNode<T> {
406-
static readonly type = 'item';
407-
408-
constructor(key: Key) {
409-
super(MenuItemNode.type, key);
410-
}
411-
412-
filter(_, __, filterFn: (textValue: string) => boolean): CollectionNode<T> | null {
413-
if (filterFn(this.textValue)) {
414-
return this.clone();
415-
}
416-
417-
return null;
418-
}
419-
}
381+
class MenuItemNode<T> extends ItemNode<T> {}
420382

421383
/**
422384
* A MenuItem represents an individual action in a Menu.

0 commit comments

Comments
 (0)