fix: improve column pinning with proper z-index stacking

- Add getStickyRank() function for layered z-index on pinned columns
- Higher z-index for rightmost pinned columns so they stack correctly
- Apply z-index dynamically based on column order in pinned list
- All pinned columns now stay visible while scrolling
This commit is contained in:
Christian Vidal Wolf
2026-04-24 13:31:36 +02:00
parent 3bc2753bec
commit 05f9f0ef10
+19 -19
View File
@@ -1330,7 +1330,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
); );
})} })}
<th style={{ width: columnWidths.unitsOuter, ...(isPinned('unitsOuter') ? { left: getStickyLeft('unitsOuter') ?? 0 } : {}) }} className={cn( <th style={{ width: columnWidths.unitsOuter, ...(isPinned('unitsOuter') ? { left: getStickyLeft('unitsOuter') ?? 0, zIndex: getStickyRank('unitsOuter') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative", "text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('unitsOuter') && "sticky bg-slate-900" isPinned('unitsOuter') && "sticky bg-slate-900"
)} onClick={() => handleSort('unitsOuter')}> )} onClick={() => handleSort('unitsOuter')}>
@@ -1361,9 +1361,9 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter); }} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter); }} />
</th> </th>
<th style={{ width: columnWidths.outerW, ...(isPinned('outerW') ? { left: getStickyLeft('outerW') ?? 0 } : {}) }} className={cn( <th style={{ width: columnWidths.outerW, ...(isPinned('outerW') ? { left: getStickyLeft('outerW') ?? 0, zIndex: getStickyRank('outerW') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative", "text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('outerW') && "sticky bg-slate-900 z-20" isPinned('outerW') && "sticky bg-slate-900"
)} onClick={() => handleSort('outerW')}> )} onClick={() => handleSort('outerW')}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1"> <span className="truncate flex items-center gap-1">
@@ -1392,9 +1392,9 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerW', columnWidths.outerW); }} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerW', columnWidths.outerW); }} />
</th> </th>
<th style={{ width: columnWidths.outerL, ...(isPinned('outerL') ? { left: getStickyLeft('outerL') ?? 0 } : {}) }} className={cn( <th style={{ width: columnWidths.outerL, ...(isPinned('outerL') ? { left: getStickyLeft('outerL') ?? 0, zIndex: getStickyRank('outerL') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative", "text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('outerL') && "sticky bg-slate-900 z-20" isPinned('outerL') && "sticky bg-slate-900"
)} onClick={() => handleSort('outerL')}> )} onClick={() => handleSort('outerL')}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1"> <span className="truncate flex items-center gap-1">
@@ -1423,9 +1423,9 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerL', columnWidths.outerL); }} /> <ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'outerL', columnWidths.outerL); }} />
</th> </th>
<th style={{ width: columnWidths.outerH, ...(isPinned('outerH') ? { left: getStickyLeft('outerH') ?? 0 } : {}) }} className={cn( <th style={{ width: columnWidths.outerH, ...(isPinned('outerH') ? { left: getStickyLeft('outerH') ?? 0, zIndex: getStickyRank('outerH') ?? 0 } : {}) }} className={cn(
"text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative", "text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
isPinned('outerH') && "sticky bg-slate-900 z-20" isPinned('outerH') && "sticky bg-slate-900"
)} onClick={() => handleSort('outerH')}> )} onClick={() => handleSort('outerH')}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="truncate flex items-center gap-1"> <span className="truncate flex items-center gap-1">
@@ -1581,24 +1581,24 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)} )}
> >
{/* Art. No. */} {/* Art. No. */}
<td className={cn("px-3 py-2.5 font-mono text-xs text-slate-300 whitespace-nowrap overflow-hidden truncate", isPinned('articleNo') && "sticky bg-slate-800 z-10")} style={isPinned('articleNo') ? { left: getStickyLeft('articleNo') ?? 0 } : {}}> <td className={cn("px-3 py-2.5 font-mono text-xs text-slate-300 whitespace-nowrap overflow-hidden truncate", isPinned('articleNo') && "sticky bg-slate-800")} style={isPinned('articleNo') ? { left: getStickyLeft('articleNo') ?? 0, zIndex: getStickyRank('articleNo') ?? 0 } : {}}>
{row[COLUMNS.ARTICLE_NO]} {row[COLUMNS.ARTICLE_NO]}
</td> </td>
{/* Article Name */} {/* Article Name */}
<td className={cn("px-3 py-2.5 text-slate-200 max-w-[200px]", isPinned('articleName') && "sticky bg-slate-800 z-10")} style={isPinned('articleName') ? { left: getStickyLeft('articleName') ?? 0 } : {}}> <td className={cn("px-3 py-2.5 text-slate-200 max-w-[200px]", isPinned('articleName') && "sticky bg-slate-800")} style={isPinned('articleName') ? { left: getStickyLeft('articleName') ?? 0, zIndex: getStickyRank('articleName') ?? 0 } : {}}>
<span className="line-clamp-1" title={row[COLUMNS.ARTICLE_NAME]}> <span className="line-clamp-1" title={row[COLUMNS.ARTICLE_NAME]}>
{row[COLUMNS.ARTICLE_NAME] || '—'} {row[COLUMNS.ARTICLE_NAME] || '—'}
</span> </span>
</td> </td>
{/* Line */} {/* Line */}
<td className={cn("px-3 py-2.5 text-slate-400 whitespace-nowrap text-xs overflow-hidden truncate", isPinned('line') && "sticky bg-slate-800 z-10")} style={isPinned('line') ? { left: getStickyLeft('line') ?? 0 } : {}}> <td className={cn("px-3 py-2.5 text-slate-400 whitespace-nowrap text-xs overflow-hidden truncate", isPinned('line') && "sticky bg-slate-800")} style={isPinned('line') ? { left: getStickyLeft('line') ?? 0, zIndex: getStickyRank('line') ?? 0 } : {}}>
{row[COLUMNS.LINE] || '—'} {row[COLUMNS.LINE] || '—'}
</td> </td>
{/* Classification */} {/* Classification */}
<td className={cn("px-3 py-2.5 overflow-hidden", isPinned('classification') && "sticky bg-slate-800 z-10")} style={isPinned('classification') ? { left: getStickyLeft('classification') ?? 0 } : {}}> <td className={cn("px-3 py-2.5 overflow-hidden", isPinned('classification') && "sticky bg-slate-800")} style={isPinned('classification') ? { left: getStickyLeft('classification') ?? 0, zIndex: getStickyRank('classification') ?? 0 } : {}}>
<span className={cn( <span className={cn(
'px-2 py-0.5 rounded text-[10px] font-bold border whitespace-nowrap inline-block max-w-full truncate', 'px-2 py-0.5 rounded text-[10px] font-bold border whitespace-nowrap inline-block max-w-full truncate',
String(row[COLUMNS.CLASSIFICATION] || '').toUpperCase().includes('CORE') String(row[COLUMNS.CLASSIFICATION] || '').toUpperCase().includes('CORE')
@@ -1612,7 +1612,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</td> </td>
{/* TYPE cell */} {/* TYPE cell */}
<td className={cn("px-3 py-2 overflow-visible relative", isPinned('productType') && "sticky bg-slate-800 z-10")} style={isPinned('productType') ? { left: getStickyLeft('productType') ?? 0 } : {}}> <td className={cn("px-3 py-2 overflow-visible relative", isPinned('productType') && "sticky bg-slate-800")} style={isPinned('productType') ? { left: getStickyLeft('productType') ?? 0, zIndex: getStickyRank('productType') ?? 0 } : {}}>
{editingType?.rowIndex === dataIndex ? ( {editingType?.rowIndex === dataIndex ? (
<div className="relative"> <div className="relative">
<input <input
@@ -1658,7 +1658,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</td> </td>
{/* ITEM TO LOGISTIC cell */} {/* ITEM TO LOGISTIC cell */}
<td className={cn("px-3 py-2 overflow-visible relative", isPinned('itemToLogistic') && "sticky bg-slate-800 z-10")} style={isPinned('itemToLogistic') ? { left: getStickyLeft('itemToLogistic') ?? 0 } : {}}> <td className={cn("px-3 py-2 overflow-visible relative", isPinned('itemToLogistic') && "sticky bg-slate-800")} style={isPinned('itemToLogistic') ? { left: getStickyLeft('itemToLogistic') ?? 0, zIndex: getStickyRank('itemToLogistic') ?? 0 } : {}}>
{editingLogistic?.rowIndex === dataIndex ? ( {editingLogistic?.rowIndex === dataIndex ? (
<input <input
ref={logisticInputRef} ref={logisticInputRef}
@@ -1697,7 +1697,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const colKey = `prc_${col.index}`; const colKey = `prc_${col.index}`;
return ( return (
<td key={col.index} className={cn("px-3 py-2 overflow-hidden", isPinned(colKey) && "sticky bg-slate-800 z-10")} style={isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0, zIndex: getStickyRank(colKey) ?? 0 } : {}}> <td key={col.index} className={cn("px-3 py-2 overflow-hidden", isPinned(colKey) && "sticky bg-slate-800")} style={isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0, zIndex: getStickyRank(colKey) ?? 0 } : {}}>
{isEditing ? ( {isEditing ? (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<input <input
@@ -1738,16 +1738,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
})} })}
{/* Units columns */} {/* Units columns */}
<td className={cn("px-3 py-2.5", isPinned('unitsOuter') && "sticky bg-slate-800 z-10")} style={isPinned('unitsOuter') ? { left: getStickyLeft('unitsOuter') ?? 0 } : {}}> <td className={cn("px-3 py-2.5", isPinned('unitsOuter') && "sticky bg-slate-800")} style={isPinned('unitsOuter') ? { left: getStickyLeft('unitsOuter') ?? 0, zIndex: getStickyRank('unitsOuter') ?? 0 } : {}}>
{unitOuterBadge(row[unitsOuterIdx])} {unitOuterBadge(row[unitsOuterIdx])}
</td> </td>
<td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerW') && "sticky bg-slate-800 z-10")} style={isPinned('outerW') ? { left: getStickyLeft('outerW') ?? 0 } : {}}> <td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerW') && "sticky bg-slate-800")} style={isPinned('outerW') ? { left: getStickyLeft('outerW') ?? 0, zIndex: getStickyRank('outerW') ?? 0 } : {}}>
{row[COLUMNS.OUTER_W] ?? '-'} {row[COLUMNS.OUTER_W] ?? '-'}
</td> </td>
<td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerL') && "sticky bg-slate-800 z-10")} style={isPinned('outerL') ? { left: getStickyLeft('outerL') ?? 0 } : {}}> <td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerL') && "sticky bg-slate-800")} style={isPinned('outerL') ? { left: getStickyLeft('outerL') ?? 0, zIndex: getStickyRank('outerL') ?? 0 } : {}}>
{row[COLUMNS.OUTER_L] ?? '-'} {row[COLUMNS.OUTER_L] ?? '-'}
</td> </td>
<td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerH') && "sticky bg-slate-800 z-10")} style={isPinned('outerH') ? { left: getStickyLeft('outerH') ?? 0 } : {}}> <td className={cn("px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate", isPinned('outerH') && "sticky bg-slate-800")} style={isPinned('outerH') ? { left: getStickyLeft('outerH') ?? 0, zIndex: getStickyRank('outerH') ?? 0 } : {}}>
{row[COLUMNS.OUTER_H] ?? '-'} {row[COLUMNS.OUTER_H] ?? '-'}
</td> </td>
@@ -1755,7 +1755,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{containerCols.map(col => { {containerCols.map(col => {
const colKey = `con_${col.index}`; const colKey = `con_${col.index}`;
return ( return (
<td key={col.index} className={cn("px-3 py-2.5 overflow-hidden truncate", isPinned(colKey) && "sticky bg-slate-800 z-10")} style={isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0, zIndex: getStickyRank(colKey) ?? 0 } : {}}> <td key={col.index} className={cn("px-3 py-2.5 overflow-hidden truncate", isPinned(colKey) && "sticky bg-slate-800")} style={isPinned(colKey) ? { left: getStickyLeft(colKey) ?? 0, zIndex: getStickyRank(colKey) ?? 0 } : {}}>
{unitBadge(row[col.index], col.name)} {unitBadge(row[col.index], col.name)}
</td> </td>
); );