refactor(chips): share the chip-overflow measure core between the two lanes (S3)

ActiveFilterChips (feed applied-filter row) and ChannelTagsControl both
hand-rolled the same single-line chip-fit measurer: a hidden mirror + a
layout-effect + ResizeObserver reading chip right edges, the real rem gap,
the pill width and the lane width. The policy half was already extracted
(decideChipLayout); the DOM-measurement half was still duplicated.

Promote that half to useChipOverflowFit(rowRef, chipsRef, pillRef, deps,
decide, initial): it owns the effect + observer + initial measure and reads
the common geometry, then hands it to each caller's own policy. The feed
row layers its leading label + trailing "Clear all" reservation on top and
keeps decideChipLayout; the tag cell's chips-only policy is extracted as
fitChipsWithPill (pure, unit-tested) alongside it. Add a jsdom test for the
hook (stubbed offsets, re-measure on deps). Behaviour unchanged — verified
in the browser: feed all/count modes + popover, tag "+N" fit + hover reveal.
This commit is contained in:
2026-08-23 23:22:52 +02:00
parent 9538fb1b7a
commit fc32188fd2
6 changed files with 313 additions and 109 deletions
+44 -8
View File
@@ -1,11 +1,13 @@
// Pure fit decision for a single-line chip lane that must share its width with fixed trailing
// controls (the feed's applied-filter row + Sort/View). Lifted out of ActiveFilterChips so the
// all/some/count math is unit-testable without a layout engine (jsdom reports 0-width boxes), the
// same split as lib/dateFilter and lib/columnSort. The caller measures real widths off a hidden
// mirror (including the live rem gap, so nothing is frozen in px) and hands them here.
//
// A sibling one-line-fit measurer lives in ChannelTags (ChannelTagsControl); if a third appears,
// promote the DOM-measurement half to a shared hook and keep this as its policy core.
// Pure fit decisions for a single-line chip lane that collapses its overflow into a "+N" pill.
// Lifted out of the components so the math is unit-testable without a layout engine (jsdom reports
// 0-width boxes), the same split as lib/dateFilter and lib/columnSort. The shared DOM-measurement
// half — the layout-effect + ResizeObserver that reads real rem boxes off a hidden mirror — lives in
// lib/useChipOverflowFit; it hands the measured geometry to one of these policies:
// - decideChipLayout: the feed's applied-filter row, which also shares its width with fixed trailing
// controls (Sort/View), reserves a leading label + a trailing "Clear all", and collapses to a
// single count pill on a narrow lane.
// - fitChipsWithPill: the chips-only channel tag cell (ChannelTagsControl) — just "how many fit
// before the +N pill".
export interface ChipFitInput {
/** Number of chips. */
@@ -51,3 +53,37 @@ export function decideChipLayout(p: ChipFitInput): ChipFit {
if (k === 0 && labelRight + gap + plusW > avail) return { mode: "count", visible: 0 };
return { mode: "some", visible: k };
}
// The chips-only policy (ChannelTagsControl): how many chips fit on the line, reserving the "+N" pill's
// width whenever any overflow. Returns the visible count.
export interface ChipsWithPillInput {
/** Right edge (offsetLeft + offsetWidth) of each chip, index-aligned. */
chipRights: number[];
/** Width of the "+N" pill. */
pillW: number;
/** Available lane width. */
avail: number;
/** The real (rem-scaled) inter-item gap. */
gap: number;
}
export function fitChipsWithPill(p: ChipsWithPillInput): number {
const { chipRights, pillW, avail, gap } = p;
const n = chipRights.length;
if (n === 0) return 0;
// Whole-set fast path: if the LAST chip's right edge already fits, every chip fits and NO pill is
// needed. The reservation loop below reserves the pill for every chip except the last and breaks at
// the first overflow, so on its own it would collapse a short trailing chip (narrower than the pill)
// that would actually have fitted, leaving "+N" beside empty space.
if ((chipRights[n - 1] ?? 0) <= avail) return n;
let count = n;
for (let i = 0; i < n; i++) {
const right = chipRights[i] ?? 0;
const moreAfter = i < n - 1;
if (right > avail - (moreAfter ? pillW + gap : 0)) {
count = i;
break;
}
}
return count;
}