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.
120 lines
4.6 KiB
TypeScript
120 lines
4.6 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { decideChipLayout, fitChipsWithPill, type ChipFitInput } from "./chipOverflow";
|
|
|
|
// Build measurer-style right edges for `n` chips of a fixed width, starting after a label, with a gap.
|
|
function rights(labelRight: number, widths: number[], gap: number): number[] {
|
|
const out: number[] = [];
|
|
let cursor = labelRight;
|
|
for (const w of widths) {
|
|
cursor += gap + w;
|
|
out.push(cursor);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const base = (over: Partial<ChipFitInput> = {}): ChipFitInput => {
|
|
const gap = 6;
|
|
const labelRight = 60;
|
|
const widths = [50, 50, 50]; // 3 chips
|
|
return {
|
|
n: 3,
|
|
labelRight,
|
|
chipRights: rights(labelRight, widths, gap),
|
|
plusW: 40,
|
|
clearW: 6 + 60,
|
|
avail: 1000,
|
|
gap,
|
|
narrowPx: 420,
|
|
...over,
|
|
};
|
|
};
|
|
|
|
describe("decideChipLayout", () => {
|
|
it("shows all chips when the whole set plus Clear all fits", () => {
|
|
expect(decideChipLayout(base({ avail: 1000 }))).toEqual({ mode: "all", visible: 3 });
|
|
});
|
|
|
|
it("empty chip set is a no-op 'all' with 0 visible", () => {
|
|
expect(decideChipLayout(base({ n: 0, chipRights: [] }))).toEqual({ mode: "all", visible: 0 });
|
|
});
|
|
|
|
it("collapses to a single count pill on a narrow lane when overflowing", () => {
|
|
// last chip right edge is ~228; avail below NARROW and below that -> count.
|
|
expect(decideChipLayout(base({ avail: 200, narrowPx: 420 }))).toEqual({
|
|
mode: "count",
|
|
visible: 0,
|
|
});
|
|
});
|
|
|
|
it("fits as many chips as leave room for the +N pill on a wide-but-tight lane", () => {
|
|
// chip right edges: 116, 172, 228; clearW 66. all-fit needs 228+66=294 <= avail; keep avail below
|
|
// that (so not 'all') but >= narrowPx (so not 'count'). Loop reserves gap(6)+plus(40)=46 per chip:
|
|
// chip2 228+46=274 > 250 -> dropped; chip1 218 <= 250 -> visible 2.
|
|
expect(decideChipLayout(base({ avail: 250, narrowPx: 200 }))).toEqual({
|
|
mode: "some",
|
|
visible: 2,
|
|
});
|
|
});
|
|
|
|
it("reserves the +N pill width so a later chip is dropped when it wouldn't fit beside it", () => {
|
|
// Two chips of width 100 after a label, gap 6, plus pill 40. Right edges: 166, 272. avail 300,
|
|
// narrowPx 100 (so 300 >= narrow -> 'some'): chip0 166+46=212 <= 300 ok; chip1 272+46=318 > 300
|
|
// -> dropped. visible 1.
|
|
const input = base({
|
|
n: 2,
|
|
chipRights: rights(60, [100, 100], 6),
|
|
plusW: 40,
|
|
clearW: 6 + 60,
|
|
avail: 300,
|
|
gap: 6,
|
|
narrowPx: 100,
|
|
});
|
|
expect(decideChipLayout(input)).toEqual({ mode: "some", visible: 1 });
|
|
});
|
|
|
|
it("falls back to the count pill when not even one chip fits beside the +N pill", () => {
|
|
// Wide-ish lane (>= narrow) but a single very wide chip: label 60, chip width 500 -> right 566.
|
|
// avail 430 >= narrow(420); all-fit 566+clearW > 430; loop: 566+6+40 > 430 -> k=0; labelRight
|
|
// 60+6+40=106 <= 430 so NOT count by that guard... use narrow-avail instead: keep avail 430 but
|
|
// make label+gap+plus exceed it to force count.
|
|
const input = base({
|
|
n: 1,
|
|
labelRight: 400,
|
|
chipRights: [966],
|
|
plusW: 40,
|
|
clearW: 0,
|
|
avail: 430,
|
|
gap: 6,
|
|
narrowPx: 420,
|
|
});
|
|
// label 400 + gap 6 + plus 40 = 446 > 430 and k===0 -> count.
|
|
expect(decideChipLayout(input)).toEqual({ mode: "count", visible: 0 });
|
|
});
|
|
});
|
|
|
|
describe("fitChipsWithPill", () => {
|
|
// Three chips of width 50, gap 6, starting at 0 -> right edges 50, 106, 162.
|
|
const chipRights = rights(0, [50, 50, 50], 6);
|
|
|
|
it("shows all chips (no pill) when the last chip's right edge fits — the whole-set fast path", () => {
|
|
expect(fitChipsWithPill({ chipRights, pillW: 40, gap: 6, avail: 200 })).toBe(3);
|
|
});
|
|
|
|
it("returns 0 for an empty chip set", () => {
|
|
expect(fitChipsWithPill({ chipRights: [], pillW: 40, gap: 6, avail: 200 })).toBe(0);
|
|
});
|
|
|
|
it("reserves the pill width for all but the last chip when overflowing", () => {
|
|
// avail 120: whole set (162) doesn't fit. chip0 right 50 <= 120-(40+6)=74 ok; chip1 right 106 > 74
|
|
// -> dropped at i=1 -> visible 1.
|
|
expect(fitChipsWithPill({ chipRights, pillW: 40, gap: 6, avail: 120 })).toBe(1);
|
|
});
|
|
|
|
it("keeps the whole set via the fast path even when a reserve-for-all loop would drop chips", () => {
|
|
// Right edges 56, 112, 118; avail 120: the whole set fits (118 <= 120). A naive loop that reserves
|
|
// pill+gap (46) for every non-last chip would drop at chip1 (112 > 120-46=74) and show "+2" beside
|
|
// empty space — the fast path returns all 3 instead.
|
|
expect(fitChipsWithPill({ chipRights: [56, 112, 118], pillW: 40, gap: 6, avail: 120 })).toBe(3);
|
|
});
|
|
});
|