fix(channels): tag-cell review follow-ups — rename-aware fit, measured gap, close reveal-all

Address the /code-review findings on the tag cell:
- key the overflow-fit measurement on tag id+name so a RENAME (which changes chip
  width) re-runs the fit, not just add/remove;
- reserve the '+N' pill space using the MEASURED gap (gap-1 is rem, scales with the
  font slider) instead of a px constant;
- force the reveal-all hover popover's state closed when nothing is collapsed, so it
  can't linger open against an unmounted '+N' trigger.
This commit is contained in:
2026-07-30 23:33:50 +02:00
parent 8ae06ea1fe
commit 2b70ba7a1f
+16 -5
View File
@@ -1,4 +1,4 @@
import { useLayoutEffect, useRef, useState } from "react";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { Check, Plus } from "lucide-react";
import { useTranslation } from "react-i18next";
import { type Tag } from "../lib/api";
@@ -146,7 +146,8 @@ export function ChannelTagsControl({
const rowRef = useRef<HTMLDivElement | null>(null);
const measureRef = useRef<HTMLDivElement | null>(null);
const [visible, setVisible] = useState(attached.length);
const idsKey = attached.map((tg) => tg.id).join(",");
// Key on id AND name so a RENAME (which changes a chip's width) re-runs the fit, not just add/remove.
const tagsKey = attached.map((tg) => `${tg.id}:${tg.name}`).join("|");
useLayoutEffect(() => {
const row = rowRef.current;
@@ -158,7 +159,12 @@ export function ChannelTagsControl({
if (!pill) return;
const pillW = pill.offsetWidth;
const chips = nodes.slice(0, -1);
const GAP = 4; // gap-1
// Measure the real rendered gap (gap-1 is rem — it scales with the font slider) off the box
// between the last chip and the sample pill, rather than assuming a px constant.
const lastChip = chips[chips.length - 1];
const gap = lastChip
? Math.max(0, pill.offsetLeft - (lastChip.offsetLeft + lastChip.offsetWidth))
: 0;
const avail = row.clientWidth;
let count = chips.length;
for (let i = 0; i < chips.length; i++) {
@@ -166,7 +172,7 @@ export function ChannelTagsControl({
if (!chip) break;
const right = chip.offsetLeft + chip.offsetWidth; // right edge within the measurer
const moreAfter = i < chips.length - 1;
if (right > avail - (moreAfter ? pillW + GAP : 0)) {
if (right > avail - (moreAfter ? pillW + gap : 0)) {
count = i;
break;
}
@@ -178,7 +184,7 @@ export function ChannelTagsControl({
const ro = new ResizeObserver(compute);
ro.observe(row);
return () => ro.disconnect();
}, [idsKey]);
}, [tagsKey]);
const hidden = attached.length - visible;
@@ -197,6 +203,11 @@ export function ChannelTagsControl({
mode: "hover",
placement: "bottom",
});
// If tags change so nothing is collapsed anymore, the "+N" trigger unmounts — force the reveal-all
// popover's state closed so it can't linger open against a gone reference.
useEffect(() => {
if (hidden === 0 && overflowOpen) setOverflowOpen(false);
}, [hidden, overflowOpen]);
const pillClass =
"shrink-0 text-[10px] px-1.5 py-0.5 rounded-full bg-card text-muted border border-border";