import { useEffectEvent, useLayoutEffect, useState, type RefObject } from "react"; // The shared DOM-measure-and-fit core for a single-line chip lane that collapses its overflow into a // trailing "+N" pill. Owns the layout-effect + ResizeObserver + initial-measure wiring and reads the // common geometry off a hidden mirror (each chip's right edge, the real rem gap, the pill width, and // the lane's available width); each caller layers its OWN fit policy in `decide` and gets that result // back. This was hand-rolled twice — the feed's applied-filter row (ActiveFilterChips, whose policy // also reserves a leading label + a trailing "Clear all" and can collapse to a single count pill) and // the channel tag cell (ChannelTagsControl, chips-only) — with the same measurement half copied in // both. The policy halves live in lib/chipOverflow.ts; this owns the measurement half. // // Coordinate note: `chipRights` are offsets within the hidden mirror (its children's offsetParent), // while `avail` is the VISIBLE lane's clientWidth — the two are comparable because the mirror is laid // out `absolute left-0 top-0` over the lane's start, so a mirror-relative right edge is the position a // chip would take in the real lane. Reads real rem boxes, so nothing is frozen in px (the text-size // slider scales the gap and chip widths, and the fit follows). /** * The class the chip LANE must carry — both halves are load-bearing. * * `relative` is the measurement contract: the hidden mirror is `absolute left-0 top-0`, so the lane is * its offsetParent and a mirror-relative right edge is directly comparable to the lane's clientWidth * (see the coordinate note above). * * `overflow-clip` is the containment half. The mirror is laid out UNBOUNDED — that is the whole point, * it reads natural widths — so its box sticks out far past the lane. An absolutely-positioned box still * contributes to its ancestors' SCROLLABLE overflow, and `visibility: hidden` does NOT exempt it, so * without a clip the mirror leaks all the way up to . Measured on the channel manager at a 1024px * viewport: the mirror dragged the document's scroll width to 1258px — the whole app scrolled * 249px sideways and the fixed header slid to x=-177 — and widened the Tags cell 84px past the width its * fixed-layout column had assigned it. `clip`, not `hidden`, because clip creates no scrollport, so * nothing can ever scroll the mirror into view. The clip cannot disturb the measurement: offsetLeft and * offsetWidth are layout values, and clipping does not change layout. * * `overflow-clip-margin` is what keeps the clip from eating the KEYBOARD FOCUS RING. A bare * `overflow: clip` cuts at the padding box, while the app-wide base ring is `outline: 2px` at * `outline-offset: 2px` — i.e. it paints 4px OUTSIDE the border box. These lanes have no padding and * their height is set by the chip itself, so the first commit of this fix silently cut the ring's top * and bottom bands off every applied-filter chip and the tag lane's `+` trigger, leaving two floating * side bars. 0.25rem is exactly the ring's reach and nowhere near the ~600px the * mirror needs, so containment is unaffected. */ export const chipLaneClass = "relative overflow-clip [overflow-clip-margin:0.25rem]"; export interface ChipRowGeometry { /** Available lane width — the visible row's clientWidth. */ avail: number; /** Right edge (offsetLeft + offsetWidth) of each chip in the measurer, index-aligned to the chips. */ chipRights: number[]; /** Width of the trailing sample "+N" pill in the measurer. */ pillW: number; /** The real rendered inter-item gap (rem-scaled), read off the box between the last chip and the pill. */ gap: number; } export function useChipOverflowFit(args: { /** The VISIBLE lane; its clientWidth is the available width, and resize is observed on it. */ rowRef: RefObject; /** The measurer's chip elements, index-aligned (a hidden mirror of the full chip set). */ chipsRef: RefObject>; /** The sample "+N" pill in the measurer. */ pillRef: RefObject; /** Re-measure when this changes (a key over the chip set — count + labels, so a rename re-fits too). */ deps: unknown; /** The caller's fit policy over the measured geometry. */ decide: (geom: ChipRowGeometry) => R; /** Value used until the first measure (and while the refs aren't mounted). */ initial: R; }): R { const { rowRef, chipsRef, pillRef, deps, decide, initial } = args; const [result, setResult] = useState(initial); // Read `decide` fresh each run (an effect event) without making it an effect dependency: the effect re-runs on `deps` // (and resize), and a caller's decide closure is rebuilt every render — depending on it would // resubscribe the ResizeObserver on every render. const decideNow = useEffectEvent(decide); useLayoutEffect(() => { const row = rowRef.current; const pill = pillRef.current; if (!row || !pill) return; const rightOf = (el: HTMLElement | null | undefined) => el ? el.offsetLeft + el.offsetWidth : 0; const run = () => { const chips = (chipsRef.current ?? []).filter((c): c is HTMLElement => c != null); const lastChip = chips[chips.length - 1]; setResult( decideNow({ avail: row.clientWidth, chipRights: chips.map(rightOf), pillW: pill.offsetWidth, gap: lastChip ? Math.max(0, pill.offsetLeft - rightOf(lastChip)) : 0, }), ); }; run(); // jsdom has no ResizeObserver — the one-shot measure above still runs there (unit tests drive it). if (typeof ResizeObserver === "undefined") return; const ro = new ResizeObserver(run); ro.observe(row); return () => ro.disconnect(); // The three refs are stable by construction, so naming them costs nothing and `deps` stays the // only thing that actually re-measures. }, [deps, rowRef, chipsRef, pillRef]); return result; }