- "content": "\"use client\";\r\n\r\nimport type { Column } from \"@tanstack/react-table\";\r\nimport * as React from \"react\";\r\n\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Slider } from \"@/components/ui/slider\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { PlusCircle, XCircle } from \"lucide-react\";\r\n\r\ntype RangeValue = [number, number];\r\n\r\nfunction getIsValidRange(value: unknown): value is RangeValue {\r\n return (\r\n Array.isArray(value) &&\r\n value.length === 2 &&\r\n typeof value[0] === \"number\" &&\r\n typeof value[1] === \"number\"\r\n );\r\n}\r\n\r\ninterface DataTableSliderFilterProps<TData> {\r\n column: Column<TData, unknown>;\r\n title?: string;\r\n}\r\n\r\nexport function DataTableSliderFilter<TData>({\r\n column,\r\n title,\r\n}: DataTableSliderFilterProps<TData>) {\r\n const id = React.useId();\r\n\r\n const columnFilterValue = getIsValidRange(column.getFilterValue())\r\n ? (column.getFilterValue() as RangeValue)\r\n : undefined;\r\n\r\n const defaultRange = column.columnDef.meta?.range;\r\n const unit = column.columnDef.meta?.unit;\r\n\r\n const [min, max] = React.useMemo((): RangeValue => {\r\n if (defaultRange && getIsValidRange(defaultRange)) return defaultRange;\r\n\r\n const values = column.getFacetedMinMaxValues();\r\n if (values && Array.isArray(values) && values.length === 2) {\r\n const [minVal, maxVal] = values;\r\n if (typeof minVal === \"number\" && typeof maxVal === \"number\") {\r\n return [minVal, maxVal];\r\n }\r\n }\r\n\r\n return [0, 100];\r\n }, [column, defaultRange]);\r\n\r\n const step = React.useMemo(() => {\r\n const rangeSize = max - min;\r\n if (rangeSize <= 20) return 1;\r\n if (rangeSize <= 100) return Math.ceil(rangeSize / 20);\r\n return Math.ceil(rangeSize / 50);\r\n }, [min, max]);\r\n\r\n const range = React.useMemo(() => {\r\n return columnFilterValue ?? [min, max];\r\n }, [columnFilterValue, min, max]);\r\n\r\n const onReset = React.useCallback(\r\n (event?: React.MouseEvent) => {\r\n event?.stopPropagation();\r\n column.setFilterValue(undefined);\r\n },\r\n [column],\r\n );\r\n\r\n const formatValue = React.useCallback((value: number) => {\r\n return value.toLocaleString(undefined, {\r\n maximumFractionDigits: 0,\r\n });\r\n }, []);\r\n\r\n const onFromInputChange = React.useCallback(\r\n (event: React.ChangeEvent<HTMLInputElement>) => {\r\n const numValue = Number(event.target.value);\r\n if (!Number.isNaN(numValue) && numValue >= min && numValue <= max) {\r\n const newRange: RangeValue = [numValue, max];\r\n column.setFilterValue(newRange);\r\n }\r\n },\r\n [column, min, max],\r\n );\r\n\r\n const onToInputChange = React.useCallback(\r\n (event: React.ChangeEvent<HTMLInputElement>) => {\r\n const numValue = Number(event.target.value);\r\n if (!Number.isNaN(numValue) && numValue <= max && numValue >= min) {\r\n const newRange: RangeValue = [min, numValue];\r\n column.setFilterValue(newRange);\r\n }\r\n },\r\n [column, max, min],\r\n );\r\n\r\n const onSliderValueChange = React.useCallback(\r\n (value: RangeValue) => {\r\n if (Array.isArray(value) && value.length === 2) {\r\n column.setFilterValue(value);\r\n }\r\n },\r\n [column],\r\n );\r\n\r\n return (\r\n <Popover>\r\n <PopoverTrigger asChild>\r\n <Button variant=\"outline\" size=\"sm\" className=\"border-dashed\">\r\n {columnFilterValue ? (\r\n <div\r\n role=\"button\"\r\n aria-label={`Clear ${title} filter`}\r\n tabIndex={0}\r\n className=\"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\r\n onClick={onReset}\r\n >\r\n <XCircle />\r\n </div>\r\n ) : (\r\n <PlusCircle />\r\n )}\r\n <span>{title}</span>\r\n {columnFilterValue ? (\r\n <>\r\n <Separator\r\n orientation=\"vertical\"\r\n className=\"mx-0.5 data-[orientation=vertical]:h-4\"\r\n />\r\n {formatValue(columnFilterValue[0])} -{\" \"}\r\n {formatValue(columnFilterValue[1])}\r\n {unit ? ` ${unit}` : \"\"}\r\n </>\r\n ) : null}\r\n </Button>\r\n </PopoverTrigger>\r\n <PopoverContent align=\"start\" className=\"flex w-auto flex-col gap-4\">\r\n <div className=\"flex flex-col gap-3\">\r\n <p className=\"font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\">\r\n {title}\r\n </p>\r\n <div className=\"flex items-center gap-4\">\r\n <Label htmlFor={`${id}-from`} className=\"sr-only\">\r\n From\r\n </Label>\r\n <div className=\"relative\">\r\n <Input\r\n id={`${id}-from`}\r\n type=\"number\"\r\n aria-valuemin={min}\r\n aria-valuemax={max}\r\n inputMode=\"numeric\"\r\n pattern=\"[0-9]*\"\r\n placeholder={min.toString()}\r\n min={min}\r\n max={max}\r\n value={range[0]?.toString()}\r\n onChange={onFromInputChange}\r\n className={cn(\"h-8 w-24\", unit && \"pr-8\")}\r\n />\r\n {unit && (\r\n <span className=\"absolute top-0 right-0 bottom-0 flex items-center rounded-r-md bg-accent px-2 text-muted-foreground text-sm\">\r\n {unit}\r\n </span>\r\n )}\r\n </div>\r\n <Label htmlFor={`${id}-to`} className=\"sr-only\">\r\n to\r\n </Label>\r\n <div className=\"relative\">\r\n <Input\r\n id={`${id}-to`}\r\n type=\"number\"\r\n aria-valuemin={min}\r\n aria-valuemax={max}\r\n inputMode=\"numeric\"\r\n pattern=\"[0-9]*\"\r\n placeholder={max.toString()}\r\n min={min}\r\n max={max}\r\n value={range[1]?.toString()}\r\n onChange={onToInputChange}\r\n className={cn(\"h-8 w-24\", unit && \"pr-8\")}\r\n />\r\n {unit && (\r\n <span className=\"absolute top-0 right-0 bottom-0 flex items-center rounded-r-md bg-accent px-2 text-muted-foreground text-sm\">\r\n {unit}\r\n </span>\r\n )}\r\n </div>\r\n </div>\r\n <Label htmlFor={`${id}-slider`} className=\"sr-only\">\r\n {title} slider\r\n </Label>\r\n <Slider\r\n id={`${id}-slider`}\r\n min={min}\r\n max={max}\r\n step={step}\r\n value={range}\r\n onValueChange={onSliderValueChange}\r\n />\r\n </div>\r\n <Button\r\n aria-label={`Clear ${title} filter`}\r\n variant=\"outline\"\r\n size=\"sm\"\r\n onClick={onReset}\r\n >\r\n Clear\r\n </Button>\r\n </PopoverContent>\r\n </Popover>\r\n );\r\n}\r\n",
0 commit comments