mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 19:15:23 +02:00
fix: portal all filter popovers to prevent occlusion by sticky columns
This commit is contained in:
@@ -32,7 +32,8 @@ export function ColumnFilterPopover({
|
||||
onClose,
|
||||
title,
|
||||
className,
|
||||
zIndex = 50
|
||||
zIndex = 50,
|
||||
triggerId
|
||||
}: ColumnFilterPopoverProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'values' | 'condition'>('values');
|
||||
@@ -52,15 +53,16 @@ export function ColumnFilterPopover({
|
||||
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 });
|
||||
if (triggerId) {
|
||||
const trigger = document.getElementById(triggerId);
|
||||
if (trigger) {
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
setPosition({
|
||||
top: rect.bottom + window.scrollY + 4,
|
||||
left: Math.min(rect.left + window.scrollX, window.innerWidth - 300)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (document.body.contains(container)) {
|
||||
@@ -195,7 +197,11 @@ export function ColumnFilterPopover({
|
||||
"absolute w-72 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl p-3 flex flex-col gap-3 animate-in fade-in zoom-in-95 duration-100 pointer-events-auto",
|
||||
className
|
||||
)}
|
||||
style={{ zIndex: 9999 }}
|
||||
style={{
|
||||
zIndex: 9999,
|
||||
top: position.top - window.scrollY,
|
||||
left: position.left - window.scrollX
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ExcelRow } from '../types';
|
||||
import { useColumns } from '../contexts/ColumnsContext';
|
||||
import {
|
||||
@@ -1149,7 +1150,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</div>
|
||||
) : (
|
||||
<table ref={tableRef} className="w-full text-sm border-collapse table-fixed">
|
||||
<thead className="sticky top-0 z-10 bg-slate-900 border-b border-slate-700">
|
||||
<thead className="sticky top-0 z-[100] bg-slate-900 border-b border-slate-700">
|
||||
<tr>
|
||||
<th style={{
|
||||
width: columnWidths.articleNo,
|
||||
@@ -1166,9 +1167,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
Article No <SortIcon current={sortConfig.key === 'articleNo' ? sortConfig.direction : null} />
|
||||
</span>
|
||||
<button
|
||||
id="filter-trigger-sku"
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'sku' ? null : 'sku'); }}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||
articleNoColFilter.terms.some(t => t.trim() !== '') ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
@@ -1177,6 +1179,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</div>
|
||||
{openFilter === 'sku' && (
|
||||
<TextFilterPopover
|
||||
triggerId="filter-trigger-sku"
|
||||
value={articleNoColFilter}
|
||||
onChange={setArticleNoColFilter}
|
||||
onClose={() => setOpenFilter(null)}
|
||||
@@ -1199,9 +1202,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{isPinned('articleName') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||
Article Name <SortIcon current={sortConfig.key === 'articleName' ? sortConfig.direction : null} /></span>
|
||||
<button
|
||||
id="filter-trigger-name"
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'name' ? null : 'name'); }}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||
nameColFilter.terms.some(t => t.trim() !== '') ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
@@ -1210,6 +1214,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</div>
|
||||
{openFilter === 'name' && (
|
||||
<TextFilterPopover
|
||||
triggerId="filter-trigger-name"
|
||||
value={nameColFilter}
|
||||
onChange={setNameColFilter}
|
||||
onClose={() => setOpenFilter(null)}
|
||||
@@ -1232,9 +1237,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{isPinned('line') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||
Line <SortIcon current={sortConfig.key === 'line' ? sortConfig.direction : null} /></span>
|
||||
<button
|
||||
id="filter-trigger-line"
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'line' ? null : 'line'); }}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||
lineMultiFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
@@ -1243,6 +1249,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</div>
|
||||
{openFilter === 'line' && (
|
||||
<ColumnFilterPopover
|
||||
triggerId="filter-trigger-line"
|
||||
uniqueValues={uniqueLines}
|
||||
selectedValues={lineMultiFilter}
|
||||
onToggle={val => setLineMultiFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||
@@ -1268,9 +1275,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{isPinned('classification') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||
Classification <SortIcon current={sortConfig.key === 'classification' ? sortConfig.direction : null} /></span>
|
||||
<button
|
||||
id="filter-trigger-class"
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'classification' ? null : 'classification'); }}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||
classificationFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
@@ -1279,6 +1287,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</div>
|
||||
{openFilter === 'classification' && (
|
||||
<ColumnFilterPopover
|
||||
triggerId="filter-trigger-class"
|
||||
uniqueValues={uniqueClassifications}
|
||||
selectedValues={classificationFilter}
|
||||
onToggle={val => setClassificationFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||
@@ -1299,14 +1308,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{isPinned('productType') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||
Type <SortIcon current={sortConfig.key === 'productType' ? sortConfig.direction : null} />
|
||||
<button
|
||||
id="filter-trigger-type"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setOpenFilter(openFilter === 'productType' ? null : 'productType'); }}
|
||||
className={cn('ml-1 p-0.5 rounded transition-colors', productTypeFilter.length > 0 ? 'text-purple-400 bg-purple-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100')}
|
||||
className={cn('ml-1 p-0.5 rounded transition-colors filter-trigger-btn', productTypeFilter.length > 0 ? 'text-purple-400 bg-purple-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100')}
|
||||
>
|
||||
<Filter className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
{openFilter === 'productType' && (
|
||||
<ColumnFilterPopover
|
||||
triggerId="filter-trigger-type"
|
||||
uniqueValues={uniqueProductTypes}
|
||||
selectedValues={productTypeFilter}
|
||||
onToggle={(val: string) => setProductTypeFilter((prev: string[]) => prev.includes(val) ? prev.filter((v: string) => v !== val) : [...prev, val])}
|
||||
@@ -1352,21 +1363,23 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{isPinned(colKey) && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||
{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
|
||||
<button
|
||||
id={`filter-trigger-${colKey}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenFilter(openFilter === `prc_${col.index}` ? null : `prc_${col.index}`);
|
||||
setOpenFilter(openFilter === colKey ? null : colKey);
|
||||
}}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
<Filter className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
{openFilter === `prc_${col.index}` && (
|
||||
<ColumnFilterPopover
|
||||
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
||||
{openFilter === colKey && (
|
||||
<ColumnFilterPopover
|
||||
triggerId={`filter-trigger-${colKey}`}
|
||||
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
||||
selectedValues={dynamicColFilters[col.index] || []}
|
||||
onToggle={val => setDynamicColFilters(prev => {
|
||||
const current = prev[col.index] || [];
|
||||
@@ -1399,9 +1412,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{isPinned('unitsOuter') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||
Units/Outer <SortIcon current={sortConfig.key === 'unitsOuter' ? sortConfig.direction : null} /></span>
|
||||
<button
|
||||
id="filter-trigger-unitsOuter"
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter'); }}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||
unitsOuterFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
@@ -1410,6 +1424,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</div>
|
||||
{openFilter === 'unitsOuter' && (
|
||||
<ColumnFilterPopover
|
||||
triggerId="filter-trigger-unitsOuter"
|
||||
uniqueValues={uniqueUnitsOuter}
|
||||
selectedValues={unitsOuterFilter}
|
||||
onToggle={val => setUnitsOuterFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||
@@ -1436,9 +1451,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{isPinned('outerW') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||
Outer W <SortIcon current={sortConfig.key === 'outerW' ? sortConfig.direction : null} /></span>
|
||||
<button
|
||||
id="filter-trigger-outerW"
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerW' ? null : 'outerW'); }}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||
outerWFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
@@ -1447,6 +1463,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</div>
|
||||
{openFilter === 'outerW' && (
|
||||
<ColumnFilterPopover
|
||||
triggerId="filter-trigger-outerW"
|
||||
uniqueValues={uniqueOuterW}
|
||||
selectedValues={outerWFilter}
|
||||
onToggle={val => setOuterWFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||
@@ -1473,9 +1490,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{isPinned('outerL') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||
Outer L <SortIcon current={sortConfig.key === 'outerL' ? sortConfig.direction : null} /></span>
|
||||
<button
|
||||
id="filter-trigger-outerL"
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerL' ? null : 'outerL'); }}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||
outerLFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
@@ -1484,6 +1502,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</div>
|
||||
{openFilter === 'outerL' && (
|
||||
<ColumnFilterPopover
|
||||
triggerId="filter-trigger-outerL"
|
||||
uniqueValues={uniqueOuterL}
|
||||
selectedValues={outerLFilter}
|
||||
onToggle={val => setOuterLFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||
@@ -1510,9 +1529,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{isPinned('outerH') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||
Outer H <SortIcon current={sortConfig.key === 'outerH' ? sortConfig.direction : null} /></span>
|
||||
<button
|
||||
id="filter-trigger-outerH"
|
||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerH' ? null : 'outerH'); }}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||
outerHFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
@@ -1521,6 +1541,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
</div>
|
||||
{openFilter === 'outerH' && (
|
||||
<ColumnFilterPopover
|
||||
triggerId="filter-trigger-outerH"
|
||||
uniqueValues={uniqueOuterH}
|
||||
selectedValues={outerHFilter}
|
||||
onToggle={val => setOuterHFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||
@@ -1551,21 +1572,23 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{isPinned(colKey) && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||
{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
|
||||
<button
|
||||
id={`filter-trigger-${colKey}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenFilter(openFilter === `con_${col.index}` ? null : `con_${col.index}`);
|
||||
setOpenFilter(openFilter === colKey ? null : colKey);
|
||||
}}
|
||||
className={cn(
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
>
|
||||
<Filter className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
{openFilter === `con_${col.index}` && (
|
||||
<ColumnFilterPopover
|
||||
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
||||
{openFilter === colKey && (
|
||||
<ColumnFilterPopover
|
||||
triggerId={`filter-trigger-${colKey}`}
|
||||
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
||||
selectedValues={dynamicColFilters[col.index] || []}
|
||||
onToggle={val => setDynamicColFilters(prev => {
|
||||
const current = prev[col.index] || [];
|
||||
@@ -2002,12 +2025,24 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
}
|
||||
|
||||
// ── Text filter popover ───────────────────────────────────────────────────────
|
||||
function TextFilterPopover({ value, onChange, onClose, zIndex = 50 }: {
|
||||
function TextFilterPopover({ value, onChange, onClose, triggerId }: {
|
||||
value: { terms: string[]; op: 'and' | 'or' };
|
||||
onChange: (v: { terms: string[]; op: 'and' | 'or' }) => void;
|
||||
onClose: () => void;
|
||||
zIndex?: number;
|
||||
triggerId: string;
|
||||
}) {
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const trigger = document.getElementById(triggerId);
|
||||
if (trigger) {
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
setPosition({
|
||||
top: rect.bottom + window.scrollY + 4,
|
||||
left: Math.min(rect.left + window.scrollX, window.innerWidth - 300)
|
||||
});
|
||||
}
|
||||
}, [triggerId]);
|
||||
const addTerm = () => {
|
||||
if (value.terms.length < 5) {
|
||||
onChange({ ...value, terms: [...value.terms, ''] });
|
||||
@@ -2030,8 +2065,15 @@ function TextFilterPopover({ value, onChange, onClose, zIndex = 50 }: {
|
||||
onChange({ ...value, terms: next });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute top-full left-0 mt-1 w-72 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl p-4 flex flex-col gap-4 animate-in fade-in zoom-in-95 duration-100" style={{ zIndex }} onClick={e => e.stopPropagation()}>
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed z-[9999] w-72 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl p-4 flex flex-col gap-4 animate-in fade-in zoom-in-95 duration-100"
|
||||
style={{
|
||||
top: position.top - window.scrollY,
|
||||
left: position.left - window.scrollX
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Show rows where field contains:</label>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user