From dc46f91e7b595e603c8734a39597963e631d2c4a Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Thu, 16 Apr 2026 11:52:57 +0200 Subject: [PATCH] Add drag selection to filter dropdowns --- src/components/ColumnFilterPopover.tsx | 91 ++++++++++++++++++++------ 1 file changed, 72 insertions(+), 19 deletions(-) diff --git a/src/components/ColumnFilterPopover.tsx b/src/components/ColumnFilterPopover.tsx index 77b4cee..08a4e3b 100644 --- a/src/components/ColumnFilterPopover.tsx +++ b/src/components/ColumnFilterPopover.tsx @@ -24,6 +24,10 @@ export function ColumnFilterPopover({ className }: ColumnFilterPopoverProps) { const [search, setSearch] = useState(''); + const [isDragging, setIsDragging] = useState(false); + const [dragStart, setDragStart] = useState(null); + const [hoveredIndex, setHoveredIndex] = useState(null); + const listRef = React.useRef(null); const filteredValues = useMemo(() => { return uniqueValues.filter(v => @@ -33,6 +37,43 @@ export function ColumnFilterPopover({ const isAllSelected = selectedValues.length === uniqueValues.length && uniqueValues.length > 0; + const handleMouseDown = (index: number) => { + setIsDragging(true); + setDragStart(index); + }; + + const handleMouseEnter = (index: number) => { + if (isDragging && dragStart !== null) { + setHoveredIndex(index); + } + }; + + const handleMouseUp = () => { + if (isDragging && dragStart !== null && hoveredIndex !== null) { + const start = Math.min(dragStart, hoveredIndex); + const end = Math.max(dragStart, hoveredIndex); + const itemsToSelect = filteredValues.slice(start, end + 1).map(v => v); + const newSelected = new Set([...selectedValues]); + itemsToSelect.forEach(v => newSelected.add(v)); + onSelectAll(Array.from(newSelected)); + } + setIsDragging(false); + setDragStart(null); + setHoveredIndex(null); + }; + + React.useEffect(() => { + const handleGlobalMouseUp = () => { + if (isDragging) { + handleMouseUp(); + } + }; + if (isDragging) { + document.addEventListener('mouseup', handleGlobalMouseUp); + return () => document.removeEventListener('mouseup', handleGlobalMouseUp); + } + }, [isDragging, dragStart, hoveredIndex]); + return (
-
- {filteredValues.map(val => ( -
onToggle(val)} - onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onToggle(val); } }} - className="flex items-center gap-2 p-1.5 hover:bg-slate-700/50 rounded cursor-pointer group" - > -
- {selectedValues.includes(val) && } +
+ {filteredValues.map((val, idx) => { + const isDragSelected = isDragging && dragStart !== null && hoveredIndex !== null && + ((idx >= dragStart && idx <= hoveredIndex) || (idx <= dragStart && idx >= hoveredIndex)); + return ( +
{ e.preventDefault(); handleMouseDown(idx); }} + onMouseEnter={() => handleMouseEnter(idx)} + onClick={(e) => { if (!isDragging) onToggle(val); }} + onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onToggle(val); } }} + className={cn( + "flex items-center gap-2 p-1.5 rounded cursor-pointer group transition-colors", + isDragSelected ? "bg-blue-600/40" : "hover:bg-slate-700/50" + )} + > +
+ {selectedValues.includes(val) && } +
+ {val || '(Empty)'}
- {val || '(Empty)'} -
- ))} + ); + })} {filteredValues.length === 0 && (
No values found
)}