FilterSearch
A composable data-table toolbar — search, a tenant switcher, a filter popover, and applied-filter chips.
Usage
import {
FilterSearch,
FilterSearchActions,
FilterSearchFilters,
FilterSearchAppliedFilters,
useFilterSearchFilters,
} from '@acronis-platform/ui-react';FilterSearch is a composable toolbar for data tables: a horizontal row that
arranges a search field, an optional tenant/scope switcher, an optional filter
popover, and trailing action buttons. It's a pure layout wrapper — you place
Search, Select, FilterSearchFilters, and FilterSearchActions as direct
children.
FilterSearchFilters is the real filter popover: a trigger button that opens
a form (Reset filters / Cancel / Apply) over a draft copy of the applied
filters. It snapshots value on open, lets field children mutate the draft,
and only commits it back through onValueChange (plus the optional
onApply) when Apply is pressed — disabled until the draft actually
differs from the applied value. That comparison is structural (a deep
compare, not reference identity), so re-picking the same array or object value
leaves Apply disabled. Cancel, Escape, and outside-press revert the draft.
Fields that don't fit the popover's height scroll internally.
Any prop it doesn't consume itself is forwarded to the trigger button, so
data-testid, id, disabled, and aria-* land on the real <button>
(className is merged onto it too). The footer labels (resetFiltersLabel,
cancelLabel, applyLabel) and the trigger label are overridable for
localization. side, align, sideOffset, contentClassName,
portalContainer, collisionBoundary, and positionMethod are forwarded to
the underlying PopoverContent for consumers who need to configure the
popover directly — portalContainer/positionMethod matter mainly when the
toolbar renders inside a constrained MFE/Shadow DOM mount (see the Popover
docs).
FilterSearchAppliedFilters renders the currently applied filters as
removable chips, plus a top-level Reset filters action that clears
everything immediately (no popover involved). It's a separate component,
rendered as a sibling row below FilterSearch, and renders nothing when
there are no applied filters. Chip labels default to "<key>: <value>" —
array values are joined with ", " and object values are JSON-stringified.
Pass getFilterChipLabel(key, value) whenever the raw key or a stringified
object isn't presentable (a localized field name, a formatted date range).
Each chip's remove-control accessible label defaults to "Remove <key> filter" — override via getRemoveFilterLabel(key) to localize it. Its
resetFiltersLabel is independent from FilterSearchFilters's own prop of
the same name — set both if you localize.
Filter fields are supplied as plain children inside FilterSearchFilters.
Each field wires itself to the draft with the useFilterSearchFilters()
context hook, which returns the current draft filters map and a per-key
setFilter setter:
function StatusField() {
const { filters, setFilter } = useFilterSearchFilters();
return (
<InputSelect
value={(filters.status as string) ?? 'all'}
onValueChange={(value) =>
setFilter('status', value === 'all' ? undefined : value)
}
>
{/* … */}
</InputSelect>
);
}Calling setFilter(key, undefined) deletes that key from the draft instead
of storing a literal undefined. Route a field's "all"/"any" option through it,
as above — an unset filter is the absence of a key, so it stays out of the
applied set and out of the chip row rather than showing a status: undefined
chip that can't be cleared.
Group fields with a Separator as a layout convention if desired. Calling
useFilterSearchFilters() outside a <FilterSearchFilters> popover throws.
Need a filter popover outside a FilterSearch toolbar? See the
Filter popover pattern for the hand-rolled
Button + Popover + InputSelect composition.
Design-pending.
FilterSearch/FilterSearchActionshave a real Figma node (see the API reference below), andFilterSearchAppliedFilters's chip row now does too — it landed as its ownFilterChipscomponent, whichFilterSearchAppliedFiltersrenders through.FilterSearchFiltersstill has no node: it themes entirely from shared semantic tokens plus its composed components' own tiers (Button,Popover), and will be reconciled against the real design once a mockup lands.
Examples
A full toolbar — tenant switcher, search, filter popover, and applied-filter chips:
function DeviceFilterFields() {
return (
<>
<TypeField />
<PricingModeField />
<Separator />
<StatusField />
</>
);
}
function Toolbar() {
const [filters, setFilters] = useState<Record<string, unknown>>({});
return (
<div className="flex flex-col gap-3">
<FilterSearch>
<Select defaultValue="acme">
<SelectTrigger className="w-56">
<SelectValue placeholder="All customers" />
</SelectTrigger>
<SelectContent>{/* … */}</SelectContent>
</Select>
<InputSearch
placeholder="Enter text to filter"
aria-label="Search"
className="w-56"
/>
<FilterSearchFilters
value={filters}
onValueChange={setFilters}
label="Table filters"
>
<DeviceFilterFields />
</FilterSearchFilters>
</FilterSearch>
<FilterSearchAppliedFilters
filters={filters}
onValueChange={setFilters}
/>
</div>
);
}Search-only, no tenant switcher or filters:
<FilterSearch>
<InputSearch
placeholder="Search table"
aria-label="Search"
className="w-56"
/>
</FilterSearch>With trailing action buttons:
<FilterSearch>
<InputSearch
placeholder="Search table"
aria-label="Search"
className="w-56"
/>
<FilterSearchFilters value={filters} onValueChange={setFilters} />
<FilterSearchActions>
<Button>Export</Button>
</FilterSearchActions>
</FilterSearch>API Reference
Prop
Type
Prop
Type
Prop
Type
Prop
Type
The useFilterSearchFilters() hook returns the current draft filters map
and a setFilter(key, value) setter for wiring filter-field children to the
popover draft — passing undefined as the value drops the key from the draft.