Component props.
FilterVariantInputProps interface for filter variant input component
Additional props
Column to filter
A 32px-tall MUI Select bound to the column's debounced filter.
// Supplier column — plain text, no custom renderer.
<SelectColumnFilter column={supplierColumn} />
// No selection → shows "BVV, HiMedia, etc..." placeholder.
// Picking "BVV" + "HiMedia" → "BVV, HiMedia" in the field,
// column.setFilterValueDebounced(["BVV", "HiMedia"]).
// Country column — declared meta.renderSelectOption: code => flag emoji.
<SelectColumnFilter column={countryColumn} />
// Picking "US" + "CN" → "🇺🇸 🇨🇳" rendered in the field;
// dropdown rows also show the flag next to each option.
export default function SelectColumnFilter({ column }: FilterVariantInputProps) {
const filterValue = column.getFilterValue() as string[] | undefined;
const [columnFilterValue, setColumnFilterValue] = useState<string[]>(filterValue ?? []);
// Content-based key so the effect only fires when the external filter
// actually changes — not on every render, where `getFilterValue()` may
// return a fresh array reference with the same contents and would otherwise
// clobber in-progress local state.
const filterKey = (filterValue ?? []).join("�");
useEffect(() => {
setColumnFilterValue(filterValue ?? []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterKey]);
const columnFilterOptions = (column.getAllUniqueValues() as (string | number)[]).map(String);
const hasOptions = columnFilterOptions.length > 0;
const columnHeader = column.getHeaderText();
const renderSelectOption = column.columnDef.meta?.renderSelectOption;
const placeholder =
column.columnDef.meta?.filterPlaceholder ?? `Filter ${columnHeader?.toLowerCase() ?? ""}`;
const handleChange = (event: SelectChangeEvent<string[]>) => {
const { value } = event.target;
const next = typeof value === "string" ? value.split(",") : value;
setColumnFilterValue(next);
column.setFilterValueDebounced(next);
};
const renderValue = (selected: string[]): ReactNode => {
if (selected.length === 0) {
return <span style={{ color: "rgba(0, 0, 0, 0.4)" }}>{placeholder}</span>;
}
if (renderSelectOption) {
// Space-separated rendered nodes — e.g. "🇺🇸 🇨🇳 🇩🇪".
return selected.map((value, idx) => (
<span key={value} style={{ marginRight: idx < selected.length - 1 ? 4 : 0 }}>
{renderSelectOption(value)}
</span>
));
}
return selected.join(", ");
};
return (
<FormControl size="small" fullWidth sx={{ m: 0, mr: 0.5 }}>
<Select
multiple
displayEmpty
value={columnFilterValue}
onChange={handleChange}
disabled={!hasOptions}
renderValue={renderValue}
inputProps={{ "aria-label": columnHeader }}
sx={{
height: 32,
// `fontSize: inherit` bypasses the project-wide MuiSelect/Input
// theme overrides and picks up the TableCell's default, so the
// filter text matches the body rows.
fontSize: "inherit",
"& .MuiSelect-select": {
py: 0,
fontSize: "inherit",
display: "flex",
alignItems: "center",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
},
}}
MenuProps={{
slotProps: { paper: { sx: { maxHeight: 300 } } },
}}
>
{columnFilterOptions.map((option) => (
<MenuItem key={option} value={option} dense>
<Checkbox
size="small"
checked={columnFilterValue.includes(option)}
sx={{ p: 0.5, mr: 0.5 }}
/>
<ListItemText primary={renderSelectOption ? renderSelectOption(option) : option} />
</MenuItem>
))}
</Select>
</FormControl>
);
}
Compact multi-select filter for header columns with discrete values. Options come from the values actually present in the data (
column.getAllUniqueValues()). The column header sits directly above, so we skip the floating label and use the placeholder (frommeta.filterPlaceholder) for hinting when nothing is selected.Display rule:
…when they overflow the narrow header cell).meta.renderSelectOption→ the renderer's output for each value, space-separated (e.g. flag icons for the country column).