Files
meshcore-analyzer/public
efitenandClaude Opus 5 5d2e14aba2 fix(#1943): cancel the deferred swatch focus so arrow keys are not undone (#1945)
Closes #1943. The colour picker's keyboard navigation is broken, and the
E2E flake that has been failing unrelated PRs (#1940, #1941, and master
pushes `589fa987` and `859173f1`) was reporting it correctly.

## Cause

`showPopover` deferred focusing the first swatch with an uncancellable
`setTimeout(..., 0)` at `channel-color-picker.js:146`, and nothing
cleared it on hide. The file contained **zero** `clearTimeout` calls.
Reopen the popover while a swatch still holds focus and that timer lands
after the user has already pressed an arrow key, pulling focus back to
the first swatch.

Proven, not argued. Instrumenting `HTMLElement.prototype.focus` with a
stack trace, on one open:

```
focus(#f97316) @10205ms  <- the keydown handler
focus(#ef4444) @10208ms  <- channel-color-picker.js:146:58
```

Three milliseconds apart.

## The user-visible bug

Worse than a flaky test. **Open the picker, arrow to a colour, press
Enter, and the first colour is assigned instead of the one you chose.**
Holding the timing still made the existing suite say so directly:

```
✗ Enter should assign focused color (#f97316), got #ef4444
```

## Why the test looked flaky

The revert happens on **every** open. Only whether the assertion reads
before or after it varies, which is why an idle machine passes and a
loaded runner does not.

#1939 (mine) assumed the opposite: a race in which the handler had not
yet moved focus, cured by waiting for it. #1943 has the measurement that
disproves it. The failing step took **16 ms** while that wait has a **3
second** budget, so the wait was resolving successfully and then the
value was reverted underneath it. It never helped. Its comment is
corrected in this PR rather than left to mislead the next reader.

## Fix

Keep a handle for the timer, cancel a pending one on both show and hide,
and inside it do nothing when the popover has since been hidden or when
focus already sits inside it.

A fresh open still focuses the first swatch, which is what the
accessibility behaviour is for. An open that inherits focus, or a user
who has already navigated, is left alone.

## Verification

- The **regression test added here fails on unmodified master** with `a
late focus timer must not move focus after the user did` and passes with
the fix.
- It is **deterministic, not load-dependent**: it reproduces the exact
sequence the stack trace identified (open, Escape, reopen, ArrowRight
before the timer lands) rather than waiting for contention. It also
asserts the Enter path, so the user-visible half is covered and not just
focus position.
- Full suite: 10 of 10, three consecutive runs.

## Note on the other flake

This is one of two E2E failures blocking the queue. The other, #1925, is
a different mechanism in a different file and is fixed separately in
#1944. Together they should leave the E2E suite deterministic again.

Same shape as @TeTeHacko's finding in #1940: something is operable
before its setup has finished. That is now three instances in this
codebase, so it may be worth a look as a pattern rather than three
separate fixes.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 09:42:10 +02:00
..