Skip to content
Open
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
4 changes: 4 additions & 0 deletions apps/www/src/content/docs/components/filter-chip/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ FilterChip supports custom filter operations through the `operations` prop. When
- When a single operation is provided, it is displayed as static text
- If no operations are specified, default operations are used based on the column type

The chip starts on the first operation. Pass `operation` with the active value to
start on another one, for example when you restore a filter from a saved query.
A value the list does not have falls back to the first one.

<Demo data={operationsDemo} />

### Controlled
Expand Down
5 changes: 5 additions & 0 deletions apps/www/src/content/docs/components/filter-chip/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export interface FilterChipProps {
/** Optional array of operations for the type of filter operation */
operations?: { label: string; value: string }[];

/** Active operation, by its `value`. A value `operations` does not have falls back to the first one.
* @default the first entry of `operations`
*/
operation?: string;

/** Callback when the filter value changes; receives the value and the active
* operation. For `select` and `multiselect`, the value is the picked
* option's `value`, with its type kept. For `number`, non-numeric input is
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { render, screen } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { getSlot } from '~/test-utils/data-slots';
import { DataTable } from '../data-table';
import styles from '../data-table.module.css';
import { DataTableColumnDef, DataTableQuery } from '../data-table.types';
Expand Down Expand Up @@ -669,6 +670,37 @@ describe('DataTable', () => {
});
});

describe('Restored filter chips', () => {
const chipColumns: DataTableColumnDef<TestData, unknown>[] = [
{
accessorKey: 'name',
header: 'Name',
enableColumnFilter: true,
filterType: 'string'
}
];

it('shows the operator the query carries', async () => {
const { container } = render(
<DataTable
data={mockData}
columns={chipColumns}
defaultSort={{ name: 'name', order: 'asc' }}
query={{
filters: [{ name: 'name', operator: 'neq', value: 'John Doe' }]
}}
>
<DataTable.Toolbar />
</DataTable>
);
await act(() => new Promise(r => setTimeout(r, 0)));

expect(getSlot(container, 'filter-chip-operation')).toHaveTextContent(
'is not'
);
});
});

describe('Restored date filters', () => {
type Dated = { id: string; when?: string };
const dated: Dated[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export function Filters<TData, TValue>({
)
}
columnType={filter.filterType}
operation={filter.operator}
options={filter.options}
selectProps={filter.selectProps}
calendarProps={filter.calendarProps}
Expand Down
27 changes: 26 additions & 1 deletion packages/react/components/data-view/__tests__/data-view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
it,
vi
} from 'vitest';

import { getSlot } from '~/test-utils/data-slots';
// biome-ignore lint/suspicious/noShadowRestrictedNames: legitimate export name
import { DataView } from '../data-view';
import type {
Expand Down Expand Up @@ -562,6 +562,31 @@ describe('DataView', () => {
});
});

describe('Restored filter chips', () => {
it('shows the operator the query carries', async () => {
const { container } = render(
<DataView
data={mockData}
fields={mockFields}
defaultSort={defaultSort}
query={{
filters: [{ name: 'name', operator: 'neq', value: 'John Doe' }]
}}
>
<DataView.Toolbar>
<DataView.Filters />
</DataView.Toolbar>
<DataView.List variant='table' columns={mockColumns} />
</DataView>
);
await act(() => new Promise(r => setTimeout(r, 0)));

expect(getSlot(container, 'filter-chip-operation')).toHaveTextContent(
'is not'
);
});
});

describe('Multi-view', () => {
const views: ViewSpec[] = [
{ value: 'table', label: 'Table' },
Expand Down
1 change: 1 addition & 0 deletions packages/react/components/data-view/components/filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export function Filters<TData>({
)
}
columnType={filter.filterType}
operation={filter.operator}
options={filter.options}
selectProps={filter.selectProps}
className={classNames?.filterChips}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,48 @@ describe('FilterChip', () => {
});
});

describe('Operation', () => {
const flushMicrotasks = async () =>
await new Promise(r => setTimeout(r, 0));

const operationText = async (ui: React.ReactElement) => {
const { container } = render(ui);
await act(flushMicrotasks);
return getSlot(container, 'filter-chip-operation')?.textContent;
};

it('shows the operation named by the prop', async () => {
expect(
await operationText(<FilterChip label='Name' operation='neq' />)
).toBe('is not');
});

it('falls back to the first operation when the type does not have it', async () => {
expect(
await operationText(<FilterChip label='Name' operation='in' />)
).toBe('is');
});

it('shows the first operation when the prop is absent', async () => {
expect(await operationText(<FilterChip label='Name' />)).toBe('is');
});

it('reads the operation from a custom list', async () => {
expect(
await operationText(
<FilterChip
label='Name'
operations={[
{ value: 'eq', label: 'is' },
{ value: 'neq', label: 'is not' }
]}
operation='neq'
/>
)
).toBe('is not');
});
});

describe('Remove Functionality', () => {
it('shows remove button when onRemove is provided', () => {
const onRemove = vi.fn();
Expand Down
25 changes: 21 additions & 4 deletions packages/react/components/filter-chip/filter-chip.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { useControlled } from '@base-ui/utils/useControlled';
import { TZDate } from '@date-fns/tz';
import { cva, cx, VariantProps } from 'class-variance-authority';
import { parseISO } from 'date-fns';
Expand Down Expand Up @@ -103,6 +104,12 @@ export interface FilterChipProps
onOperationChange?: (operation: string) => void;
leadingIcon?: ReactNode;
operations?: FilterOperator<string>[];
/**
* Active operation, by its `value`. A value `operations` does not have falls
* back to the first one.
* @default the first entry of `operations`
*/
operation?: string;
selectProps?: BaseSelectProps;
/** Props forwarded to the `CalendarPreview` for `columnType="date"`. */
calendarProps?: FilterChipCalendarProps;
Expand All @@ -129,6 +136,7 @@ export const FilterChip = ({
leadingIcon,
variant,
operations,
operation: operationProp,
selectProps,
calendarProps,
...props
Expand All @@ -137,9 +145,18 @@ export const FilterChip = ({
? operations
: filterOperators[columnType];

const [operation, setOperation] = useState<FilterOperation | undefined>(
computedOperations?.[0]
);
const selectedOperation =
operationProp === undefined
? undefined
: (computedOperations?.find(option => option.value === operationProp) ??
computedOperations?.[0]);

const [operation, setOperation] = useControlled<FilterOperation | undefined>({
controlled: selectedOperation,
default: computedOperations?.[0],
name: 'FilterChip',
state: 'operation'
});
const isNumberColumn = columnType === FilterType.number;
// `??` not `||`, since a falsy option value like `0` is a real selection.
// For `number`, the value is shown without an exponent (`1e-7`), and a
Expand Down Expand Up @@ -183,7 +200,7 @@ export const FilterChip = ({
setOperation(operation);
if (operation?.value) onOperationChange?.(operation.value);
},
[onOperationChange]
[onOperationChange, setOperation]
);

const handleFilterValueChange = useCallback(
Expand Down
Loading