Skip to content

Listbox - continued - #27

Merged
ukutaht merged 12 commits into
mainfrom
pr-25
Sep 14, 2026
Merged

ukutaht merged 12 commits into
mainfrom
pr-25

Conversation

@ukutaht

@ukutaht ukutaht commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #25

RobertJoonas and others added 12 commits September 10, 2026 13:10
Dropdown's menu semantics (role="menu"/"menuitem") don't fit picking a
value for a form - that's an ARIA listbox pattern instead. Listbox
renders a hidden form field and instantly updates the trigger's label
on selection, ahead of any phx-change round-trip, while keeping the
same Floating UI positioning and keyboard navigation as Dropdown.

Also adds a demo docs page with live examples, Wallaby coverage, and
an Introduction page entry.
phx:show-start/phx:hide-end fire asynchronously, and the inner
listbox's display is mutated as part of that same async completion.
A rapid close-then-reopen (or reopen-then-close) - e.g. keyboard
users toggling quickly, or key-repeat/assistive tech firing events
faster than a human would - can start overlapping show/hide
sequences, and whichever one's completion lands last wins,
regardless of which was triggered most recently. The listbox could
then silently close itself right after being reopened.

Both handlers now re-assert the inner listbox's visibility against
our own synchronous state (the wrapper) so a stale completion
corrects itself instead of clobbering a more recent action.
Also guards handleEnterOrSpace against a stale data-focus attribute
left over from a just-closed listbox, and tears down any previous
autoUpdate loop before starting a new one to avoid leaking it when
handleShowStart fires more than once for the same open.

Verified with 30 consecutive runs of the keyboard toggle test
(previously ~30% flaky) plus 3 full clean runs of the Listbox suite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every modal Wallaby fixture rendered <.modal_overlay /> with no CSS
class, so it was an empty div with zero rendered height - genuinely
invisible to Selenium's visibility check regardless of its `display`
value, which is why "expected overlay visible" assertions failed
consistently (100% reproducible in isolation, not a timing flake).

Fixtures that also wrapped .modal_panel in unstyled divs had a second,
compounding issue: once the overlay correctly got `position: fixed`
(covering the full viewport), it started painting above the panel's
*unpositioned* wrapper divs per normal CSS stacking rules (positioned
elements paint above static ones regardless of DOM order) - silently
swallowing clicks meant for buttons inside the panel, such as the
close button.

Fixed by giving each fixture's overlay and panel wrapper the same
classes already shown in the moduledoc's own documented examples.
No changes to lib/prima/modal.ex or assets/js/hooks/modal.js - this
was a test-fixture authoring gap, not a component bug.

Verified with multiple full runs of all four affected suites, both
isolated (--max-cases 1) and under normal concurrency: 27/27 passing
consistently, versus 100% reproducible failure before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"Orange" is always present in the option list, even before the
debounced (200ms) async search actually runs - an empty search
matches every option server-side. So the test's assertion that
"Orange" exists didn't prove the search for "Orange" had completed;
under CPU load (a slow CI runner, or a stress-tested local machine),
Enter could fire before the debounce/round-trip landed, selecting
whatever was still focused from the stale, unfiltered list instead.

Reproduced 100% (10/10) under manufactured CPU pressure, root-caused
via a debug trace showing addSelection("Cherry") when the test meant
to select "Orange". No bug in combobox.js - Enter correctly selects
whatever's focused; the test had no signal that the filtered results
had actually arrived. Fixed by waiting for the option count to narrow
to the single match "Orange" produces among the fixture's options,
which only becomes true once the debounced search has completed.

Verified with 15 consecutive runs under CPU pressure (previously
reproduced instantly) plus a clean full run of the suite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same root cause as the Listbox fix (6f6bc49): phx:show-start/phx:hide-end
fire asynchronously, and the inner menu's display is mutated as part of
that same async completion. A rapid close-then-reopen - e.g. Escape
immediately followed by Enter/Space, exactly what
"focus returns to trigger button when dropdown closes" does - can call
execJS(show) and execJS(hide) back-to-back before the previous call's
internal steps finish, so their completions interleave and whichever
lands last wins, regardless of which was triggered most recently. The
menu could then silently close itself right after being reopened.

dropdown.js is what Listbox's hook was originally copied from, so it
carried the identical gap. Applied the same fix: handleShowStart/
handleHideEnd now re-assert the inner menu's visibility against the
wrapper (our own synchronous state) so a stale completion corrects
itself, handleEnterOrSpace no longer trusts a stale data-focus left
over from a just-closed menu, and any previous autoUpdate loop is torn
down before a new one starts.

Verified with 30 consecutive runs of the previously-failing test
(reported as a fresh failure on a clean local run) plus 3 full clean
runs of all 6 Dropdown Wallaby suites (54 features each).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ukutaht
ukutaht merged commit b1dda8e into main Sep 14, 2026
1 of 2 checks passed
@ukutaht
ukutaht deleted the pr-25 branch September 14, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants