fix(channels): a pick in the +N fold keeps the focus on its chip after the fold closes

The closing popover hands the focus back to "+N" in a microtask it queues as it unmounts, after the
chip was focused (seen in Chrome; jsdom does not show it). A second focus, queued after it, wins.
This commit is contained in:
2026-11-06 02:32:49 +01:00
parent 4d1cf66d96
commit 673decf4d4
2 changed files with 10 additions and 4 deletions
@@ -152,12 +152,14 @@ describe("TagFilterChips focus on a toggle that moves the chip", () => {
// React gives the focus back to it after the commit. Only a remount (row ↔ fold) or a chip that
// leaves the list loses it.
it("out of the fold: picked in the popover, it is focused on the row", () => {
it("out of the fold: picked in the popover, it is focused on the row", async () => {
fit.visible = 1;
render(<Row start={[]} />);
fireEvent.click(moreBtn()!);
const inFold = named("beta");
press(inFold);
// The closing popover hands the focus back to "+N" a microtask later: past that too.
await act(async () => {});
const onRow = named("beta");
expect(onRow).not.toBe(inFold);
expect(document.activeElement).toBe(onRow);
@@ -136,13 +136,14 @@ export default function TagFilterChips({
// closed fold, or onto the chip now in its old place when it is gone. A toggle in the open fold
// closes it, so the focus never sits outside a popover left open. Held for the frame, since the fit
// re-decides in layout effects after the toggle's render.
const follow = useRef<{ key: string; at: number } | null>(null);
const follow = useRef<{ key: string; at: number; closed: boolean } | null>(null);
const foldRef = useRef<HTMLDivElement | null>(null);
const moreRef = useRef<HTMLButtonElement | null>(null);
const onToggled = (chipKey: string, from: HTMLElement) => {
follow.current = { key: chipKey, at: chips.findIndex((c) => c.key === chipKey) };
const closed = !!foldRef.current?.contains(from);
follow.current = { key: chipKey, at: chips.findIndex((c) => c.key === chipKey), closed };
requestAnimationFrame(() => (follow.current = null));
if (foldRef.current?.contains(from)) setOpen(false);
if (closed) setOpen(false);
};
useLayoutEffect(() => {
const target = follow.current;
@@ -158,6 +159,9 @@ export default function TagFilterChips({
find(inBox(foldRef.current)) ??
(gone ? (row[Math.min(target.at, row.length - 1)] ?? moreRef.current) : moreRef.current);
if (el && document.activeElement !== el) el.focus();
// The closing popover gives the focus back to "+N" in a microtask it queued as it unmounted
// (seen in Chrome): this one, queued after it, has the last word.
if (el && target.closed) queueMicrotask(() => el.isConnected && el.focus());
});
const setMore = (el: HTMLButtonElement | null) => {
moreRef.current = el;