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.
90 lines
3.9 KiB
TypeScript
90 lines
3.9 KiB
TypeScript
// 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. */
|
|
n: number;
|
|
/** Right edge (offsetLeft + offsetWidth) of the leading label, in the measurer. */
|
|
labelRight: number;
|
|
/** Right edge of each chip, in the measurer (index-aligned to the chips). */
|
|
chipRights: number[];
|
|
/** Width of the "+N" overflow pill. */
|
|
plusW: number;
|
|
/** Width the trailing "Clear all" button needs INCLUDING its leading gap (0 when n <= 1, since it
|
|
* only renders with more than one chip). Reserved only in the "all" branch, which renders it. */
|
|
clearW: number;
|
|
/** Available lane width. */
|
|
avail: number;
|
|
/** The real (rem-scaled) inter-item gap. */
|
|
gap: number;
|
|
/** At or below this lane width an overflow collapses to a single count pill instead of "+N". */
|
|
narrowPx: number;
|
|
}
|
|
|
|
export type ChipFit = { mode: "all" | "some" | "count"; visible: number };
|
|
|
|
export function decideChipLayout(p: ChipFitInput): ChipFit {
|
|
const { n, labelRight, chipRights, plusW, clearW, avail, gap, narrowPx } = p;
|
|
if (n <= 0) return { mode: "all", visible: 0 };
|
|
|
|
// Do all chips (plus the Clear all button it renders) fit? Use the last chip's right edge.
|
|
const allRight = chipRights[n - 1] ?? labelRight;
|
|
if (allRight + clearW <= avail) return { mode: "all", visible: n };
|
|
|
|
// Overflow. On a narrow lane, collapse everything into one count pill.
|
|
if (avail < narrowPx) return { mode: "count", visible: 0 };
|
|
|
|
// Otherwise fit as many chips as leave room for the "+N" pill after them.
|
|
let k = 0;
|
|
for (let i = 0; i < n; i++) {
|
|
const right = chipRights[i] ?? 0;
|
|
if (right + gap + plusW <= avail) k = i + 1;
|
|
else break;
|
|
}
|
|
// If not even one chip fits beside the pill, fall back to the count pill.
|
|
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;
|
|
}
|