From f99240d8ca5f45d1e72f7d884c6d247f0a885eef Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 5 Sep 2026 22:22:14 +0200 Subject: [PATCH] fix(e10): stop the hidden chip measurer scrolling the whole app sideways MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four chip lanes render an intentionally unbounded hidden "mirror" so the shared fit core can read each chip's natural width. An absolutely positioned box still contributes to its ancestors' SCROLLABLE overflow, and `visibility: hidden` does not exempt it — and nothing between the toolbar band and clipped it. At the 1024x768 floor that meant the channel manager's document scroll width was 1258px: the whole app scrolled 249px sideways into dead space and dragged the fixed header to x=-177. The same mirror was also 228px wide inside the Tags column's 144px cell, which pushed the table to 990px inside its 906px container — i.e. it broke DataTable's "the table never exceeds its container" contract and truncated channel names to "The...". One cause, two symptoms. The lane now carries a shared `chipLaneClass` = `relative overflow-clip`, so the contract lives in one constant instead of four hand-rolled class strings. `clip` rather than `hidden` because it creates no scrollport, so nothing can ever scroll the mirror into view. Measurement is untouched: offsetLeft/offsetWidth are layout values and a clip does not change layout. Measured after: document 1258 -> 1024, sideways scroll 249 -> 0, table 990 -> 921 with no cell overflow, and the "+N" collapse still fits exactly as before. A source-level test asserts every component rendering a mirror uses the lane class — the drift a fifth hand-rolled lane would introduce, which no DOM test can catch since jsdom has no layout engine to overflow. --- frontend/src/components/ActiveFilterChips.tsx | 4 +- frontend/src/components/ChannelTags.tsx | 6 +-- frontend/src/components/TagFilterChips.tsx | 4 +- frontend/src/lib/chipLane.contract.test.ts | 48 +++++++++++++++++++ frontend/src/lib/useChipOverflowFit.ts | 19 ++++++++ 5 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 frontend/src/lib/chipLane.contract.test.ts diff --git a/frontend/src/components/ActiveFilterChips.tsx b/frontend/src/components/ActiveFilterChips.tsx index e335dabf..47253798 100644 --- a/frontend/src/components/ActiveFilterChips.tsx +++ b/frontend/src/components/ActiveFilterChips.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { Filter, X } from "lucide-react"; import type { ActiveFilter } from "../lib/useActiveFilters"; import { decideChipLayout } from "../lib/chipOverflow"; -import { useChipOverflowFit } from "../lib/useChipOverflowFit"; +import { chipLaneClass, useChipOverflowFit } from "../lib/useChipOverflowFit"; import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; // The applied-filter row above the feed: every narrowing currently in force, each removable on its @@ -172,7 +172,7 @@ export default function ActiveFilterChips({
{/* Hidden mirror of the full set, used only to measure natural widths. Kept out of the a11y tree diff --git a/frontend/src/components/ChannelTags.tsx b/frontend/src/components/ChannelTags.tsx index 1a6e497d..01050987 100644 --- a/frontend/src/components/ChannelTags.tsx +++ b/frontend/src/components/ChannelTags.tsx @@ -5,7 +5,7 @@ import { type Tag } from "../lib/api"; import { useAnchoredPopover, PopoverSurface, PopoverItem } from "./ui/AnchoredPopover"; import { useScrollFade } from "../lib/useScrollFade"; import { fitChipsWithPill } from "../lib/chipOverflow"; -import { useChipOverflowFit } from "../lib/useChipOverflowFit"; +import { chipLaneClass, useChipOverflowFit } from "../lib/useChipOverflowFit"; // The channel tag surface, shared by the channel manager (table + cards) and the channel detail // page. Built on the R8 popover platform. Two design goals it fixes over the old inline TagsCell: @@ -129,7 +129,7 @@ function SuggestedChipsLane({ }, [hidden, overflowOpen]); return ( -
+
{/* Hidden measurer — real ghost chips at natural width so the fit hook reads their right edges. */}
{/* The add trigger LEADS the row — a stable left anchor that never moves as chips are added (they grow to its right), so its editor popover can't drift. Compact icon in the width- diff --git a/frontend/src/components/TagFilterChips.tsx b/frontend/src/components/TagFilterChips.tsx index fb5590ed..550c4df9 100644 --- a/frontend/src/components/TagFilterChips.tsx +++ b/frontend/src/components/TagFilterChips.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { fitChipsWithPill } from "../lib/chipOverflow"; -import { useChipOverflowFit } from "../lib/useChipOverflowFit"; +import { chipLaneClass, useChipOverflowFit } from "../lib/useChipOverflowFit"; import { filterChipClass } from "./ui/FilterChipGroup"; import { useAnchoredPopover, PopoverSurface } from "./ui/AnchoredPopover"; @@ -74,7 +74,7 @@ export default function TagFilterChips({ const moreLabel = t("channels.tags.moreCount", { count: hidden }); return ( -
+
{/* Hidden measurer: all chips + a sample "+N" pill at natural widths, so the shared fit core can read each chip's right edge, the rem gap and the pill width off real boxes. */}