diff --git a/apps/www/src/content/docs/components/filter-chip/index.mdx b/apps/www/src/content/docs/components/filter-chip/index.mdx
index 7558ac6eb..0a1f1fccd 100644
--- a/apps/www/src/content/docs/components/filter-chip/index.mdx
+++ b/apps/www/src/content/docs/components/filter-chip/index.mdx
@@ -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.
+
### Controlled
diff --git a/apps/www/src/content/docs/components/filter-chip/props.ts b/apps/www/src/content/docs/components/filter-chip/props.ts
index bbd8d2d6b..482e8aea9 100644
--- a/apps/www/src/content/docs/components/filter-chip/props.ts
+++ b/apps/www/src/content/docs/components/filter-chip/props.ts
@@ -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
diff --git a/packages/react/components/data-table/__tests__/data-table.test.tsx b/packages/react/components/data-table/__tests__/data-table.test.tsx
index a006720d9..737a77052 100644
--- a/packages/react/components/data-table/__tests__/data-table.test.tsx
+++ b/packages/react/components/data-table/__tests__/data-table.test.tsx
@@ -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';
@@ -669,6 +670,37 @@ describe('DataTable', () => {
});
});
+ describe('Restored filter chips', () => {
+ const chipColumns: DataTableColumnDef[] = [
+ {
+ accessorKey: 'name',
+ header: 'Name',
+ enableColumnFilter: true,
+ filterType: 'string'
+ }
+ ];
+
+ it('shows the operator the query carries', async () => {
+ const { container } = render(
+
+
+
+ );
+ 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[] = [
diff --git a/packages/react/components/data-table/components/filters.tsx b/packages/react/components/data-table/components/filters.tsx
index b120f22df..a79dc5637 100644
--- a/packages/react/components/data-table/components/filters.tsx
+++ b/packages/react/components/data-table/components/filters.tsx
@@ -158,6 +158,7 @@ export function Filters({
)
}
columnType={filter.filterType}
+ operation={filter.operator}
options={filter.options}
selectProps={filter.selectProps}
calendarProps={filter.calendarProps}
diff --git a/packages/react/components/data-view/__tests__/data-view.test.tsx b/packages/react/components/data-view/__tests__/data-view.test.tsx
index 0592c7d8c..07b7bf551 100644
--- a/packages/react/components/data-view/__tests__/data-view.test.tsx
+++ b/packages/react/components/data-view/__tests__/data-view.test.tsx
@@ -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 {
@@ -562,6 +562,31 @@ describe('DataView', () => {
});
});
+ describe('Restored filter chips', () => {
+ it('shows the operator the query carries', async () => {
+ const { container } = render(
+
+
+
+
+
+
+ );
+ 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' },
diff --git a/packages/react/components/data-view/components/filters.tsx b/packages/react/components/data-view/components/filters.tsx
index 28f6a5cdc..ab36936e0 100644
--- a/packages/react/components/data-view/components/filters.tsx
+++ b/packages/react/components/data-view/components/filters.tsx
@@ -170,6 +170,7 @@ export function Filters({
)
}
columnType={filter.filterType}
+ operation={filter.operator}
options={filter.options}
selectProps={filter.selectProps}
className={classNames?.filterChips}
diff --git a/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx b/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx
index c82920a60..391d9a2d8 100644
--- a/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx
+++ b/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx
@@ -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()
+ ).toBe('is not');
+ });
+
+ it('falls back to the first operation when the type does not have it', async () => {
+ expect(
+ await operationText()
+ ).toBe('is');
+ });
+
+ it('shows the first operation when the prop is absent', async () => {
+ expect(await operationText()).toBe('is');
+ });
+
+ it('reads the operation from a custom list', async () => {
+ expect(
+ await operationText(
+
+ )
+ ).toBe('is not');
+ });
+ });
+
describe('Remove Functionality', () => {
it('shows remove button when onRemove is provided', () => {
const onRemove = vi.fn();
diff --git a/packages/react/components/filter-chip/filter-chip.tsx b/packages/react/components/filter-chip/filter-chip.tsx
index 4afe7aeff..b3f5a8acb 100644
--- a/packages/react/components/filter-chip/filter-chip.tsx
+++ b/packages/react/components/filter-chip/filter-chip.tsx
@@ -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';
@@ -103,6 +104,12 @@ export interface FilterChipProps
onOperationChange?: (operation: string) => void;
leadingIcon?: ReactNode;
operations?: FilterOperator[];
+ /**
+ * 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;
@@ -129,6 +136,7 @@ export const FilterChip = ({
leadingIcon,
variant,
operations,
+ operation: operationProp,
selectProps,
calendarProps,
...props
@@ -137,9 +145,18 @@ export const FilterChip = ({
? operations
: filterOperators[columnType];
- const [operation, setOperation] = useState(
- computedOperations?.[0]
- );
+ const selectedOperation =
+ operationProp === undefined
+ ? undefined
+ : (computedOperations?.find(option => option.value === operationProp) ??
+ computedOperations?.[0]);
+
+ const [operation, setOperation] = useControlled({
+ 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
@@ -183,7 +200,7 @@ export const FilterChip = ({
setOperation(operation);
if (operation?.value) onOperationChange?.(operation.value);
},
- [onOperationChange]
+ [onOperationChange, setOperation]
);
const handleFilterValueChange = useCallback(