Skip to content

Commit e58e59e

Browse files
fix: datatable filter design (#834)
* fix: datatable filter design * feat: update dataview component too
1 parent 8dd4071 commit e58e59e

13 files changed

Lines changed: 210 additions & 61 deletions

File tree

apps/www/src/app/examples/dataview-beta/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,7 @@ const Page = () => {
610610
subHeading='Add your first teammate to get started.'
611611
/>
612612
</DataView.ZeroState>
613+
<DataView.ClearFilters />
613614
</DataView>
614615
</Flex>
615616
</Flex>

apps/www/src/components/dataview-demo.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ export function DataViewSearchDemo() {
227227
<DataView.EmptyState>
228228
<Text>No people match your search.</Text>
229229
</DataView.EmptyState>
230+
<DataView.ClearFilters />
230231
</DataView>
231232
</div>
232233
</Flex>
@@ -288,6 +289,7 @@ export function DataViewEmptyZeroDemo() {
288289
<DataView.ZeroState>
289290
<Text>Nothing here yet.</Text>
290291
</DataView.ZeroState>
292+
<DataView.ClearFilters />
291293
</DataView>
292294
</div>
293295
</Flex>

apps/www/src/content/docs/components/dataview/demo.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export const emptyZeroPreview = {
9595
<DataView.ZeroState>
9696
<Text>Nothing here yet.</Text>
9797
</DataView.ZeroState>
98+
<DataView.ClearFilters />
9899
</DataView>`
99100
}
100101
]
@@ -325,6 +326,7 @@ export const searchPreview = {
325326
<DataView.EmptyState>
326327
<Text>No people match your search.</Text>
327328
</DataView.EmptyState>
329+
<DataView.ClearFilters />
328330
</DataView>`
329331
}
330332
]

apps/www/src/content/docs/components/dataview/index.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949

5050
<DataView.EmptyState>{/* no matches */}</DataView.EmptyState>
5151
<DataView.ZeroState>{/* no data yet */}</DataView.ZeroState>
52+
<DataView.ClearFilters />
5253
</DataView>
5354
```
5455

@@ -86,10 +87,30 @@ Empty/zero is computed once on context (`isEmptyState`, `isZeroState`) and expos
8687
<DataView.ZeroState>
8788
<Text>Nothing here yet.</Text>
8889
</DataView.ZeroState>
90+
<DataView.ClearFilters />
8991
```
9092

9193
Renderers return `null` when `!hasData` — siblings render the messaging.
9294

95+
### Clear filters
96+
97+
When rows are hidden by filters, `DataView.List` automatically renders a flat
98+
footer summarising the hidden count with a **Clear Filters** action — the
99+
default treatment. `DataView.ClearFilters` is a separate sibling entry that
100+
surfaces the same action as a **bordered panel** in the empty state (a query
101+
returned no rows). It reads context, resets `filters`/`search` on click, and
102+
renders nothing outside the empty state — so place it once alongside
103+
`DataView.List`, separate from `DataView.EmptyState`, and it self-manages
104+
visibility.
105+
106+
```tsx
107+
<DataView.List variant="table" columns={tableColumns} />
108+
<DataView.EmptyState>
109+
<Text>No matches for your filters.</Text>
110+
</DataView.EmptyState>
111+
<DataView.ClearFilters />
112+
```
113+
93114
### Display Properties (column visibility)
94115

95116
Visibility is a **single global map** on context. `DataView.List` honours it for free (TanStack column visibility hides the grid track). For free-form renderers, wrap fields in `DataView.DisplayAccess`:
@@ -154,6 +175,12 @@ The popover housing the view switcher, Ordering, Grouping, and Display Propertie
154175

155176
<auto-type-table path="./props.ts" name="DataViewDisplayControlsProps" />
156177

178+
### DataView.ClearFilters
179+
180+
A context-driven "Clear Filters" affordance for the empty state. Place it as a sibling of `DataView.List` (separate from `DataView.EmptyState`); it renders a bordered panel when a query returns no rows and nothing otherwise. The flat footer for the data state is rendered automatically by `DataView.List`.
181+
182+
<auto-type-table path="./props.ts" name="DataViewClearFiltersProps" />
183+
157184
## Examples
158185

159186
### Search

apps/www/src/content/docs/components/dataview/props.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ export interface DataViewZeroStateProps {
205205
children: ReactNode;
206206
}
207207

208+
export interface DataViewClearFiltersProps {
209+
/** Class applied to the filter-summary row. */
210+
className?: string;
211+
}
212+
208213
export interface DataViewDisplayControlsProps {
209214
/** Custom trigger element for the popover. */
210215
trigger?: ReactNode;

packages/raystack/components/data-table/components/content.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,10 @@ export function Content({
313313
</Table>
314314
{showFilterSummary ? (
315315
<Flex
316-
className={styles.filterSummaryFooter}
316+
className={cx(
317+
styles.filterSummaryFooter,
318+
isEmptyState && styles.filterSummaryFooterEmpty
319+
)}
317320
justify='center'
318321
align='center'
319322
>

packages/raystack/components/data-table/data-table.module.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@
6363
box-sizing: border-box;
6464
}
6565

66+
/* Empty-state variant of the footer: the same row hugs its content and gains a
67+
border so it reads as a compact panel instead of a full-width bar. */
68+
.filterSummaryFooterEmpty {
69+
width: fit-content;
70+
gap: var(--rs-space-3);
71+
margin: var(--rs-space-6) auto var(--rs-space-9);
72+
padding: var(--rs-space-3) var(--rs-space-4);
73+
border: 0.5px solid var(--rs-color-border-base-primary);
74+
border-radius: var(--rs-radius-2);
75+
}
76+
6677
.filterSummaryCount {
6778
color: var(--rs-color-foreground-base-primary);
6879
font-family: var(--rs-font-body);

packages/raystack/components/data-view/__tests__/data-view.test.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -648,12 +648,39 @@ describe('DataView', () => {
648648
<DataView.EmptyState>
649649
<div data-testid='empty'>no matches</div>
650650
</DataView.EmptyState>
651+
<DataView.ClearFilters />
651652
</DataView>
652653
);
653-
// In empty state, filter summary is *not* shown by the List (renderer
654-
// returns null when !hasData). EmptyState sibling handles it. Just
655-
// confirm the empty state path is taken.
654+
// In the empty state the List renders nothing; the sibling
655+
// DataView.ClearFilters surfaces the affordance.
656656
expect(screen.getByTestId('empty')).toBeInTheDocument();
657+
const clearButton = screen.getByText('Clear Filters');
658+
expect(clearButton).toBeInTheDocument();
659+
660+
// Clearing exits the empty state and reveals the rows.
661+
await user.click(clearButton);
662+
expect(screen.queryByTestId('empty')).not.toBeInTheDocument();
663+
expect(screen.getByText('John Doe')).toBeInTheDocument();
664+
});
665+
666+
it('renders the flat footer summary when rows are hidden by filters', () => {
667+
render(
668+
<DataView
669+
data={mockData}
670+
fields={mockFields}
671+
defaultSort={defaultSort}
672+
mode='server'
673+
totalRowCount={10}
674+
query={{
675+
filters: [{ name: 'name', operator: 'neq', value: 'John Doe' }]
676+
}}
677+
>
678+
<DataView.List variant='table' columns={mockColumns} />
679+
</DataView>
680+
);
681+
// List auto-renders DataViewClearFilters as the footer (data present).
682+
expect(screen.getByText('items hidden by filters')).toBeInTheDocument();
683+
expect(screen.getByText('Clear Filters')).toBeInTheDocument();
657684
});
658685
});
659686

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use client';
2+
3+
import { Cross2Icon } from '@radix-ui/react-icons';
4+
import { cx } from 'class-variance-authority';
5+
import { useCallback } from 'react';
6+
import { Button } from '../../button';
7+
import { Flex } from '../../flex';
8+
import styles from '../data-view.module.css';
9+
import { useDataView } from '../hooks/useDataView';
10+
import {
11+
countLeafRows,
12+
getClientHiddenLeafRowCount,
13+
hasActiveTableFiltering
14+
} from '../utils';
15+
16+
export interface DataViewClearFiltersProps {
17+
className?: string;
18+
}
19+
20+
/**
21+
* The filter-summary row plus a "Clear Filters" action. Reads everything from
22+
* `DataView` context and renders nothing when there is nothing to clear.
23+
*
24+
* Flat by default (the footer `DataView.List` renders when rows are hidden by
25+
* filters); a bordered panel in the empty state. Shared between the List footer
26+
* and `DataView.ClearFilters` so the markup lives in one place.
27+
*
28+
* Internal — not exported from the package.
29+
*/
30+
export function FilterSummary({ className }: DataViewClearFiltersProps) {
31+
const {
32+
table,
33+
mode,
34+
isLoading,
35+
totalRowCount,
36+
isEmptyState,
37+
updateTableQuery
38+
} = useDataView();
39+
40+
const rows = table?.getRowModel()?.rows ?? [];
41+
const hiddenLeafRowCount =
42+
mode === 'client'
43+
? getClientHiddenLeafRowCount(table)
44+
: totalRowCount !== undefined
45+
? Math.max(0, totalRowCount - countLeafRows(rows))
46+
: null;
47+
const hasActiveFiltering = !isLoading && hasActiveTableFiltering(table);
48+
const showFilterSummary =
49+
hasActiveFiltering &&
50+
(mode === 'server' ||
51+
(typeof hiddenLeafRowCount === 'number' && hiddenLeafRowCount > 0));
52+
53+
const handleClearFilters = useCallback(() => {
54+
updateTableQuery(prev => ({ ...prev, filters: [], search: '' }));
55+
}, [updateTableQuery]);
56+
57+
// Matches DataTable: render only when rows are hidden by filters.
58+
// `isEmptyState` controls styling (bordered panel), not visibility.
59+
if (!showFilterSummary) return null;
60+
61+
return (
62+
<Flex
63+
className={cx(
64+
styles.filterSummaryFooter,
65+
isEmptyState && styles.filterSummaryFooterEmpty,
66+
className
67+
)}
68+
justify='center'
69+
align='center'
70+
>
71+
{mode === 'server' && hiddenLeafRowCount === null ? (
72+
<span className={styles.filterSummaryLabel}>
73+
Some items might be hidden by filters
74+
</span>
75+
) : (
76+
<Flex align='center' gap={2}>
77+
<span className={styles.filterSummaryCount}>
78+
{hiddenLeafRowCount}
79+
</span>
80+
<span className={styles.filterSummaryLabel}>
81+
items hidden by filters
82+
</span>
83+
</Flex>
84+
)}
85+
<Button
86+
variant='text'
87+
color='neutral'
88+
size='small'
89+
trailingIcon={<Cross2Icon />}
90+
onClick={handleClearFilters}
91+
>
92+
Clear Filters
93+
</Button>
94+
</Flex>
95+
);
96+
}
97+
98+
FilterSummary.displayName = 'DataView.FilterSummary';
99+
100+
/**
101+
* Surfaces the bordered "Clear Filters" panel in the empty state (a query
102+
* returned no rows). Place it as a sibling of `DataView.List` — separate from
103+
* `DataView.EmptyState`. Renders nothing outside the empty state; the flat
104+
* footer for the data state is rendered automatically by `DataView.List`.
105+
*/
106+
export function DataViewClearFilters({ className }: DataViewClearFiltersProps) {
107+
const { isEmptyState } = useDataView();
108+
if (!isEmptyState) return null;
109+
return <FilterSummary className={className} />;
110+
}
111+
112+
DataViewClearFilters.displayName = 'DataView.ClearFilters';

packages/raystack/components/data-view/components/list.tsx

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
'use client';
22

3-
import { Cross2Icon } from '@radix-ui/react-icons';
43
import type { Header, Row } from '@tanstack/react-table';
54
import { flexRender } from '@tanstack/react-table';
65
import { cx } from 'class-variance-authority';
76
import { CSSProperties, useCallback, useEffect, useMemo, useRef } from 'react';
87

98
import { Badge } from '../../badge';
10-
import { Button } from '../../button';
11-
import { Flex } from '../../flex';
129
import { Skeleton } from '../../skeleton';
1310
import styles from '../data-view.module.css';
1411
import {
@@ -22,11 +19,7 @@ import { useElementHeight } from '../hooks/useElementHeight';
2219
import { useInfiniteScroll } from '../hooks/useInfiniteScroll';
2320
import { useStickyGroupAnchor } from '../hooks/useStickyGroupAnchor';
2421
import { useVirtualRows } from '../hooks/useVirtualRows';
25-
import {
26-
countLeafRows,
27-
getClientHiddenLeafRowCount,
28-
hasActiveTableFiltering
29-
} from '../utils';
22+
import { FilterSummary } from './clear-filters';
3023

3124
function formatGridWidth(width: string | number | undefined) {
3225
if (width === undefined) return '1fr';
@@ -60,8 +53,6 @@ export function DataViewList<TData, TValue = unknown>({
6053
loadingRowCount = 3,
6154
loadMoreData,
6255
tableQuery,
63-
totalRowCount,
64-
updateTableQuery,
6556
activeView,
6657
registerFieldsForView,
6758
hasData
@@ -188,22 +179,6 @@ export function DataViewList<TData, TValue = unknown>({
188179
onLoadMore: loadMoreData
189180
});
190181

191-
const hiddenLeafRowCount =
192-
mode === 'client'
193-
? getClientHiddenLeafRowCount(table)
194-
: totalRowCount !== undefined
195-
? Math.max(0, totalRowCount - countLeafRows(rows))
196-
: null;
197-
const hasActiveFiltering = !isLoading && hasActiveTableFiltering(table);
198-
const showFilterSummary =
199-
hasActiveFiltering &&
200-
(mode === 'server' ||
201-
(typeof hiddenLeafRowCount === 'number' && hiddenLeafRowCount > 0));
202-
203-
const handleClearFilters = useCallback(() => {
204-
updateTableQuery(prev => ({ ...prev, filters: [], search: '' }));
205-
}, [updateTableQuery]);
206-
207182
// Sticky group anchor needs to recompute on scroll only. rAF-throttled so
208183
// the binary search runs at most once per frame regardless of how fast the
209184
// scroll events fire (mousewheel can dispatch dozens per frame on macOS).
@@ -507,37 +482,7 @@ export function DataViewList<TData, TValue = unknown>({
507482
aria-hidden='true'
508483
/>
509484
</div>
510-
{showFilterSummary ? (
511-
<Flex
512-
className={styles.filterSummaryFooter}
513-
justify='center'
514-
align='center'
515-
>
516-
{mode === 'server' && hiddenLeafRowCount === null ? (
517-
<span className={styles.filterSummaryLabel}>
518-
Some items might be hidden by filters
519-
</span>
520-
) : (
521-
<Flex align='center' gap={2}>
522-
<span className={styles.filterSummaryCount}>
523-
{hiddenLeafRowCount}
524-
</span>
525-
<span className={styles.filterSummaryLabel}>
526-
items hidden by filters
527-
</span>
528-
</Flex>
529-
)}
530-
<Button
531-
variant='text'
532-
color='neutral'
533-
size='small'
534-
trailingIcon={<Cross2Icon />}
535-
onClick={handleClearFilters}
536-
>
537-
Clear Filters
538-
</Button>
539-
</Flex>
540-
) : null}
485+
<FilterSummary />
541486
</div>
542487
);
543488
}

0 commit comments

Comments
 (0)