Custom range filter
Basic usage
tsx
import { Flex } from '@semcore/ui/base-components';
import { FilterTrigger } from '@semcore/ui/base-trigger';
import Button from '@semcore/ui/button';
import Dropdown from '@semcore/ui/dropdown';
import InputNumber from '@semcore/ui/input-number';
import { Text } from '@semcore/ui/typography';
import React, { useState, useRef } from 'react';
interface ValueState {
from: string;
to: string;
}
interface InputRangeProps {
value: ValueState;
changeValue: (updatedValue: ValueState) => void;
[key: string]: unknown;
}
const InputRange: React.FC<InputRangeProps> = ({ value: valueState, changeValue, ...other }) => {
const minRange = 1;
const maxRange = 8;
const fromRef = useRef<HTMLInputElement | null>(null);
const toRef = useRef<HTMLInputElement | null>(null);
const handleChange = (key: keyof ValueState) => (value: string | null) => {
valueState[key] = value ?? '';
changeValue({ ...valueState });
};
const handleBlur = () => {
setTimeout(() => {
if (document.activeElement !== fromRef.current && document.activeElement !== toRef.current) {
const { from, to } = valueState;
if (from > to && to !== '') {
changeValue({
from: Math.max(Number(to), minRange).toString(),
to: Math.min(Number(from), maxRange).toString(),
});
}
if ((to === '' || from === '') && (to !== '' || from !== '')) {
changeValue({
from: from !== '' ? from : minRange.toString(),
to: to !== '' ? to : maxRange.toString(),
});
}
}
}, 0);
};
const { from, to } = valueState;
return (
<Flex {...other}>
<InputNumber neighborLocation='right'>
<InputNumber.Value
min={minRange}
max={maxRange}
aria-label='From'
placeholder='From'
value={from}
onChange={handleChange('from')}
onBlur={handleBlur}
ref={fromRef}
autoFocus
/>
<InputNumber.Controls />
</InputNumber>
<InputNumber neighborLocation='left'>
<InputNumber.Value
min={minRange}
max={maxRange}
aria-label='To'
placeholder='To'
value={to}
onChange={handleChange('to')}
onBlur={handleBlur}
ref={toRef}
/>
<InputNumber.Controls />
</InputNumber>
</Flex>
);
};
const setTriggerText = ({ from, to }: { from: string; to: string }): string | null => {
if (from !== '' && to !== '') {
return from === to ? `${from}` : `${from}–${to}`;
}
return null;
};
const Demo = () => {
const [value, setValue] = useState<ValueState>({ from: '', to: '' });
const [filters, setFilters] = useState(false);
const [visible, setVisible] = useState(false);
const [displayValue, setDisplayValue] = useState('');
const clearAll = () => {
setFilters(false);
setValue({ from: '', to: '' });
setVisible(false);
};
const applyFilters = () => {
const { from, to } = value;
setVisible(false);
setFilters(!!(from || to));
setDisplayValue(setTriggerText(value) ?? '');
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter') {
applyFilters();
}
};
return (
<Dropdown visible={visible} onVisibleChange={setVisible}>
<Dropdown.Trigger
placeholder='Competitive Density'
aria-label='Competitive Density'
empty={!filters}
onClear={clearAll}
tag={FilterTrigger}
>
<span aria-hidden>Com.:</span>
{' '}
{displayValue}
</Dropdown.Trigger>
<Dropdown.Popper w={240} p={2} pb={3} aria-label='Competitive Density' tabIndex={-1}>
<Text id='title-CD' size={200} bold>
Custom range
</Text>
<InputRange
role='group'
aria-labelledby='title-CD'
value={value}
changeValue={setValue}
my={2}
onKeyDown={handleKeyDown}
/>
<Button use='primary' theme='info' w='100%' onClick={applyFilters}>
Apply
</Button>
</Dropdown.Popper>
</Dropdown>
);
};
export default Demo;
Presets
tsx
import { Flex } from '@semcore/ui/base-components';
import { FilterTrigger } from '@semcore/ui/base-trigger';
import Button from '@semcore/ui/button';
import Divider from '@semcore/ui/divider';
import InputNumber from '@semcore/ui/input-number';
import Select from '@semcore/ui/select';
import { Text } from '@semcore/ui/typography';
import React, { useState, useRef } from 'react';
interface ValueState {
from: string;
to: string;
}
interface InputRangeProps {
value: ValueState;
changeValue: (updatedValue: ValueState) => void;
[key: string]: unknown;
}
const minRange = 1;
const InputRange: React.FC<InputRangeProps> = ({ value: valueState, changeValue, ...other }) => {
const fromRef = useRef<HTMLInputElement | null>(null);
const toRef = useRef<HTMLInputElement | null>(null);
const handleChange = (key: keyof ValueState) => (value: string | number | undefined) => {
changeValue({
...valueState,
[key]: value !== undefined ? String(value) : '',
});
};
const handleBlur = () => {
setTimeout(() => {
if (document.activeElement !== fromRef.current && document.activeElement !== toRef.current) {
const from = Number(valueState.from);
const to = Number(valueState.to);
if (from > to && valueState.to !== '') {
changeValue({
from: to > minRange ? to.toString() : minRange.toString(),
to: from.toString(),
});
}
if (valueState.from === '' && valueState.to !== '') {
changeValue({
from: minRange.toString(),
to: valueState.to,
});
}
}
}, 0);
};
const { from, to } = valueState;
return (
<Flex {...other}>
<InputNumber neighborLocation='right'>
<InputNumber.Value
min={minRange}
aria-label='From'
placeholder='From'
value={from}
onChange={handleChange('from')}
onBlur={handleBlur}
ref={fromRef}
/>
<InputNumber.Controls />
</InputNumber>
<InputNumber neighborLocation='left'>
<InputNumber.Value
min={minRange}
aria-label='To'
placeholder='To'
value={to}
onChange={handleChange('to')}
onBlur={handleBlur}
ref={toRef}
/>
<InputNumber.Controls />
</InputNumber>
</Flex>
);
};
const numberFormat = new Intl.NumberFormat('en-US');
const setTriggerText = ({ from, to }: ValueState): string | undefined => {
if (from !== '') {
if (to === '') return `${numberFormat.format(Number(from))}+`;
if (from === to) return numberFormat.format(Number(from));
return `${numberFormat.format(Number(from))}–${numberFormat.format(Number(to))}`;
} else if (to !== '') {
return `${numberFormat.format(minRange)}–${numberFormat.format(Number(to))}`;
}
return undefined;
};
const Demo = () => {
const [visible, setVisible] = useState(false);
const [customRange, setCustomRange] = useState<ValueState>({ from: '', to: '' });
const [selectValue, setSelectValue] = useState<string | undefined>(undefined);
const [displayValue, setDisplayValue] = useState<string | undefined>(undefined);
const triggerRef = useRef<HTMLButtonElement | null>(null);
const clearAll = () => {
setCustomRange({ from: '', to: '' });
setSelectValue(undefined);
setDisplayValue(undefined);
};
const applyFilters = () => {
setVisible(false);
setDisplayValue(setTriggerText(customRange));
setSelectValue(undefined);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
e.stopPropagation();
if (
e.key === 'Tab' &&
e.shiftKey &&
e.target instanceof HTMLElement &&
e.target.getAttribute('aria-label') === 'From'
) {
e.preventDefault();
triggerRef.current?.focus();
}
if (e.key === 'Enter') {
e.preventDefault();
applyFilters();
}
if (e.key === 'Escape') {
e.preventDefault();
setVisible(false);
}
};
const handleKeyDownApply = (e: React.KeyboardEvent) => {
if (e.key === 'Tab' && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
triggerRef.current?.focus();
}
};
const handleSelect = (value: string) => {
setDisplayValue(value);
setSelectValue(value);
setCustomRange({ from: '', to: '' });
};
return (
<Select
visible={visible}
onVisibleChange={setVisible}
onChange={handleSelect}
value={selectValue}
>
<Select.Trigger
placeholder='Volume'
aria-label='Volume'
active={visible}
empty={!displayValue}
onClear={clearAll}
tag={FilterTrigger}
triggerRef={triggerRef}
>
<span aria-hidden>Volume: </span>
{displayValue}
</Select.Trigger>
<Select.Popper w={224} aria-label='Volume'>
<Select.List aria-label='Presets'>
{['100,001+', '10,001–100,000', '1,001–10,000', '101–1,000', '11–100', '1–10'].map(
(item) => (
<Select.Option key={item} value={item}>
{item}
</Select.Option>
),
)}
</Select.List>
<Divider my={1} />
<Flex px={2} pt={1} pb={3} gap={2} direction='column'>
<Text id='custom-range-title' size={200} bold>
Custom range
</Text>
<InputRange
role='group'
aria-labelledby='custom-range-title'
value={customRange}
changeValue={setCustomRange}
onKeyDown={handleKeyDown}
/>
<Button
use='primary'
theme='info'
w='100%'
onClick={applyFilters}
onKeyDown={handleKeyDownApply}
>
Apply
</Button>
</Flex>
</Select.Popper>
</Select>
);
};
export default Demo;