Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/www/src/app/examples/dataview-beta/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ const Page = () => {
subHeading='Add your first teammate to get started.'
/>
</DataView.ZeroState>
<DataView.ClearFilters />
</DataView>
</Flex>
</Flex>
Expand Down
2 changes: 2 additions & 0 deletions apps/www/src/components/dataview-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export function DataViewSearchDemo() {
<DataView.EmptyState>
<Text>No people match your search.</Text>
</DataView.EmptyState>
<DataView.ClearFilters />
</DataView>
</div>
</Flex>
Expand Down Expand Up @@ -288,6 +289,7 @@ export function DataViewEmptyZeroDemo() {
<DataView.ZeroState>
<Text>Nothing here yet.</Text>
</DataView.ZeroState>
<DataView.ClearFilters />
</DataView>
</div>
</Flex>
Expand Down
2 changes: 2 additions & 0 deletions apps/www/src/content/docs/components/dataview/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const emptyZeroPreview = {
<DataView.ZeroState>
<Text>Nothing here yet.</Text>
</DataView.ZeroState>
<DataView.ClearFilters />
</DataView>`
}
]
Expand Down Expand Up @@ -325,6 +326,7 @@ export const searchPreview = {
<DataView.EmptyState>
<Text>No people match your search.</Text>
</DataView.EmptyState>
<DataView.ClearFilters />
</DataView>`
}
]
Expand Down
27 changes: 27 additions & 0 deletions apps/www/src/content/docs/components/dataview/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {

<DataView.EmptyState>{/* no matches */}</DataView.EmptyState>
<DataView.ZeroState>{/* no data yet */}</DataView.ZeroState>
<DataView.ClearFilters />
</DataView>
```

Expand Down Expand Up @@ -86,10 +87,30 @@ Empty/zero is computed once on context (`isEmptyState`, `isZeroState`) and expos
<DataView.ZeroState>
<Text>Nothing here yet.</Text>
</DataView.ZeroState>
<DataView.ClearFilters />
```

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

### Clear filters

When rows are hidden by filters, `DataView.List` automatically renders a flat
footer summarising the hidden count with a **Clear Filters** action — the
default treatment. `DataView.ClearFilters` is a separate sibling entry that
surfaces the same action as a **bordered panel** in the empty state (a query
returned no rows). It reads context, resets `filters`/`search` on click, and
renders nothing outside the empty state — so place it once alongside
`DataView.List`, separate from `DataView.EmptyState`, and it self-manages
visibility.

```tsx
<DataView.List variant="table" columns={tableColumns} />
<DataView.EmptyState>
<Text>No matches for your filters.</Text>
</DataView.EmptyState>
<DataView.ClearFilters />
```

### Display Properties (column visibility)

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`:
Expand Down Expand Up @@ -154,6 +175,12 @@ The popover housing the view switcher, Ordering, Grouping, and Display Propertie

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

### DataView.ClearFilters

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

<auto-type-table path="./props.ts" name="DataViewClearFiltersProps" />

## Examples

### Search
Expand Down
5 changes: 5 additions & 0 deletions apps/www/src/content/docs/components/dataview/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ export interface DataViewZeroStateProps {
children: ReactNode;
}

export interface DataViewClearFiltersProps {
/** Class applied to the filter-summary row. */
className?: string;
}

export interface DataViewDisplayControlsProps {
/** Custom trigger element for the popover. */
trigger?: ReactNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,10 @@ export function Content({
</Table>
{showFilterSummary ? (
<Flex
className={styles.filterSummaryFooter}
className={cx(
styles.filterSummaryFooter,
isEmptyState && styles.filterSummaryFooterEmpty
)}
justify='center'
align='center'
>
Expand Down
11 changes: 11 additions & 0 deletions packages/raystack/components/data-table/data-table.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@
box-sizing: border-box;
}

/* Empty-state variant of the footer: the same row hugs its content and gains a
border so it reads as a compact panel instead of a full-width bar. */
.filterSummaryFooterEmpty {
width: fit-content;
gap: var(--rs-space-3);
margin: var(--rs-space-6) auto var(--rs-space-9);
padding: var(--rs-space-3) var(--rs-space-4);
border: 0.5px solid var(--rs-color-border-base-primary);
border-radius: var(--rs-radius-2);
}

.filterSummaryCount {
color: var(--rs-color-foreground-base-primary);
font-family: var(--rs-font-body);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,12 +648,39 @@ describe('DataView', () => {
<DataView.EmptyState>
<div data-testid='empty'>no matches</div>
</DataView.EmptyState>
<DataView.ClearFilters />
</DataView>
);
// In empty state, filter summary is *not* shown by the List (renderer
// returns null when !hasData). EmptyState sibling handles it. Just
// confirm the empty state path is taken.
// In the empty state the List renders nothing; the sibling
// DataView.ClearFilters surfaces the affordance.
expect(screen.getByTestId('empty')).toBeInTheDocument();
const clearButton = screen.getByText('Clear Filters');
expect(clearButton).toBeInTheDocument();

// Clearing exits the empty state and reveals the rows.
await user.click(clearButton);
expect(screen.queryByTestId('empty')).not.toBeInTheDocument();
expect(screen.getByText('John Doe')).toBeInTheDocument();
});

it('renders the flat footer summary when rows are hidden by filters', () => {
render(
<DataView
data={mockData}
fields={mockFields}
defaultSort={defaultSort}
mode='server'
totalRowCount={10}
query={{
filters: [{ name: 'name', operator: 'neq', value: 'John Doe' }]
}}
>
<DataView.List variant='table' columns={mockColumns} />
</DataView>
);
// List auto-renders DataViewClearFilters as the footer (data present).
expect(screen.getByText('items hidden by filters')).toBeInTheDocument();
expect(screen.getByText('Clear Filters')).toBeInTheDocument();
});
});

Expand Down
112 changes: 112 additions & 0 deletions packages/raystack/components/data-view/components/clear-filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use client';

import { Cross2Icon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
import { useCallback } from 'react';
import { Button } from '../../button';
import { Flex } from '../../flex';
import styles from '../data-view.module.css';
import { useDataView } from '../hooks/useDataView';
import {
countLeafRows,
getClientHiddenLeafRowCount,
hasActiveTableFiltering
} from '../utils';

export interface DataViewClearFiltersProps {
className?: string;
}

/**
* The filter-summary row plus a "Clear Filters" action. Reads everything from
* `DataView` context and renders nothing when there is nothing to clear.
*
* Flat by default (the footer `DataView.List` renders when rows are hidden by
* filters); a bordered panel in the empty state. Shared between the List footer
* and `DataView.ClearFilters` so the markup lives in one place.
*
* Internal — not exported from the package.
*/
export function FilterSummary({ className }: DataViewClearFiltersProps) {
const {
table,
mode,
isLoading,
totalRowCount,
isEmptyState,
updateTableQuery
} = useDataView();

const rows = table?.getRowModel()?.rows ?? [];
const hiddenLeafRowCount =
mode === 'client'
? getClientHiddenLeafRowCount(table)
: totalRowCount !== undefined
? Math.max(0, totalRowCount - countLeafRows(rows))
: null;
const hasActiveFiltering = !isLoading && hasActiveTableFiltering(table);
const showFilterSummary =
hasActiveFiltering &&
(mode === 'server' ||
(typeof hiddenLeafRowCount === 'number' && hiddenLeafRowCount > 0));

const handleClearFilters = useCallback(() => {
updateTableQuery(prev => ({ ...prev, filters: [], search: '' }));
}, [updateTableQuery]);

// Matches DataTable: render only when rows are hidden by filters.
// `isEmptyState` controls styling (bordered panel), not visibility.
if (!showFilterSummary) return null;

return (
<Flex
className={cx(
styles.filterSummaryFooter,
isEmptyState && styles.filterSummaryFooterEmpty,
className
)}
justify='center'
align='center'
>
{mode === 'server' && hiddenLeafRowCount === null ? (
<span className={styles.filterSummaryLabel}>
Some items might be hidden by filters
</span>
) : (
<Flex align='center' gap={2}>
<span className={styles.filterSummaryCount}>
{hiddenLeafRowCount}
</span>
<span className={styles.filterSummaryLabel}>
items hidden by filters
</span>
</Flex>
)}
<Button
variant='text'
color='neutral'
size='small'
trailingIcon={<Cross2Icon />}
onClick={handleClearFilters}
>
Clear Filters
</Button>
</Flex>
);
}

FilterSummary.displayName = 'DataView.FilterSummary';

/**
* Surfaces the bordered "Clear Filters" panel in the empty state (a query
* returned no rows). Place it as a sibling of `DataView.List` — separate from
* `DataView.EmptyState`. Renders nothing outside the empty state; the flat
* footer for the data state is rendered automatically by `DataView.List`.
*/
export function DataViewClearFilters({ className }: DataViewClearFiltersProps) {
const { isEmptyState } = useDataView();
if (!isEmptyState) return null;
return <FilterSummary className={className} />;
}

DataViewClearFilters.displayName = 'DataView.ClearFilters';
59 changes: 2 additions & 57 deletions packages/raystack/components/data-view/components/list.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
'use client';

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

import { Badge } from '../../badge';
import { Button } from '../../button';
import { Flex } from '../../flex';
import { Skeleton } from '../../skeleton';
import styles from '../data-view.module.css';
import {
Expand All @@ -22,11 +19,7 @@ import { useElementHeight } from '../hooks/useElementHeight';
import { useInfiniteScroll } from '../hooks/useInfiniteScroll';
import { useStickyGroupAnchor } from '../hooks/useStickyGroupAnchor';
import { useVirtualRows } from '../hooks/useVirtualRows';
import {
countLeafRows,
getClientHiddenLeafRowCount,
hasActiveTableFiltering
} from '../utils';
import { FilterSummary } from './clear-filters';

function formatGridWidth(width: string | number | undefined) {
if (width === undefined) return '1fr';
Expand Down Expand Up @@ -60,8 +53,6 @@ export function DataViewList<TData, TValue = unknown>({
loadingRowCount = 3,
loadMoreData,
tableQuery,
totalRowCount,
updateTableQuery,
activeView,
registerFieldsForView,
hasData
Expand Down Expand Up @@ -188,22 +179,6 @@ export function DataViewList<TData, TValue = unknown>({
onLoadMore: loadMoreData
});

const hiddenLeafRowCount =
mode === 'client'
? getClientHiddenLeafRowCount(table)
: totalRowCount !== undefined
? Math.max(0, totalRowCount - countLeafRows(rows))
: null;
const hasActiveFiltering = !isLoading && hasActiveTableFiltering(table);
const showFilterSummary =
hasActiveFiltering &&
(mode === 'server' ||
(typeof hiddenLeafRowCount === 'number' && hiddenLeafRowCount > 0));

const handleClearFilters = useCallback(() => {
updateTableQuery(prev => ({ ...prev, filters: [], search: '' }));
}, [updateTableQuery]);

// Sticky group anchor needs to recompute on scroll only. rAF-throttled so
// the binary search runs at most once per frame regardless of how fast the
// scroll events fire (mousewheel can dispatch dozens per frame on macOS).
Expand Down Expand Up @@ -507,37 +482,7 @@ export function DataViewList<TData, TValue = unknown>({
aria-hidden='true'
/>
</div>
{showFilterSummary ? (
<Flex
className={styles.filterSummaryFooter}
justify='center'
align='center'
>
{mode === 'server' && hiddenLeafRowCount === null ? (
<span className={styles.filterSummaryLabel}>
Some items might be hidden by filters
</span>
) : (
<Flex align='center' gap={2}>
<span className={styles.filterSummaryCount}>
{hiddenLeafRowCount}
</span>
<span className={styles.filterSummaryLabel}>
items hidden by filters
</span>
</Flex>
)}
<Button
variant='text'
color='neutral'
size='small'
trailingIcon={<Cross2Icon />}
onClick={handleClearFilters}
>
Clear Filters
</Button>
</Flex>
) : null}
<FilterSummary />
</div>
);
}
Expand Down
Loading
Loading