Skip to content

Commit 432a43c

Browse files
committed
adding support for table filtering
1 parent 361286b commit 432a43c

File tree

7 files changed

+138
-60
lines changed

7 files changed

+138
-60
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
242242
}
243243
}
244244

245-
function filterChildren<T>(collection: BaseCollection<T>, newCollection: BaseCollection<T>, firstChildKey: Key | null, filterFn: (textValue: string) => boolean): [Key | null, Key | null] {
245+
export function filterChildren<T>(collection: BaseCollection<T>, newCollection: BaseCollection<T>, firstChildKey: Key | null, filterFn: (textValue: string) => boolean): [Key | null, Key | null] {
246246
// loop over the siblings for firstChildKey
247247
// create new nodes based on calling node.filter for each child
248248
// if it returns null then don't include it, otherwise update its prev/next keys

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, filterChildren} from './BaseCollection';
1717

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type {TableHeaderProps, TableBodyProps, ColumnProps, RowProps, CellProps}
1616
export type {TreeGridState, TreeGridStateProps} from './useTreeGridState';
1717

1818
export {useTableColumnResizeState} from './useTableColumnResizeState';
19-
export {useTableState} from './useTableState';
19+
export {useTableState, UNSTABLE_useFilteredTableState} from './useTableState';
2020
export {TableHeader} from './TableHeader';
2121
export {TableBody} from './TableBody';
2222
export {Column} from './Column';

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,18 @@ export function useTableState<T extends object>(props: TableStateProps<T>): Tabl
107107
}
108108
};
109109
}
110+
111+
/**
112+
* Filters a collection using the provided filter function and returns a new ListState.
113+
*/
114+
export function UNSTABLE_useFilteredTableState<T extends object>(state: TableState<T>, filterFn: ((nodeValue: string) => boolean) | null | undefined): TableState<T> {
115+
let collection = useMemo(() => filterFn ? state.collection.filter!(filterFn) : state.collection, [state.collection, filterFn]) as ITableCollection<T>;
116+
let selectionManager = state.selectionManager.withCollection(collection);
117+
// TODO: handle focus key reset? That logic is in useGridState
118+
119+
return {
120+
...state,
121+
collection,
122+
selectionManager
123+
};
124+
}

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

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import {AriaLabelingProps, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared';
2-
import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent, useCachedChildren} from '@react-aria/collections';
2+
import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent, filterChildren, useCachedChildren} from '@react-aria/collections';
33
import {buildHeaderRows, TableColumnResizeState} from '@react-stately/table';
44
import {ButtonContext} from './Button';
55
import {CheckboxContext} from './RSPContexts';
66
import {CollectionProps, CollectionRendererContext, DefaultCollectionRenderer, ItemRenderProps} from './Collection';
77
import {ColumnSize, ColumnStaticSize, TableCollection as ITableCollection, TableProps as SharedTableProps} from '@react-types/table';
88
import {ContextValue, DEFAULT_SLOT, DOMProps, Provider, RenderProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps} from './utils';
9-
import {DisabledBehavior, DraggableCollectionState, DroppableCollectionState, MultipleSelectionState, Node, SelectionBehavior, SelectionMode, SortDirection, TableState, useMultipleSelectionState, useTableColumnResizeState, useTableState} from 'react-stately';
9+
import {DisabledBehavior, DraggableCollectionState, DroppableCollectionState, MultipleSelectionState, Node, SelectionBehavior, SelectionMode, SortDirection, TableState, UNSTABLE_useFilteredTableState, useMultipleSelectionState, useTableColumnResizeState, useTableState} from 'react-stately';
1010
import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop';
1111
import {DragAndDropHooks} from './useDragAndDrop';
1212
import {DraggableItemResult, DragPreviewRenderer, DropIndicatorAria, DroppableCollectionResult, FocusScope, ListKeyboardDelegate, mergeProps, useFocusRing, useHover, useLocale, useLocalizedStringFormatter, useTable, useTableCell, useTableColumnHeader, useTableColumnResize, useTableHeaderRow, useTableRow, useTableRowGroup, useTableSelectAllCheckbox, useTableSelectionCheckbox, useVisuallyHidden} from 'react-aria';
@@ -16,6 +16,11 @@ import {GridNode} from '@react-types/grid';
1616
import intlMessages from '../intl/*.json';
1717
import React, {createContext, ForwardedRef, forwardRef, JSX, ReactElement, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
1818
import ReactDOM from 'react-dom';
19+
import {UNSTABLE_InternalAutocompleteContext} from './Autocomplete';
20+
21+
export type Mutable<T> = {
22+
-readonly[P in keyof T]: T[P]
23+
}
1924

2025
class TableCollection<T> extends BaseCollection<T> implements ITableCollection<T> {
2126
headerRows: GridNode<T>[] = [];
@@ -160,6 +165,7 @@ class TableCollection<T> extends BaseCollection<T> implements ITableCollection<T
160165
collection.rowHeaderColumnKeys = this.rowHeaderColumnKeys;
161166
collection.head = this.head;
162167
collection.body = this.body;
168+
// TODO clone updateColumns as well but it is private
163169
return collection;
164170
}
165171

@@ -190,6 +196,20 @@ class TableCollection<T> extends BaseCollection<T> implements ITableCollection<T
190196

191197
return text.join(' ');
192198
}
199+
200+
filter(filterFn: (textValue: string) => boolean): TableCollection<T> {
201+
// TODO: ideally we wouldn't need to reimplement this but we need a TableCollection, not a BaseCollection
202+
// Also need to handle the fact that a bunch of properites are private
203+
let clone = this.clone() as Mutable<TableCollection<T>>;
204+
// @ts-ignore
205+
let [firstKey, lastKey] = filterChildren(this, clone, this.firstKey, filterFn);
206+
// @ts-ignore
207+
clone.firstKey = firstKey;
208+
// @ts-ignore
209+
clone.lastKey = lastKey;
210+
// @ts-ignore
211+
return clone;
212+
}
193213
}
194214

195215
interface ResizableTableContainerContextValue {
@@ -363,23 +383,25 @@ interface TableInnerProps {
363383

364384

365385
function TableInner({props, forwardedRef: ref, selectionState, collection}: TableInnerProps) {
386+
let {filter} = useContext(UNSTABLE_InternalAutocompleteContext) || {};
366387
let tableContainerContext = useContext(ResizableTableContainerContext);
367388
ref = useObjectRef(useMemo(() => mergeRefs(ref, tableContainerContext?.tableRef), [ref, tableContainerContext?.tableRef]));
368-
let state = useTableState({
389+
let tableState = useTableState({
369390
...props,
370391
collection,
371392
children: undefined,
372393
UNSAFE_selectionState: selectionState
373394
});
374395

396+
let filteredState = UNSTABLE_useFilteredTableState(tableState, filter);
375397
let {isVirtualized, layoutDelegate, dropTargetDelegate: ctxDropTargetDelegate, CollectionRoot} = useContext(CollectionRendererContext);
376398
let {dragAndDropHooks} = props;
377399
let {gridProps} = useTable({
378400
...props,
379401
layoutDelegate,
380402
isVirtualized
381-
}, state, ref);
382-
let selectionManager = state.selectionManager;
403+
}, filteredState, ref);
404+
let selectionManager = filteredState.selectionManager;
383405
let hasDragHooks = !!dragAndDropHooks?.useDraggableCollectionState;
384406
let hasDropHooks = !!dragAndDropHooks?.useDroppableCollectionState;
385407
let dragHooksProvided = useRef(hasDragHooks);
@@ -405,7 +427,7 @@ function TableInner({props, forwardedRef: ref, selectionState, collection}: Tabl
405427

406428
if (hasDragHooks && dragAndDropHooks) {
407429
dragState = dragAndDropHooks.useDraggableCollectionState!({
408-
collection,
430+
collection: filteredState.collection,
409431
selectionManager,
410432
preview: dragAndDropHooks.renderDragPreview ? preview : undefined
411433
});
@@ -419,12 +441,12 @@ function TableInner({props, forwardedRef: ref, selectionState, collection}: Tabl
419441

420442
if (hasDropHooks && dragAndDropHooks) {
421443
dropState = dragAndDropHooks.useDroppableCollectionState!({
422-
collection,
444+
collection: filteredState.collection,
423445
selectionManager
424446
});
425447

426448
let keyboardDelegate = new ListKeyboardDelegate({
427-
collection,
449+
collection: filteredState.collection,
428450
disabledKeys: selectionManager.disabledKeys,
429451
disabledBehavior: selectionManager.disabledBehavior,
430452
ref,
@@ -448,7 +470,7 @@ function TableInner({props, forwardedRef: ref, selectionState, collection}: Tabl
448470
isDropTarget: isRootDropTarget,
449471
isFocused,
450472
isFocusVisible,
451-
state
473+
state: filteredState
452474
}
453475
});
454476

@@ -459,7 +481,7 @@ function TableInner({props, forwardedRef: ref, selectionState, collection}: Tabl
459481
if (tableContainerContext) {
460482
layoutState = tableContainerContext.useTableColumnResizeState({
461483
tableWidth: tableContainerContext.tableWidth
462-
}, state);
484+
}, filteredState);
463485
if (!isVirtualized) {
464486
style = {
465487
...style,
@@ -475,7 +497,7 @@ function TableInner({props, forwardedRef: ref, selectionState, collection}: Tabl
475497
return (
476498
<Provider
477499
values={[
478-
[TableStateContext, state],
500+
[TableStateContext, filteredState],
479501
[TableColumnResizeStateContext, layoutState],
480502
[DragAndDropContext, {dragAndDropHooks, dragState, dropState}],
481503
[DropIndicatorContext, {render: TableDropIndicatorWrapper}]
@@ -492,7 +514,7 @@ function TableInner({props, forwardedRef: ref, selectionState, collection}: Tabl
492514
data-focused={isFocused || undefined}
493515
data-focus-visible={isFocusVisible || undefined}>
494516
<CollectionRoot
495-
collection={collection}
517+
collection={filteredState.collection}
496518
scrollRef={tableContainerContext?.scrollRef ?? ref}
497519
persistedKeys={useDndPersistedKeys(selectionManager, dragAndDropHooks, dropState)} />
498520
</ElementType>
@@ -552,6 +574,10 @@ class TableHeaderNode extends CollectionNode<any> {
552574
constructor(key: Key) {
553575
super(TableHeaderNode.type, key);
554576
}
577+
578+
filter(): CollectionNode<any> | null {
579+
return this.clone();
580+
}
555581
}
556582

557583
/**
@@ -699,6 +725,10 @@ class TableColumnNode extends CollectionNode<any> {
699725
constructor(key: Key) {
700726
super(TableColumnNode.type, key);
701727
}
728+
729+
filter(): CollectionNode<any> | null {
730+
return this.clone();
731+
}
702732
}
703733

704734
/**
@@ -938,12 +968,17 @@ export interface TableBodyProps<T> extends Omit<CollectionProps<T>, 'disabledKey
938968
}
939969

940970
// TODO: do we need this
971+
// These should probably be expecting TableCollection, will need to update others
941972
class TableBodyNode extends CollectionNode<any> {
942973
static readonly type = 'tablebody';
943974

944975
constructor(key: Key) {
945976
super(TableBodyNode.type, key);
946977
}
978+
979+
filter(collection: BaseCollection<any>, newCollection: BaseCollection<any>, filterFn: (textValue: string) => boolean): CollectionNode<any> | null {
980+
return super.filter(collection, newCollection, filterFn);
981+
}
947982
}
948983

949984
/**
@@ -1054,6 +1089,19 @@ class TableRowNode extends CollectionNode<any> {
10541089
constructor(key: Key) {
10551090
super(TableRowNode.type, key);
10561091
}
1092+
1093+
// TODO: bug is that filtering retains all rows after before the last match
1094+
filter(collection: BaseCollection<any>, newCollection: BaseCollection<any>, filterFn: (textValue: string) => boolean): CollectionNode<any> | null {
1095+
// todo walk children and if any match, just return whole thing?
1096+
let cells = collection.getChildren(this.key);
1097+
for (let cell of cells) {
1098+
if (filterFn(cell.textValue)) {
1099+
return this.clone();
1100+
}
1101+
}
1102+
1103+
return null;
1104+
}
10571105
}
10581106

10591107
/**
@@ -1247,6 +1295,10 @@ class TableCellNode extends CollectionNode<any> {
12471295
constructor(key: Key) {
12481296
super(TableCellNode.type, key);
12491297
}
1298+
1299+
filter(): CollectionNode<any> | null {
1300+
return this.clone();
1301+
}
12501302
}
12511303

12521304
/**
@@ -1414,6 +1466,10 @@ class TableLoaderNode extends CollectionNode<any> {
14141466
constructor(key: Key) {
14151467
super(TableLoaderNode.type, key);
14161468
}
1469+
1470+
filter(): CollectionNode<any> | null {
1471+
return this.clone();
1472+
}
14171473
}
14181474

14191475
export const TableLoadMoreItem = createLeafComponent(TableLoaderNode, function TableLoadingIndicator(props: TableLoadMoreItemProps, ref: ForwardedRef<HTMLTableRowElement>, item: Node<object>) {

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

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

1313
import {action} from '@storybook/addon-actions';
14-
import {Autocomplete, Button, Cell, Collection, Column, DialogTrigger, GridList, Header, Input, Keyboard, Label, ListBox, ListBoxSection, ListLayout, Menu, MenuItem, MenuSection, MenuTrigger, OverlayArrow, Popover, Row, SearchField, Select, SelectValue, Separator, SubmenuTrigger, Table, TableBody, TableHeader, TagGroup, TagList, Text, TextField, Tooltip, TooltipTrigger, Virtualizer} from 'react-aria-components';
14+
import {Autocomplete, Button, Cell, Collection, Column, DialogTrigger, GridList, Header, Input, Keyboard, Label, ListBox, ListBoxSection, ListLayout, Menu, MenuItem, MenuSection, MenuTrigger, OverlayArrow, Popover, Row, SearchField, Select, SelectValue, Separator, SubmenuTrigger, Table, TableBody, TableHeader, TableLayout, TagGroup, TagList, Text, TextField, Tooltip, TooltipTrigger, Virtualizer} from 'react-aria-components';
1515
import {Meta, StoryObj} from '@storybook/react';
1616
import {MyCheckbox} from './Table.stories';
1717
import {MyListBoxItem, MyMenuItem} from './utils';
@@ -983,50 +983,57 @@ export const AutocompleteWithTable = () => {
983983
<Label style={{display: 'block'}}>Test</Label>
984984
<Input />
985985
</TextField>
986-
<Table aria-label="Files" selectionMode="multiple" style={{height: 300, width: 300}}>
987-
<TableHeader>
988-
<Column>
989-
<MyCheckbox slot="selection" />
990-
</Column>
991-
<Column isRowHeader>Name</Column>
992-
<Column>Type</Column>
993-
<Column>Date Modified</Column>
994-
</TableHeader>
995-
<TableBody>
996-
<Row>
997-
<Cell>
998-
<MyCheckbox slot="selection" />
999-
</Cell>
1000-
<Cell>Games</Cell>
1001-
<Cell>File folder</Cell>
1002-
<Cell>6/7/2020</Cell>
1003-
</Row>
1004-
<Row>
1005-
<Cell>
1006-
<MyCheckbox slot="selection" />
1007-
</Cell>
1008-
<Cell>Program Files</Cell>
1009-
<Cell>File folder</Cell>
1010-
<Cell>4/7/2021</Cell>
1011-
</Row>
1012-
<Row>
1013-
<Cell>
1014-
<MyCheckbox slot="selection" />
1015-
</Cell>
1016-
<Cell>bootmgr</Cell>
1017-
<Cell>System file</Cell>
1018-
<Cell>11/20/2010</Cell>
1019-
</Row>
1020-
<Row>
1021-
<Cell>
986+
<Virtualizer
987+
layout={TableLayout}
988+
layoutOptions={{
989+
rowHeight: 25,
990+
headingHeight: 25
991+
}}>
992+
<Table aria-label="Files" selectionMode="multiple" style={{height: 400, width: 400, overflow: 'auto', scrollPaddingTop: 25}}>
993+
<TableHeader style={{background: 'var(--spectrum-gray-100)', width: '100%', height: '100%'}}>
994+
<Column>
1022995
<MyCheckbox slot="selection" />
1023-
</Cell>
1024-
<Cell>log.txt</Cell>
1025-
<Cell>Text Document</Cell>
1026-
<Cell>1/18/2016</Cell>
1027-
</Row>
1028-
</TableBody>
1029-
</Table>
996+
</Column>
997+
<Column isRowHeader>Name</Column>
998+
<Column>Type</Column>
999+
<Column>Date Modified</Column>
1000+
</TableHeader>
1001+
<TableBody>
1002+
<Row id="1">
1003+
<Cell>
1004+
<MyCheckbox slot="selection" />
1005+
</Cell>
1006+
<Cell>Games</Cell>
1007+
<Cell>File folder</Cell>
1008+
<Cell>6/7/2020</Cell>
1009+
</Row>
1010+
<Row id="2">
1011+
<Cell>
1012+
<MyCheckbox slot="selection" />
1013+
</Cell>
1014+
<Cell>Program Files</Cell>
1015+
<Cell>File folder</Cell>
1016+
<Cell>4/7/2021</Cell>
1017+
</Row>
1018+
<Row id="3">
1019+
<Cell>
1020+
<MyCheckbox slot="selection" />
1021+
</Cell>
1022+
<Cell>bootmgr</Cell>
1023+
<Cell>System file</Cell>
1024+
<Cell>11/20/2010</Cell>
1025+
</Row>
1026+
<Row id="4">
1027+
<Cell>
1028+
<MyCheckbox slot="selection" />
1029+
</Cell>
1030+
<Cell>log.txt</Cell>
1031+
<Cell>Text Document</Cell>
1032+
<Cell>1/18/2016</Cell>
1033+
</Row>
1034+
</TableBody>
1035+
</Table>
1036+
</Virtualizer>
10301037
</div>
10311038
</AutocompleteWrapper>
10321039
);

packages/react-stately/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export {useSearchFieldState} from '@react-stately/searchfield';
5353
export {useSelectState} from '@react-stately/select';
5454
export {useSliderState} from '@react-stately/slider';
5555
export {useMultipleSelectionState} from '@react-stately/selection';
56-
export {useTableState, TableHeader, TableBody, Column, Row, Cell, useTableColumnResizeState} from '@react-stately/table';
56+
export {useTableState, TableHeader, TableBody, Column, Row, Cell, useTableColumnResizeState, UNSTABLE_useFilteredTableState} from '@react-stately/table';
5757
export {useTabListState} from '@react-stately/tabs';
5858
export {useToastState, ToastQueue, useToastQueue} from '@react-stately/toast';
5959
export {useToggleState, useToggleGroupState} from '@react-stately/toggle';

0 commit comments

Comments
 (0)