fix: improve filter popover positioning and reliability across all views

This commit is contained in:
Christian Vidal Wolf
2026-04-25 09:14:25 +02:00
parent f0f78a6126
commit a2bb2f2307
7 changed files with 54 additions and 21 deletions
+2
View File
@@ -265,6 +265,7 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
)} )}
</span> </span>
<button <button
id={`details-filter-trigger-${col}`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setOpenFilterCol(openFilterCol === col ? null : col); setOpenFilterCol(openFilterCol === col ? null : col);
@@ -286,6 +287,7 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
{openFilterCol === col && ( {openFilterCol === col && (
<ColumnFilterPopover <ColumnFilterPopover
triggerId={`details-filter-trigger-${col}`}
uniqueValues={getUniqueValues(col)} uniqueValues={getUniqueValues(col)}
selectedValues={columnFilters[col] || []} selectedValues={columnFilters[col] || []}
onToggle={(val) => toggleColumnFilter(col, val)} onToggle={(val) => toggleColumnFilter(col, val)}
+26 -16
View File
@@ -1,4 +1,4 @@
import React, { useState, useMemo, useEffect, useRef } from 'react'; import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Search, Check, X, Filter } from 'lucide-react'; import { Search, Check, X, Filter } from 'lucide-react';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
@@ -43,34 +43,44 @@ export function ColumnFilterPopover({
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null); const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
const [position, setPosition] = useState({ top: 0, left: 0 }); const [position, setPosition] = useState({ top: 0, left: 0 });
const updatePosition = useCallback(() => {
if (!triggerId) return;
const trigger = document.getElementById(triggerId);
if (!trigger) return;
const rect = trigger.getBoundingClientRect();
setPosition({
top: rect.bottom + window.scrollY + 4,
left: Math.min(rect.left + window.scrollX, window.innerWidth + window.scrollX - 300)
});
}, [triggerId]);
useEffect(() => { useEffect(() => {
const container = document.createElement('div'); const container = document.createElement('div');
container.id = 'filter-portal-' + Math.random().toString(36).substr(2, 9); container.id = 'filter-portal-' + Math.random().toString(36).substring(2, 9);
container.style.position = 'fixed'; container.style.position = 'absolute';
container.style.zIndex = '9999'; container.style.zIndex = '9999';
container.style.top = '0'; container.style.top = '0';
container.style.left = '0'; container.style.left = '0';
container.style.width = '100%';
container.style.pointerEvents = 'none'; container.style.pointerEvents = 'none';
document.body.appendChild(container); document.body.appendChild(container);
setPortalContainer(container); setPortalContainer(container);
if (triggerId) { updatePosition();
const trigger = document.getElementById(triggerId);
if (trigger) { // Update on scroll and resize
const rect = trigger.getBoundingClientRect(); window.addEventListener('scroll', updatePosition, true);
setPosition({ window.addEventListener('resize', updatePosition);
top: rect.bottom + window.scrollY + 4,
left: Math.min(rect.left + window.scrollX, window.innerWidth - 300)
});
}
}
return () => { return () => {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
if (document.body.contains(container)) { if (document.body.contains(container)) {
document.body.removeChild(container); document.body.removeChild(container);
} }
}; };
}, []); }, [updatePosition]);
const isDraggingRef = React.useRef(false); const isDraggingRef = React.useRef(false);
const dragStartRef = React.useRef<number | null>(null); const dragStartRef = React.useRef<number | null>(null);
@@ -202,8 +212,8 @@ export function ColumnFilterPopover({
)} )}
style={{ style={{
zIndex: 9999, zIndex: 9999,
top: position.top - window.scrollY, top: position.top,
left: position.left - window.scrollX left: position.left
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
+4
View File
@@ -454,6 +454,7 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative"> <div className="relative">
<button <button
id="dims-filter-trigger-line"
onClick={() => setOpenFilter(openFilter === 'line' ? null : 'line')} onClick={() => setOpenFilter(openFilter === 'line' ? null : 'line')}
className={cn( className={cn(
"flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors", "flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors",
@@ -465,6 +466,7 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
</button> </button>
{openFilter === 'line' && ( {openFilter === 'line' && (
<ColumnFilterPopover <ColumnFilterPopover
triggerId="dims-filter-trigger-line"
uniqueValues={uniqueLines} uniqueValues={uniqueLines}
selectedValues={lineFilter} selectedValues={lineFilter}
onToggle={val => setLineFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])} onToggle={val => setLineFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
@@ -479,6 +481,7 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
<div className="relative"> <div className="relative">
<button <button
id="dims-filter-trigger-class"
onClick={() => setOpenFilter(openFilter === 'class' ? null : 'class')} onClick={() => setOpenFilter(openFilter === 'class' ? null : 'class')}
className={cn( className={cn(
"flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors", "flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors",
@@ -490,6 +493,7 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
</button> </button>
{openFilter === 'class' && ( {openFilter === 'class' && (
<ColumnFilterPopover <ColumnFilterPopover
triggerId="dims-filter-trigger-class"
uniqueValues={uniqueClasses} uniqueValues={uniqueClasses}
selectedValues={classFilter} selectedValues={classFilter}
onToggle={val => setClassFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])} onToggle={val => setClassFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
+2
View File
@@ -255,6 +255,7 @@ export function MatrixView({ data, headers, rowStatuses }: MatrixViewProps) {
)} )}
</span> </span>
<button <button
id={`matrix-filter-trigger-${index}`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setOpenFilterCol(openFilterCol === index ? null : index); setOpenFilterCol(openFilterCol === index ? null : index);
@@ -276,6 +277,7 @@ export function MatrixView({ data, headers, rowStatuses }: MatrixViewProps) {
{openFilterCol === index && ( {openFilterCol === index && (
<ColumnFilterPopover <ColumnFilterPopover
triggerId={`matrix-filter-trigger-${index}`}
uniqueValues={getUniqueValues(index)} uniqueValues={getUniqueValues(index)}
selectedValues={columnFilters[index] || []} selectedValues={columnFilters[index] || []}
onToggle={(val) => toggleColumnFilter(index, val)} onToggle={(val) => toggleColumnFilter(index, val)}
+2
View File
@@ -422,6 +422,7 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
) : ( ) : (
<> <>
<button <button
id={`missing-filter-trigger-${col}`}
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === col ? null : col); }} onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === col ? null : col); }}
className={cn( className={cn(
"w-full flex items-center justify-between px-1.5 py-0.5 bg-slate-800 border rounded text-[10px] transition-colors", "w-full flex items-center justify-between px-1.5 py-0.5 bg-slate-800 border rounded text-[10px] transition-colors",
@@ -433,6 +434,7 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
</button> </button>
{openFilter === col && ( {openFilter === col && (
<ColumnFilterPopover <ColumnFilterPopover
triggerId={`missing-filter-trigger-${col}`}
uniqueValues={allValues} uniqueValues={allValues}
selectedValues={selectedFilters} selectedValues={selectedFilters}
onToggle={(val) => setColumnFilters(prev => { onToggle={(val) => setColumnFilters(prev => {
+16 -5
View File
@@ -2067,16 +2067,27 @@ function TextFilterPopover({ value, onChange, onClose, triggerId }: {
}) { }) {
const [position, setPosition] = useState({ top: 0, left: 0 }); const [position, setPosition] = useState({ top: 0, left: 0 });
useEffect(() => { const updatePosition = useCallback(() => {
const trigger = document.getElementById(triggerId); const trigger = document.getElementById(triggerId);
if (trigger) { if (trigger) {
const rect = trigger.getBoundingClientRect(); const rect = trigger.getBoundingClientRect();
setPosition({ setPosition({
top: rect.bottom + window.scrollY + 4, top: rect.bottom + window.scrollY + 4,
left: Math.min(rect.left + window.scrollX, window.innerWidth - 300) left: Math.min(rect.left + window.scrollX, window.innerWidth + window.scrollX - 300)
}); });
} }
}, [triggerId]); }, [triggerId]);
useEffect(() => {
updatePosition();
window.addEventListener('scroll', updatePosition, true);
window.addEventListener('resize', updatePosition);
return () => {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
};
}, [updatePosition]);
const addTerm = () => { const addTerm = () => {
if (value.terms.length < 5) { if (value.terms.length < 5) {
onChange({ ...value, terms: [...value.terms, ''] }); onChange({ ...value, terms: [...value.terms, ''] });
@@ -2101,10 +2112,10 @@ function TextFilterPopover({ value, onChange, onClose, triggerId }: {
return createPortal( return createPortal(
<div <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" className="absolute 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={{ style={{
top: position.top - window.scrollY, top: position.top,
left: position.left - window.scrollX left: position.left
}} }}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
+2
View File
@@ -326,6 +326,7 @@ export function ProductDescriptions({ data, headers, asinColumnIndex, onEdit, ro
)} )}
</span> </span>
<button <button
id={`desc-filter-trigger-${col}`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setOpenFilterCol(openFilterCol === col ? null : col); setOpenFilterCol(openFilterCol === col ? null : col);
@@ -347,6 +348,7 @@ export function ProductDescriptions({ data, headers, asinColumnIndex, onEdit, ro
{openFilterCol === col && ( {openFilterCol === col && (
<ColumnFilterPopover <ColumnFilterPopover
triggerId={`desc-filter-trigger-${col}`}
uniqueValues={getUniqueValues(col)} uniqueValues={getUniqueValues(col)}
selectedValues={columnFilters[col] || []} selectedValues={columnFilters[col] || []}
onToggle={(val) => toggleColumnFilter(col, val)} onToggle={(val) => toggleColumnFilter(col, val)}