Post-Mortem: Diff Tab Redesign, Mobile Drag Fix, and the CSS Specificity Trap
TLDR
During a front-end overhaul of the Ginexys PDF Processor diff panel, three distinct defects emerged: incorrect mouse event coordinate math on mobile screen widths ($<1024\text{px}$), missing touch interaction support on split-pane resizers, and a CSS cascade collision that left inactive diff panels visible on all tabs. We resolved all three by querying computed element styles at drag runtime, registering touch listeners with{ passive: false }, and decoupling layout utility classes from component visibility logic.
| Defect Area | Root Cause | Engineering Solution |
|---|---|---|
| Mobile Axis Resizing | Hardcoded clientX on vertical layouts | Query getComputedStyle($el).flexDirection |
| Mobile Touch Resizing | Browser intercepted touch scroll events | Add { passive: false } & call preventDefault() |
| Panel Visibility Collision | Equal specificity display: flex override | Scope selector to .view-panel.diff-layout |
Technical problem analysis & solutions
1. Dynamic layout axis detection
The visual diff panel switches fromflex-direction: row to flex-direction: column at $1024\text{px}$. The initial resizing handler calculated drag offsets using clientX and outerWidth(), which produced invalid dimensions on vertical stacked layouts.
To fix this, determine layout direction dynamically at drag start by inspecting computed element styles:
export function initResponsivePanelResizer($container, $divider) {
$divider.on('mousedown touchstart', (e) => {
e.preventDefault();
// Query active computed style directly from DOM const isStacked = window.getComputedStyle($container[0]).flexDirection === 'column'; const firstPane = $container.find('.vd-pane').first(); const startSizePx = isStacked ? firstPane.outerHeight() : firstPane.outerWidth(); const startPos = extractPointCoordinate(e, isStacked);
const onMove = (moveEvt) => { const currentPos = extractPointCoordinate(moveEvt, isStacked); const delta = currentPos - startPos; const totalSize = isStacked ? $container.outerHeight() : $container.outerWidth();
const newSizePx = Math.max(120, Math.min(totalSize - 120, startSizePx + delta)); const percentage = (newSizePx / totalSize) * 100;
$container.find('.vd-pane').eq(0).css('flex', 0 0 ${percentage}%); $container.find('.vd-pane').eq(1).css('flex', 0 0 ${100 - percentage}%); };
bindDocumentEvents(onMove); }); }
2. Touch drag event registration
Mobile web browsers defaulttouchmove listeners to passive, preventing preventDefault() calls and causing the page to scroll during divider dragging.
Register touch listeners explicitly with { passive: false }:
function extractPointCoordinate(evt, isStacked) {
const src = evt.touches?.[0] ?? evt;
return isStacked ? src.clientY : src.clientX;
}
// Ensure event listener explicitly allows preventDefault() dividerElement.addEventListener('touchstart', (e) => { if (e.touches.length === 1) { e.preventDefault(); // Prevents scroll gesture interference startDragHandler(e); } }, { passive: false });
3. CSS cascade resolution
A layout helper rule (.diff-layout { display: flex }) shared equal specificity ((0, 1, 0)) with .view-panel { display: none }. Because .diff-layout appeared later in the stylesheet source order, it forced inactive diff panels to remain displayed (display: flex).
/ REFACTORED: Remove display: flex to prevent cascade collision /
.view-panel.diff-layout {
flex-direction: column;
}
Rule of thumb: UsegetComputedStyle()at runtime to handle responsive layout switches, and keep component visibility (display) separated from helper styling.