From e96d245f59f8c8ba8e7f4094dbf64e37c564a046 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Fri, 24 Apr 2026 17:08:23 +0200 Subject: [PATCH] fix: ensure filter dropdowns are not covered by pinned columns --- src/components/ColumnFilterPopover.tsx | 432 +++++++++++++------------ src/components/PricingView.tsx | 95 ++++-- 2 files changed, 309 insertions(+), 218 deletions(-) diff --git a/src/components/ColumnFilterPopover.tsx b/src/components/ColumnFilterPopover.tsx index 03ed1b4..5e6de0a 100644 --- a/src/components/ColumnFilterPopover.tsx +++ b/src/components/ColumnFilterPopover.tsx @@ -1,4 +1,5 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; import { Search, Check, X, Filter } from 'lucide-react'; import { cn } from '../lib/utils'; @@ -37,6 +38,36 @@ export function ColumnFilterPopover({ const [activeTab, setActiveTab] = useState<'values' | 'condition'>('values'); const [condition, setCondition] = useState({ type: 'equals', value: '' }); const [conditionResult, setConditionResult] = useState([]); + const [portalContainer, setPortalContainer] = useState(null); + const [position, setPosition] = useState({ top: 0, left: 0 }); + + useEffect(() => { + const container = document.createElement('div'); + container.id = 'filter-portal-' + Math.random().toString(36).substr(2, 9); + container.style.position = 'fixed'; + container.style.zIndex = '9999'; + container.style.top = '0'; + container.style.left = '0'; + container.style.pointerEvents = 'none'; + document.body.appendChild(container); + setPortalContainer(container); + + const btns = document.querySelectorAll('.filter-trigger-btn'); + btns.forEach(btn => { + if (btn instanceof HTMLElement) { + const rect = btn.getBoundingClientRect(); + const currentLeft = rect.left; + const currentTop = rect.bottom + 4; + setPosition({ top: currentTop, left: currentLeft }); + } + }); + + return () => { + if (document.body.contains(container)) { + document.body.removeChild(container); + } + }; + }, []); const isDraggingRef = React.useRef(false); const dragStartRef = React.useRef(null); @@ -157,224 +188,229 @@ export function ColumnFilterPopover({ }, []); return ( -
= 200 ? 9999 : zIndex }} - onClick={(e) => e.stopPropagation()} - > - - - {title &&
{title}
} - -
- - -
- - {activeTab === 'values' ? ( - <> -
- - setSearch(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && search) { - e.preventDefault(); - onSelectAll(filteredValues); - onClose(); - } - }} - className="w-full bg-slate-900 border border-slate-700 rounded p-1.5 pl-8 pr-7 text-xs text-white focus:outline-none focus:border-blue-500" - autoFocus - /> - {search && ( - - )} -
- -
- {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 (didDragRef.current) return; - if (e.shiftKey && lastClickedIndex !== null) { - const start = Math.min(lastClickedIndex, idx); - const end = Math.max(lastClickedIndex, idx); - const itemsToSelect = filteredValues.slice(start, end + 1); - const newSelected = new Set([...selectedValues, ...itemsToSelect]); - onSelectAll(Array.from(newSelected)); - } else { - onToggle(val); - } - setLastClickedIndex(idx); - }} - onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onToggle(val); setLastClickedIndex(idx); } }} - className={cn( - "flex items-center gap-2 p-1.5 rounded cursor-pointer group transition-colors select-none", - isDragSelected ? "bg-blue-600/40" : "hover:bg-slate-700/50" - )} - > -
- {selectedValues.includes(val) && } -
- {val || '(Empty)'} -
- ); - })} - {filteredValues.length === 0 && ( -
No values found
- )} -
+ + -
-
- - - -
+ {title &&
{title}
} + +
+
- - ) : ( - <> -
-
- - -
-
- - setCondition({ ...condition, value: e.target.value })} - onKeyDown={(e) => { - if (e.key === 'Enter' && condition.value) { - e.preventDefault(); - applyCondition(); - } - }} - placeholder="Enter value..." - className="w-full mt-1 bg-slate-900 border border-slate-700 rounded p-1.5 text-xs text-white focus:outline-none focus:border-blue-500" - /> -
- - {condition.type === 'between' && ( -
- + {activeTab === 'values' ? ( + <> +
+ setCondition({ ...condition, value2: e.target.value })} + placeholder="Filter values..." + value={search} + onChange={e => setSearch(e.target.value)} onKeyDown={(e) => { - if (e.key === 'Enter' && condition.value && condition.value2) { + if (e.key === 'Enter' && search) { e.preventDefault(); - applyCondition(); + onSelectAll(filteredValues); + onClose(); } }} - placeholder="Enter second value..." - className="w-full mt-1 bg-slate-900 border border-slate-700 rounded p-1.5 text-xs text-white focus:outline-none focus:border-blue-500" + className="w-full bg-slate-900 border border-slate-700 rounded p-1.5 pl-8 pr-7 text-xs text-white focus:outline-none focus:border-blue-500" + autoFocus /> + {search && ( + + )}
- )} - -
+
+ {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 (didDragRef.current) return; + if (e.shiftKey && lastClickedIndex !== null) { + const start = Math.min(lastClickedIndex, idx); + const end = Math.max(lastClickedIndex, idx); + const itemsToSelect = filteredValues.slice(start, end + 1); + const newSelected = new Set([...selectedValues, ...itemsToSelect]); + onSelectAll(Array.from(newSelected)); + } else { + onToggle(val); + } + setLastClickedIndex(idx); + }} + onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onToggle(val); setLastClickedIndex(idx); } }} + className={cn( + "flex items-center gap-2 p-1.5 rounded cursor-pointer group transition-colors select-none", + isDragSelected ? "bg-blue-600/40" : "hover:bg-slate-700/50" + )} + > +
+ {selectedValues.includes(val) && } +
+ {val || '(Empty)'} +
+ ); + })} + {filteredValues.length === 0 && ( +
No values found
+ )} +
- {conditionResult.length > 0 && ( -
- Found {conditionResult.length} matching value{conditionResult.length !== 1 ? 's' : ''} -
+
+
+ + + +
+ +
+ + ) : ( + <> +
+
+ + +
+ +
+ + setCondition({ ...condition, value: e.target.value })} + onKeyDown={(e) => { + if (e.key === 'Enter' && condition.value) { + e.preventDefault(); + applyCondition(); + } + }} + placeholder="Enter value..." + className="w-full mt-1 bg-slate-900 border border-slate-700 rounded p-1.5 text-xs text-white focus:outline-none focus:border-blue-500" + /> +
+ + {condition.type === 'between' && ( +
+ + setCondition({ ...condition, value2: e.target.value })} + onKeyDown={(e) => { + if (e.key === 'Enter' && condition.value && condition.value2) { + e.preventDefault(); + applyCondition(); + } + }} + placeholder="Enter second value..." + className="w-full mt-1 bg-slate-900 border border-slate-700 rounded p-1.5 text-xs text-white focus:outline-none focus:border-blue-500" + /> +
+ )} + + +
+ + {conditionResult.length > 0 && ( +
+ Found {conditionResult.length} matching value{conditionResult.length !== 1 ? 's' : ''} +
+ )} + )} - +
, + portalContainer )}
); diff --git a/src/components/PricingView.tsx b/src/components/PricingView.tsx index 63fa1da..8b2c80a 100644 --- a/src/components/PricingView.tsx +++ b/src/components/PricingView.tsx @@ -1151,7 +1151,12 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, - - - - - - - -
handleSort('articleNo')}> @@ -1175,12 +1180,17 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, value={articleNoColFilter} onChange={setArticleNoColFilter} onClose={() => setOpenFilter(null)} - zIndex={isPinned('articleNo') ? 200 : 50} + zIndex={50} /> )} { e.stopPropagation(); handleResizeStart(e, 'articleNo', columnWidths.articleNo); }} /> handleSort('articleName')}> @@ -1203,12 +1213,17 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, value={nameColFilter} onChange={setNameColFilter} onClose={() => setOpenFilter(null)} - zIndex={isPinned('articleName') ? 200 : 50} + zIndex={500} /> )} { e.stopPropagation(); handleResizeStart(e, 'articleName', columnWidths.articleName); }} /> handleSort('line')}> @@ -1234,12 +1249,17 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, onSelectAll={vals => setLineMultiFilter(vals)} onClear={() => { setLineMultiFilter([]); setOpenFilter(null); }} onClose={() => setOpenFilter(null)} - zIndex={isPinned('line') ? 200 : 50} + zIndex={500} /> )} { e.stopPropagation(); handleResizeStart(e, 'line', columnWidths.line); }} /> handleSort('classification')}> @@ -1265,7 +1285,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, onSelectAll={vals => setClassificationFilter(vals)} onClear={() => { setClassificationFilter([]); setOpenFilter(null); }} onClose={() => setOpenFilter(null)} - zIndex={isPinned('classification') ? 200 : 50} + zIndex={50} /> )} { e.stopPropagation(); handleResizeStart(e, 'classification', columnWidths.classification); }} /> @@ -1293,13 +1313,18 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, onSelectAll={vals => setProductTypeFilter(vals)} onClear={() => { setProductTypeFilter([]); setOpenFilter(null); }} onClose={() => setOpenFilter(null)} - zIndex={isPinned('productType') ? 200 : 50} + zIndex={50} /> )} { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} /> handleSort('itemToLogistic')}> @@ -1313,7 +1338,12 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, const colKey = `prc_${col.index}`; const width = columnWidths[colKey] ?? 100; return ( - handleSort(col.index)}> @@ -1355,7 +1385,12 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, ); })} - handleSort('unitsOuter')}> @@ -1381,13 +1416,18 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, onSelectAll={vals => setUnitsOuterFilter(vals)} onClear={() => { setUnitsOuterFilter([]); setOpenFilter(null); }} onClose={() => setOpenFilter(null)} - zIndex={isPinned('unitsOuter') ? 200 : 50} + zIndex={50} /> )} { e.stopPropagation(); handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter); }} /> handleSort('outerW')}> @@ -1413,13 +1453,18 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, onSelectAll={vals => setOuterWFilter(vals)} onClear={() => { setOuterWFilter([]); setOpenFilter(null); }} onClose={() => setOpenFilter(null)} - zIndex={isPinned('outerW') ? 200 : 50} + zIndex={50} /> )} { e.stopPropagation(); handleResizeStart(e, 'outerW', columnWidths.outerW); }} /> handleSort('outerL')}> @@ -1445,13 +1490,18 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, onSelectAll={vals => setOuterLFilter(vals)} onClear={() => { setOuterLFilter([]); setOpenFilter(null); }} onClose={() => setOpenFilter(null)} - zIndex={isPinned('outerL') ? 200 : 50} + zIndex={50} /> )} { e.stopPropagation(); handleResizeStart(e, 'outerL', columnWidths.outerL); }} /> handleSort('outerH')}> @@ -1477,7 +1527,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, onSelectAll={vals => setOuterHFilter(vals)} onClear={() => { setOuterHFilter([]); setOpenFilter(null); }} onClose={() => setOpenFilter(null)} - zIndex={isPinned('outerH') ? 200 : 50} + zIndex={50} /> )} { e.stopPropagation(); handleResizeStart(e, 'outerH', columnWidths.outerH); }} /> @@ -1487,7 +1537,12 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, const colKey = `con_${col.index}`; const width = columnWidths[colKey] ?? 110; return ( - handleSort(col.index)}>