Fix Esc handling in filter popover

This commit is contained in:
2025-01-15 16:55:10 -05:00
parent 3c600659e5
commit e5f97ab836

View File

@@ -18,13 +18,10 @@ import {
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
type FilterValue = string | number | boolean; type FilterValue = string | number | boolean;
type ComparisonOperator = '=' | '>' | '>=' | '<' | '<=' | 'between'; type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "between";
interface FilterValueWithOperator { interface FilterValueWithOperator {
value: FilterValue | [number, number]; value: FilterValue | [number, number];
@@ -43,7 +40,7 @@ interface ActiveFilter {
interface FilterOption { interface FilterOption {
id: string; id: string;
label: string; label: string;
type: 'select' | 'number' | 'boolean' | 'text'; type: "select" | "number" | "boolean" | "text";
options?: { label: string; value: string }[]; options?: { label: string; value: string }[];
group: string; group: string;
operators?: ComparisonOperator[]; operators?: ComparisonOperator[];
@@ -51,163 +48,163 @@ interface FilterOption {
const FILTER_OPTIONS: FilterOption[] = [ const FILTER_OPTIONS: FilterOption[] = [
// Basic Info Group // Basic Info Group
{ id: 'search', label: 'Search', type: 'text', group: 'Basic Info' }, { id: "search", label: "Search", type: "text", group: "Basic Info" },
{ id: 'sku', label: 'SKU', type: 'text', group: 'Basic Info' }, { id: "sku", label: "SKU", type: "text", group: "Basic Info" },
{ id: 'vendor', label: 'Vendor', type: 'select', group: 'Basic Info' }, { id: "vendor", label: "Vendor", type: "select", group: "Basic Info" },
{ id: 'brand', label: 'Brand', type: 'select', group: 'Basic Info' }, { id: "brand", label: "Brand", type: "select", group: "Basic Info" },
{ id: 'category', label: 'Category', type: 'select', group: 'Basic Info' }, { id: "category", label: "Category", type: "select", group: "Basic Info" },
// Inventory Group // Inventory Group
{ {
id: 'stockStatus', id: "stockStatus",
label: 'Stock Status', label: "Stock Status",
type: 'select', type: "select",
options: [ options: [
{ label: 'Critical', value: 'critical' }, { label: "Critical", value: "critical" },
{ label: 'At Risk', value: 'at-risk' }, { label: "At Risk", value: "at-risk" },
{ label: 'Reorder', value: 'reorder' }, { label: "Reorder", value: "reorder" },
{ label: 'Healthy', value: 'healthy' }, { label: "Healthy", value: "healthy" },
{ label: 'Overstocked', value: 'overstocked' }, { label: "Overstocked", value: "overstocked" },
{ label: 'New', value: 'new' } { label: "New", value: "new" },
], ],
group: 'Inventory' group: "Inventory",
}, },
{ {
id: 'stock', id: "stock",
label: 'Stock Quantity', label: "Stock Quantity",
type: 'number', type: "number",
group: 'Inventory', group: "Inventory",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
{ {
id: 'daysOfStock', id: "daysOfStock",
label: 'Days of Stock', label: "Days of Stock",
type: 'number', type: "number",
group: 'Inventory', group: "Inventory",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
{ {
id: 'replenishable', id: "replenishable",
label: 'Replenishable', label: "Replenishable",
type: 'select', type: "select",
options: [ options: [
{ label: 'Yes', value: 'true' }, { label: "Yes", value: "true" },
{ label: 'No', value: 'false' } { label: "No", value: "false" },
], ],
group: 'Inventory' group: "Inventory",
}, },
// Pricing Group // Pricing Group
{ {
id: 'price', id: "price",
label: 'Price', label: "Price",
type: 'number', type: "number",
group: 'Pricing', group: "Pricing",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
{ {
id: 'costPrice', id: "costPrice",
label: 'Cost Price', label: "Cost Price",
type: 'number', type: "number",
group: 'Pricing', group: "Pricing",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
{ {
id: 'landingCost', id: "landingCost",
label: 'Landing Cost', label: "Landing Cost",
type: 'number', type: "number",
group: 'Pricing', group: "Pricing",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
// Sales Metrics Group // Sales Metrics Group
{ {
id: 'dailySalesAvg', id: "dailySalesAvg",
label: 'Daily Sales Avg', label: "Daily Sales Avg",
type: 'number', type: "number",
group: 'Sales Metrics', group: "Sales Metrics",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
{ {
id: 'weeklySalesAvg', id: "weeklySalesAvg",
label: 'Weekly Sales Avg', label: "Weekly Sales Avg",
type: 'number', type: "number",
group: 'Sales Metrics', group: "Sales Metrics",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
{ {
id: 'monthlySalesAvg', id: "monthlySalesAvg",
label: 'Monthly Sales Avg', label: "Monthly Sales Avg",
type: 'number', type: "number",
group: 'Sales Metrics', group: "Sales Metrics",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
// Financial Metrics Group // Financial Metrics Group
{ {
id: 'margin', id: "margin",
label: 'Margin %', label: "Margin %",
type: 'number', type: "number",
group: 'Financial Metrics', group: "Financial Metrics",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
{ {
id: 'gmroi', id: "gmroi",
label: 'GMROI', label: "GMROI",
type: 'number', type: "number",
group: 'Financial Metrics', group: "Financial Metrics",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
// Lead Time & Stock Coverage Group // Lead Time & Stock Coverage Group
{ {
id: 'leadTime', id: "leadTime",
label: 'Lead Time (Days)', label: "Lead Time (Days)",
type: 'number', type: "number",
group: 'Lead Time & Coverage', group: "Lead Time & Coverage",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
{ {
id: 'leadTimeStatus', id: "leadTimeStatus",
label: 'Lead Time Status', label: "Lead Time Status",
type: 'select', type: "select",
options: [ options: [
{ label: 'On Target', value: 'on_target' }, { label: "On Target", value: "on_target" },
{ label: 'Warning', value: 'warning' }, { label: "Warning", value: "warning" },
{ label: 'Critical', value: 'critical' } { label: "Critical", value: "critical" },
], ],
group: 'Lead Time & Coverage' group: "Lead Time & Coverage",
}, },
{ {
id: 'stockCoverage', id: "stockCoverage",
label: 'Stock Coverage Ratio', label: "Stock Coverage Ratio",
type: 'number', type: "number",
group: 'Lead Time & Coverage', group: "Lead Time & Coverage",
operators: ['=', '>', '>=', '<', '<=', 'between'] operators: ["=", ">", ">=", "<", "<=", "between"],
}, },
// Classification Group // Classification Group
{ {
id: 'abcClass', id: "abcClass",
label: 'ABC Class', label: "ABC Class",
type: 'select', type: "select",
options: [ options: [
{ label: 'A', value: 'A' }, { label: "A", value: "A" },
{ label: 'B', value: 'B' }, { label: "B", value: "B" },
{ label: 'C', value: 'C' } { label: "C", value: "C" },
], ],
group: 'Classification' group: "Classification",
}, },
{ {
id: 'managingStock', id: "managingStock",
label: 'Managing Stock', label: "Managing Stock",
type: 'select', type: "select",
options: [ options: [
{ label: 'Yes', value: 'true' }, { label: "Yes", value: "true" },
{ label: 'No', value: 'false' } { label: "No", value: "false" },
], ],
group: 'Classification' group: "Classification",
} },
]; ];
interface ProductFiltersProps { interface ProductFiltersProps {
@@ -229,16 +226,21 @@ export function ProductFilters({
}: ProductFiltersProps) { }: ProductFiltersProps) {
const [showCommand, setShowCommand] = React.useState(false); const [showCommand, setShowCommand] = React.useState(false);
const [selectedFilter, setSelectedFilter] = React.useState<FilterOption | null>(null); const [selectedFilter, setSelectedFilter] = React.useState<FilterOption | null>(null);
const [selectedOperator, setSelectedOperator] = React.useState<ComparisonOperator>('='); const [selectedOperator, setSelectedOperator] = React.useState<ComparisonOperator>("=");
const [inputValue, setInputValue] = React.useState(""); const [inputValue, setInputValue] = React.useState("");
const [inputValue2, setInputValue2] = React.useState(""); const [inputValue2, setInputValue2] = React.useState("");
const [searchValue, setSearchValue] = React.useState(""); const [searchValue, setSearchValue] = React.useState("");
// Add refs for the inputs
const numberInputRef = React.useRef<HTMLInputElement>(null);
const selectInputRef = React.useRef<HTMLInputElement>(null);
const textInputRef = React.useRef<HTMLInputElement>(null);
// Reset states when popup closes // Reset states when popup closes
const handlePopoverClose = () => { const handlePopoverClose = () => {
setShowCommand(false); setShowCommand(false);
setSelectedFilter(null); setSelectedFilter(null);
setSelectedOperator('='); setSelectedOperator("=");
setInputValue(""); setInputValue("");
setInputValue2(""); setInputValue2("");
setSearchValue(""); setSearchValue("");
@@ -248,7 +250,7 @@ export function ProductFilters({
React.useEffect(() => { React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
// Command/Ctrl + K to toggle filter // Command/Ctrl + K to toggle filter
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault(); e.preventDefault();
if (!showCommand) { if (!showCommand) {
setShowCommand(true); setShowCommand(true);
@@ -256,35 +258,31 @@ export function ProductFilters({
handlePopoverClose(); handlePopoverClose();
} }
} }
// Only handle Escape at the root level
if (e.key === 'Escape' && !selectedFilter) {
handlePopoverClose();
}
}; };
window.addEventListener('keydown', handleKeyDown); window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown);
}, [selectedFilter, showCommand]); }, [showCommand]);
// Update filter options with dynamic data // Update filter options with dynamic data
const filterOptions = React.useMemo(() => { const filterOptions = React.useMemo(() => {
return FILTER_OPTIONS.map(option => { return FILTER_OPTIONS.map((option) => {
if (option.id === 'category') { if (option.id === "category") {
return { return {
...option, ...option,
options: categories.map(cat => ({ label: cat, value: cat })) options: categories.map((cat) => ({ label: cat, value: cat })),
}; };
} }
if (option.id === 'vendor') { if (option.id === "vendor") {
return { return {
...option, ...option,
options: vendors.map(vendor => ({ label: vendor, value: vendor })) options: vendors.map((vendor) => ({ label: vendor, value: vendor })),
}; };
} }
if (option.id === 'brand') { if (option.id === "brand") {
return { return {
...option, ...option,
options: brands.map(brand => ({ label: brand, value: brand })) options: brands.map((brand) => ({ label: brand, value: brand })),
}; };
} }
return option; return option;
@@ -296,7 +294,8 @@ export function ProductFilters({
if (!searchValue) return filterOptions; if (!searchValue) return filterOptions;
const search = searchValue.toLowerCase(); const search = searchValue.toLowerCase();
return filterOptions.filter(option => return filterOptions.filter(
(option) =>
option.label.toLowerCase().includes(search) || option.label.toLowerCase().includes(search) ||
option.group.toLowerCase().includes(search) option.group.toLowerCase().includes(search)
); );
@@ -305,6 +304,17 @@ export function ProductFilters({
const handleSelectFilter = React.useCallback((filter: FilterOption) => { const handleSelectFilter = React.useCallback((filter: FilterOption) => {
setSelectedFilter(filter); setSelectedFilter(filter);
setInputValue(""); setInputValue("");
// Focus the appropriate input after state updates
requestAnimationFrame(() => {
if (filter.type === "number") {
numberInputRef.current?.focus();
} else if (filter.type === "select") {
selectInputRef.current?.focus();
} else {
textInputRef.current?.focus();
}
});
}, []); }, []);
const handleApplyFilter = (value: FilterValue | [number, number]) => { const handleApplyFilter = (value: FilterValue | [number, number]) => {
@@ -314,8 +324,8 @@ export function ProductFilters({
...activeFilters, ...activeFilters,
[selectedFilter.id]: { [selectedFilter.id]: {
value, value,
operator: selectedOperator operator: selectedOperator,
} },
}; };
onFilterChange(newFilters as Record<string, ActiveFilterValue>); onFilterChange(newFilters as Record<string, ActiveFilterValue>);
@@ -324,7 +334,7 @@ export function ProductFilters({
const handleBackToFilters = () => { const handleBackToFilters = () => {
setSelectedFilter(null); setSelectedFilter(null);
setSelectedOperator('='); setSelectedOperator("=");
setInputValue(""); setInputValue("");
setInputValue2(""); setInputValue2("");
}; };
@@ -333,11 +343,13 @@ export function ProductFilters({
if (!activeFilters) return []; if (!activeFilters) return [];
return Object.entries(activeFilters).map(([id, value]): ActiveFilter => { return Object.entries(activeFilters).map(([id, value]): ActiveFilter => {
const option = filterOptions.find(opt => opt.id === id); const option = filterOptions.find((opt) => opt.id === id);
let displayValue = String(value); let displayValue = String(value);
if (option?.type === 'select' && option.options) { if (option?.type === "select" && option.options) {
const optionLabel = option.options.find(opt => opt.value === value)?.label; const optionLabel = option.options.find(
(opt) => opt.value === value
)?.label;
if (optionLabel) displayValue = optionLabel; if (optionLabel) displayValue = optionLabel;
} }
@@ -345,7 +357,7 @@ export function ProductFilters({
id, id,
label: option?.label || id, label: option?.label || id,
value, value,
displayValue displayValue,
}; };
}); });
}, [activeFilters, filterOptions]); }, [activeFilters, filterOptions]);
@@ -354,15 +366,29 @@ export function ProductFilters({
<ToggleGroup <ToggleGroup
type="single" type="single"
value={selectedOperator} value={selectedOperator}
onValueChange={(value: ComparisonOperator) => value && setSelectedOperator(value)} onValueChange={(value: ComparisonOperator) =>
value && setSelectedOperator(value)
}
className="flex-wrap" className="flex-wrap"
> >
<ToggleGroupItem value="=" aria-label="equals">=</ToggleGroupItem> <ToggleGroupItem value="=" aria-label="equals">
<ToggleGroupItem value=">" aria-label="greater than">{'>'}</ToggleGroupItem> =
<ToggleGroupItem value=">=" aria-label="greater than or equal"></ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem value="<" aria-label="less than">{'<'}</ToggleGroupItem> <ToggleGroupItem value=">" aria-label="greater than">
<ToggleGroupItem value="<=" aria-label="less than or equal"></ToggleGroupItem> {">"}
<ToggleGroupItem value="between" aria-label="between">Between</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem value=">=" aria-label="greater than or equal">
</ToggleGroupItem>
<ToggleGroupItem value="<" aria-label="less than">
{"<"}
</ToggleGroupItem>
<ToggleGroupItem value="<=" aria-label="less than or equal">
</ToggleGroupItem>
<ToggleGroupItem value="between" aria-label="between">
Between
</ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
); );
@@ -380,13 +406,14 @@ export function ProductFilters({
{renderOperatorSelect()} {renderOperatorSelect()}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <Input
ref={numberInputRef}
type="number" type="number"
placeholder={`Enter ${selectedFilter?.label.toLowerCase()}`} placeholder={`Enter ${selectedFilter?.label.toLowerCase()}`}
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === "Enter") {
if (selectedOperator === 'between') { if (selectedOperator === "between") {
if (inputValue2) { if (inputValue2) {
const val1 = parseFloat(inputValue); const val1 = parseFloat(inputValue);
const val2 = parseFloat(inputValue2); const val2 = parseFloat(inputValue2);
@@ -400,14 +427,14 @@ export function ProductFilters({
handleApplyFilter(val); handleApplyFilter(val);
} }
} }
} else if (e.key === 'Escape') { } else if (e.key === "Escape") {
e.stopPropagation(); e.stopPropagation();
handleBackToFilters(); handleBackToFilters();
} }
}} }}
className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/> />
{selectedOperator === 'between' && ( {selectedOperator === "between" && (
<> <>
<span>and</span> <span>and</span>
<Input <Input
@@ -416,13 +443,13 @@ export function ProductFilters({
value={inputValue2} value={inputValue2}
onChange={(e) => setInputValue2(e.target.value)} onChange={(e) => setInputValue2(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === "Enter") {
const val1 = parseFloat(inputValue); const val1 = parseFloat(inputValue);
const val2 = parseFloat(inputValue2); const val2 = parseFloat(inputValue2);
if (!isNaN(val1) && !isNaN(val2)) { if (!isNaN(val1) && !isNaN(val2)) {
handleApplyFilter([val1, val2]); handleApplyFilter([val1, val2]);
} }
} else if (e.key === 'Escape') { } else if (e.key === "Escape") {
e.stopPropagation(); e.stopPropagation();
handleBackToFilters(); handleBackToFilters();
} }
@@ -433,7 +460,7 @@ export function ProductFilters({
)} )}
<Button <Button
onClick={() => { onClick={() => {
if (selectedOperator === 'between') { if (selectedOperator === "between") {
const val1 = parseFloat(inputValue); const val1 = parseFloat(inputValue);
const val2 = parseFloat(inputValue2); const val2 = parseFloat(inputValue2);
if (!isNaN(val1) && !isNaN(val2)) { if (!isNaN(val1) && !isNaN(val2)) {
@@ -455,7 +482,7 @@ export function ProductFilters({
const getFilterDisplayValue = (filter: ActiveFilter) => { const getFilterDisplayValue = (filter: ActiveFilter) => {
const filterValue = activeFilters[filter.id]; const filterValue = activeFilters[filter.id];
const filterOption = filterOptions.find(opt => opt.id === filter.id); const filterOption = filterOptions.find((opt) => opt.id === filter.id);
// For between ranges // For between ranges
if (Array.isArray(filterValue)) { if (Array.isArray(filterValue)) {
@@ -463,8 +490,13 @@ export function ProductFilters({
} }
// For direct selections (select type) or text search // For direct selections (select type) or text search
if (filterOption?.type === 'select' || filterOption?.type === 'text' || typeof filterValue !== 'object') { if (
const value = typeof filterValue === 'object' ? filterValue.value : filterValue; filterOption?.type === "select" ||
filterOption?.type === "text" ||
typeof filterValue !== "object"
) {
const value =
typeof filterValue === "object" ? filterValue.value : filterValue;
return `${filter.label}: ${value}`; return `${filter.label}: ${value}`;
} }
@@ -472,12 +504,12 @@ export function ProductFilters({
const operator = filterValue.operator; const operator = filterValue.operator;
const value = filterValue.value; const value = filterValue.value;
const operatorDisplay = { const operatorDisplay = {
'=': '=', "=": "=",
'>': '>', ">": ">",
'>=': '≥', ">=": "≥",
'<': '<', "<": "<",
'<=': '≤', "<=": "≤",
'between': 'between' between: "between",
}[operator]; }[operator];
return `${filter.label} ${operatorDisplay} ${value}`; return `${filter.label} ${operatorDisplay} ${value}`;
@@ -498,37 +530,29 @@ export function ProductFilters({
modal={true} modal={true}
> >
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button variant="outline" size="sm" className="h-8 border-dashed">
variant="outline" <Plus
size="sm" className={cn(
className="h-8 border-dashed"
>
<Plus className={cn(
"mr-2 h-4 w-4 transition-transform duration-200", "mr-2 h-4 w-4 transition-transform duration-200",
showCommand && "rotate-[135deg]" showCommand && "rotate-[135deg]"
)} /> )}
/>
{showCommand ? "Cancel" : "Add Filter"} {showCommand ? "Cancel" : "Add Filter"}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
className="p-0 w-[520px]" className="p-0 w-[520px]"
align="start" align="start"
onKeyDown={(e) => { onEscapeKeyDown={(event) => {
if (e.key === 'Escape') { console.log('Escape pressed, selectedFilter:', selectedFilter); // Debug log
e.preventDefault();
e.stopPropagation();
if (selectedFilter) { if (selectedFilter) {
event.preventDefault();
event.stopPropagation();
handleBackToFilters(); handleBackToFilters();
} else {
handlePopoverClose();
}
} }
}} }}
> >
<Command <Command className="rounded-none border-0" shouldFilter={false}>
className="rounded-none border-0"
shouldFilter={false}
>
{!selectedFilter ? ( {!selectedFilter ? (
<> <>
<CommandInput <CommandInput
@@ -536,7 +560,7 @@ export function ProductFilters({
value={searchValue} value={searchValue}
onValueChange={setSearchValue} onValueChange={setSearchValue}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Escape') { if (e.key === "Escape") {
e.preventDefault(); e.preventDefault();
handlePopoverClose(); handlePopoverClose();
} }
@@ -545,11 +569,14 @@ export function ProductFilters({
<CommandList> <CommandList>
<CommandEmpty>No filters found.</CommandEmpty> <CommandEmpty>No filters found.</CommandEmpty>
{Object.entries( {Object.entries(
filteredOptions.reduce<Record<string, FilterOption[]>>((acc, filter) => { filteredOptions.reduce<Record<string, FilterOption[]>>(
(acc, filter) => {
if (!acc[filter.group]) acc[filter.group] = []; if (!acc[filter.group]) acc[filter.group] = [];
acc[filter.group].push(filter); acc[filter.group].push(filter);
return acc; return acc;
}, {}) },
{}
)
).map(([group, filters]) => ( ).map(([group, filters]) => (
<React.Fragment key={group}> <React.Fragment key={group}>
<CommandGroup heading={group}> <CommandGroup heading={group}>
@@ -559,7 +586,7 @@ export function ProductFilters({
value={`${filter.id} ${filter.label}`} value={`${filter.id} ${filter.label}`}
onSelect={() => { onSelect={() => {
handleSelectFilter(filter); handleSelectFilter(filter);
if (filter.type !== 'select') { if (filter.type !== "select") {
setInputValue(""); setInputValue("");
} }
}} }}
@@ -582,7 +609,7 @@ export function ProductFilters({
))} ))}
</CommandList> </CommandList>
</> </>
) : selectedFilter.type === 'number' ? ( ) : selectedFilter.type === "number" ? (
<div className="p-4"> <div className="p-4">
<div className="flex flex-col gap-4 items-start"> <div className="flex flex-col gap-4 items-start">
<div className="mb-4"> <div className="mb-4">
@@ -597,13 +624,14 @@ export function ProductFilters({
{renderOperatorSelect()} {renderOperatorSelect()}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <Input
ref={numberInputRef}
type="number" type="number"
placeholder={`Enter ${selectedFilter?.label.toLowerCase()}`} placeholder={`Enter ${selectedFilter?.label.toLowerCase()}`}
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === "Enter") {
if (selectedOperator === 'between') { if (selectedOperator === "between") {
if (inputValue2) { if (inputValue2) {
const val1 = parseFloat(inputValue); const val1 = parseFloat(inputValue);
const val2 = parseFloat(inputValue2); const val2 = parseFloat(inputValue2);
@@ -617,14 +645,14 @@ export function ProductFilters({
handleApplyFilter(val); handleApplyFilter(val);
} }
} }
} else if (e.key === 'Escape') { } else if (e.key === "Escape") {
e.preventDefault(); e.preventDefault();
handleBackToFilters(); handleBackToFilters();
} }
}} }}
className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/> />
{selectedOperator === 'between' && ( {selectedOperator === "between" && (
<> <>
<span>and</span> <span>and</span>
<Input <Input
@@ -633,13 +661,13 @@ export function ProductFilters({
value={inputValue2} value={inputValue2}
onChange={(e) => setInputValue2(e.target.value)} onChange={(e) => setInputValue2(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === "Enter") {
const val1 = parseFloat(inputValue); const val1 = parseFloat(inputValue);
const val2 = parseFloat(inputValue2); const val2 = parseFloat(inputValue2);
if (!isNaN(val1) && !isNaN(val2)) { if (!isNaN(val1) && !isNaN(val2)) {
handleApplyFilter([val1, val2]); handleApplyFilter([val1, val2]);
} }
} else if (e.key === 'Escape') { } else if (e.key === "Escape") {
e.preventDefault(); e.preventDefault();
handleBackToFilters(); handleBackToFilters();
} }
@@ -650,7 +678,7 @@ export function ProductFilters({
)} )}
<Button <Button
onClick={() => { onClick={() => {
if (selectedOperator === 'between') { if (selectedOperator === "between") {
const val1 = parseFloat(inputValue); const val1 = parseFloat(inputValue);
const val2 = parseFloat(inputValue2); const val2 = parseFloat(inputValue2);
if (!isNaN(val1) && !isNaN(val2)) { if (!isNaN(val1) && !isNaN(val2)) {
@@ -669,17 +697,18 @@ export function ProductFilters({
</div> </div>
</div> </div>
</div> </div>
) : selectedFilter.type === 'select' ? ( ) : selectedFilter.type === "select" ? (
<> <>
<CommandInput <CommandInput
ref={selectInputRef}
placeholder={`Select ${selectedFilter.label.toLowerCase()}...`} placeholder={`Select ${selectedFilter.label.toLowerCase()}...`}
value={inputValue} value={inputValue}
onValueChange={setInputValue} onValueChange={setInputValue}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Backspace' && !inputValue) { if (e.key === "Backspace" && !inputValue) {
e.preventDefault(); e.preventDefault();
handleBackToFilters(); handleBackToFilters();
} else if (e.key === 'Escape') { } else if (e.key === "Escape") {
e.preventDefault(); e.preventDefault();
handleBackToFilters(); handleBackToFilters();
} }
@@ -696,8 +725,10 @@ export function ProductFilters({
</CommandItem> </CommandItem>
<CommandSeparator /> <CommandSeparator />
{selectedFilter.options {selectedFilter.options
?.filter(option => ?.filter((option) =>
option.label.toLowerCase().includes(inputValue.toLowerCase()) option.label
.toLowerCase()
.includes(inputValue.toLowerCase())
) )
.map((option) => ( .map((option) => (
<CommandItem <CommandItem
@@ -708,21 +739,21 @@ export function ProductFilters({
> >
{option.label} {option.label}
</CommandItem> </CommandItem>
)) ))}
}
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</> </>
) : ( ) : (
<> <>
<CommandInput <CommandInput
ref={textInputRef}
placeholder={`Enter ${selectedFilter.label.toLowerCase()}...`} placeholder={`Enter ${selectedFilter.label.toLowerCase()}...`}
value={inputValue} value={inputValue}
onValueChange={setInputValue} onValueChange={setInputValue}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && inputValue.trim()) { if (e.key === "Enter" && inputValue.trim()) {
handleApplyFilter(inputValue.trim()); handleApplyFilter(inputValue.trim());
} else if (e.key === 'Escape') { } else if (e.key === "Escape") {
e.preventDefault(); e.preventDefault();
handleBackToFilters(); handleBackToFilters();
} }