Filter Button
<Stack spacing={4} direction="column"><State initial={false}>{([value, setValue]) => (<FilterButton label="Active" value={value} onClick={() => setValue(!value)} />)}</State><Stack spacing={1}><FilterButton label="Active" value={true} /><FilterButton label="Active" value={false} /></Stack></Stack>
<Stack spacing={4} direction="column"><State initial={false}>{([value, setValue]) => (<FilterButton label="Active" icon="check" value={value} onClick={() => setValue(!value)} />)}</State><Stack spacing={1}><FilterButton label="Active" icon="check" value={true} /><FilterButton label="Active" icon="check" value={false} /></Stack></Stack>
const SingleSelect = () => {const [value, setValue] = React.useState(0);const [open, setOpen] = React.useState(false);const onChangeHandler = (e) => {setValue(e);setOpen(false);};const sortOptions = [{text: 'Newest',value: 0,},{text: 'Oldest',value: 1,},{text: 'Name: A to Z',value: 2,},{text: 'Name: Z to A',value: 3,},{text: 'Price: Low to High',value: 4,},{text: 'Price: High to Low',value: 5,},];return (<Popoverel="span"direction="br"padding="s"onClickOutside={() => setOpen(false)}open={open}portal // This is optionaltrigger={<FilterButtononClick={() => setOpen(!open)}value={value !== 0 && sortOptions[value].text}label="Sort"expandIcon={open}/>}><OptionList options={sortOptions} onChange={onChangeHandler} /></Popover>);};render (SingleSelect)
const MultiSelect = () => {const [IValue, setIValue] = React.useState([]);const [value, setValue] = React.useState(IValue);const [open, setOpen] = React.useState(false);const multiOptions = [{text: 'Category 0',value: 0,},{text: 'Category 1',value: 1,},{text: 'Category 2',value: 2,},{text: 'Category 3',value: 3,},{text: 'Category 4',value: 4,},{text: 'Category 5',value: 5,},];const onChangeHandler = (data, checked) => {if (checked) {setValue((prevState) => [...prevState, data]);} else {setValue((prevState) =>[...prevState].filter((item) => item !== data));}};const onSaveHandler = () => {setIValue(value);setOpen(false);};const onCancelHandler = () => {setValue(IValue);setOpen(false);};return (<Popoverel="span"direction="br"padding="s"onClickOutside={() => setOpen(false)}open={open}portal // This is optionalfooter={<Stack direction="row-reverse" spacing="1" className="w-100"><Buttonsize="small"primaryonClick={onSaveHandler}disabled={value === IValue}>Apply Filter</Button><Button size="small" onClick={onCancelHandler}>Cancel</Button></Stack>}trigger={<FilterButtononClick={() => setOpen(!open)}value={IValue.length > 0 && IValue.length}label="Category"expandIcon={open}/>}><OptionListmultipleoptions={multiOptions}onChange={onChangeHandler}value={value}/></Popover>);};render (MultiSelect)
const CustomInput = () => {const [value, setValue] = React.useState({min: undefined, max: undefined});const [iValue, setIValue] = React.useState(value);const [open, setOpen] = React.useState(false);const minRef = React.useRef(null);const maxRef = React.useRef(null);const onApply = () => {setValue(iValue);setOpen(false);};const onChangeHandler = (e, d) => {// TODO: Requires more validationsetIValue(prevState => ({...prevState, [d.placeholder.toLowerCase()]: d.value}));}const onClearHandler = () => {setValue({min: undefined, max: undefined});setOpen(false);}return (<Popoverel="span"direction="br"padding="s"onClickOutside={() => setOpen(false)}open={open}portal // This is optionaltrigger={<FilterButtononClick={() => setOpen(!open)}value={!!value.min && !!value.max ? `${value.min} to ${value.max}`: !!value.min ? `minimum ${value.min}`: !!value.max ? `maximum ${value.max}`: undefined}label="Hours"expandIcon={open}/>}><Stack direction="column" spacing={1}><Stack direction="row" spacing={2} alignItems="center"><Input placeholder="Min" defaultValue={value.min} style={{ minWidth: 0 }} onChange={onChangeHandler} type="number" />to<Input placeholder="Max" defaultValue={value.max} style={{ minWidth: 0 }} onChange={onChangeHandler} type="number" /></Stack><Stack direction="row-reverse" spacing={1}><Button full onClick={onApply} primary>Apply</Button><Button full onClick={onClearHandler}>Clear</Button></Stack></Stack></Popover>);};render (CustomInput)
const CustomDateRange = () => {const [dateRange, setDateRange] = React.useState();return (<Stackdirection="column"className="m-2"spacing={2}style={{ maxWidth: 400 }}><DateRangePickervalue={dateRange}onChange={e => {setDateRange(e);}}trigger={(value, props, open) => <FilterButton {...props} label="Date Range" icon="event" value={!!dateRange}/>}/></Stack>);};render (CustomDateRange)
In the case where a filter or sort option does not have an off state, but instead a default option, the Filter Button is shown as non-active when the default option is selected. As an example, when sorting content where the default order is alphabetical A→Z, the Filter Button should appear non-active when that option is selected, since it is the default.
Selecting an option from All Filters should also update the related Filter Button when available. This also works visa versa, if filter was applied by an individual Filter Button, it should also reflect the selection on All Filter button.
const AllFilterExample = () => {const [states, setStates] = React.useState({sort: 1,category: [],});const sortOptions = [{text: 'Newest',value: 0,},{text: 'Oldest',value: 1,},{text: 'Name: A to Z',value: 2,},{text: 'Name: Z to A',value: 3,},{text: 'Price: Low to High',value: 4,},{text: 'Price: High to Low',value: 5,},];const categoryOptions = [{text: 'Category 0',value: 0,},{text: 'Category 1',value: 1,},{text: 'Category 2',value: 2,},{text: 'Category 3',value: 3,},{text: 'Category 4',value: 4,},{text: 'Category 5',value: 5,},];const SortFilter = React.useCallback(() => {const [open, setOpen] = React.useState(false);// This effect won't be called on window resize, because// it is wrapped within useCallback() hook.React.useEffect(() => console.log('SortFilter effect'), []);const onChangeHandler = (e) => {setStates((prevState) => ({ ...prevState, sort: e }));setOpen(false);};return (<Popoverel="span"direction="br"padding="s"onClickOutside={() => setOpen(false)}open={open}portaltrigger={<FilterButtononClick={() => setOpen(!open)}value={states.sort !== 0 && sortOptions[states.sort].text}label="Sort"expandIcon={open}/>}><OptionList options={sortOptions} onChange={onChangeHandler} /></Popover>);}, [states]);const CategoryFilter = () => {const [value, setValue] = React.useState(states.category);const [open, setOpen] = React.useState(false);// This effect to be called on each window resize in GatsbyReact.useEffect(() => console.log('CategoryFilter effect'), []);const onChangeHandler = (data, checked) => {if (checked) {setValue((prevState) => [...prevState, data]);} else {setValue((prevState) =>[...prevState].filter((item) => item !== data));}};const onSaveHandler = () => {setStates((prevState) => ({ ...prevState, category: value }));setOpen(false);};const onCancelHandler = () => {setValue(states.category);setOpen(false);};return (<Popoverel="span"direction="br"padding="s"onClickOutside={() => setOpen(false)}open={open}portalfooter={<Stackdirection="row-reverse"spacing="1"className="w-100"><Buttonsize="small"primaryonClick={onSaveHandler}disabled={value === states.category}>Apply Filter</Button><Button size="small" onClick={onCancelHandler}>Cancel</Button></Stack>}trigger={<FilterButtononClick={() => setOpen(!open)}value={states.category.length > 0 && states.category.length}label="Category"expandIcon={open}/>}><OptionListmultipleoptions={categoryOptions}onChange={onChangeHandler}value={value}/></Popover>);};const AllFilter = () => {const countFilter = () => {if (states.sort !== 0 && states.category.length !== 0) return 2;if (states.sort !== 0 || states.category.length !== 0) return 1;return undefined;};const [IStates, setIStates] = React.useState(states);const [open, setOpen] = React.useState(false);const [counter, setCounter] = React.useState(countFilter);React.useEffect(() => setCounter(countFilter), [IStates]);const close = () => {setIStates(states);setOpen(false);};return (<><FilterButtonlabel="all filters"icon="funnel"value={counter}onClick={() => setOpen(true)}/><Modalopen={open}title="All Filters"footer={<Stackdirection="row-reverse"justifyContent="space-between"className="w-100"><Stack direction="row-reverse" spacing={2}><Buttonsmallcolor="primary"onClick={() => {setStates(IStates);close();}}>Apply</Button><Button small fill="subtle" onClick={close}>Cancel</Button></Stack><LinkprimaryclassName="fs-2"onClick={() =>setIStates({ sort: 0, category: [] })}>Clear</Link></Stack>}><Form.AnvilSelectvalue={IStates.sort}options={sortOptions}onChange={(e) =>setIStates((prevState) => ({...prevState,sort: e.value,}))}trigger={{ clearable: false }}label="Sort by"/><Form.AnvilSelectmultiple={{ selectAll: true }}footer={{actionName: 'Done',onActionClick: () => null,}}value={IStates.category}options={categoryOptions}onChange={(e) => {setIStates((prevState) => ({...prevState,category: e.map((a) => a.value),}));}}label="Category"trigger={{ placeholder: 'Select categories to filter' }}/></Modal></>);};return (<Stack spacing={1}><AllFilter /><SortFilter /><CategoryFilter /></Stack>);};render (AllFilterExample)
Refrain from using icon only variant but when you do need one, make sure the icon is obvious for everyone.
We have three options to handle a set of Filter Buttons.
When using with table, use table's column sort instead of adding filter button for sorting.
Labels of filter categories and values should be easy to understand. When a filter name is ambiguous on its own, add a descriptive word related to the status.
For example, Low doesn’t make sense out of context. Add the word “risk” so that know it’s related to risk.
“Apply Filter” is descriptive of the action being taken and will make the button name consistent across the platform.
Under-the-hood, this is a button and supports all button keyboard interactions.
label
is always required for this component because it is using it as aria-label internally for better accessibility. This can be overwritten if you pass your own aria-label
prop.
import { FilterButton } from '@servicetitan/design-system';