After a plan change confirmed in the dialog on the Products page, keyboard focus falls back to the top of the page #302

Open
opened 2026-10-30 22:51:33 +00:00 by cgalo5758 · 0 comments
Owner

What happens

The Products page is the member page that lists the plans an organization can hold and lets a member switch, resume or cancel one. Those buttons open one shared confirm dialog. When the member confirms a switch, the console answers by re-rendering the whole Plans region, #member-plans, with a banner saying the change was submitted (internal/server/member_plan_forms.go:30-31, internal/server/member_plan_moves.go:297, :453).

The button that opened the dialog sits inside that region (internal/embeds/templates/partials/member_plans.html:195-200, inside internal/embeds/templates/products.html:53), so the answer removes it. The dialog's script then closes the dialog, because the answer landed outside it (internal/embeds/static/confirm-action-modal.js:156-164). Bootstrap, the CSS and JavaScript library the dialog is built on, returns focus to the button that opened a dialog only while that button is still visible, and a removed button is not (internal/embeds/static/bootstrap.bundle.js:4499-4502, :147-148). Focus ends on the page body. The next Tab starts at the top of the page, and a keyboard user has to tab through the navigation again to get back to their plans.

Resume plan and Cancel plan answer into the same region (member_plans.html:155-165, :168-178) and leave focus in the same place, as does a switch preview the console refuses (member_plan_moves.go:135-138). Any other use of the shared dialog whose answer replaces the control that opened it does the same.

What should happen

After the dialog closes, focus lands on something that says what happened: the banner at the top of the re-rendered Plans region, or the card of the plan now held. An autofocus in the re-rendered region is not enough by itself: htmx focuses it during the swap, while the dialog is still open, and the dialog's focus trap moves focus back inside the dialog (bootstrap.bundle.js:4095-4110); the trap is released only when the dialog starts to close (:4321).

Where

internal/embeds/static/confirm-action-modal.js:138-171; internal/server/member_plan_moves.go:419-469; internal/embeds/templates/partials/member_plans.html:10-25, :155-200

Steps

  1. Sign in as a member whose organization pays for a plan on a ladder with another paid tier (docs/testing.md covers the test stack and the Stripe sandbox).
  2. On the Products page, reach the other tier's button with Tab and press Enter, then Tab to Confirm switch and press Enter.
  3. After the dialog closes, press Tab once. Focus goes to the first link of the page, not to the plans.

Why it matters

docs/design-system.md:314-315 promises that modals restore focus; here a keyboard or screen-reader user loses their place after every plan change and has to find the plans again from the top of the page.

### What happens The Products page is the member page that lists the plans an organization can hold and lets a member switch, resume or cancel one. Those buttons open one shared confirm dialog. When the member confirms a switch, the console answers by re-rendering the whole Plans region, `#member-plans`, with a banner saying the change was submitted (`internal/server/member_plan_forms.go:30-31`, `internal/server/member_plan_moves.go:297`, `:453`). The button that opened the dialog sits inside that region (`internal/embeds/templates/partials/member_plans.html:195-200`, inside `internal/embeds/templates/products.html:53`), so the answer removes it. The dialog's script then closes the dialog, because the answer landed outside it (`internal/embeds/static/confirm-action-modal.js:156-164`). Bootstrap, the CSS and JavaScript library the dialog is built on, returns focus to the button that opened a dialog only while that button is still visible, and a removed button is not (`internal/embeds/static/bootstrap.bundle.js:4499-4502`, `:147-148`). Focus ends on the page body. The next Tab starts at the top of the page, and a keyboard user has to tab through the navigation again to get back to their plans. Resume plan and Cancel plan answer into the same region (`member_plans.html:155-165`, `:168-178`) and leave focus in the same place, as does a switch preview the console refuses (`member_plan_moves.go:135-138`). Any other use of the shared dialog whose answer replaces the control that opened it does the same. ### What should happen After the dialog closes, focus lands on something that says what happened: the banner at the top of the re-rendered Plans region, or the card of the plan now held. An `autofocus` in the re-rendered region is not enough by itself: htmx focuses it during the swap, while the dialog is still open, and the dialog's focus trap moves focus back inside the dialog (`bootstrap.bundle.js:4095-4110`); the trap is released only when the dialog starts to close (`:4321`). ### Where `internal/embeds/static/confirm-action-modal.js:138-171`; `internal/server/member_plan_moves.go:419-469`; `internal/embeds/templates/partials/member_plans.html:10-25`, `:155-200` ### Steps 1. Sign in as a member whose organization pays for a plan on a ladder with another paid tier (`docs/testing.md` covers the test stack and the Stripe sandbox). 2. On the Products page, reach the other tier's button with Tab and press Enter, then Tab to Confirm switch and press Enter. 3. After the dialog closes, press Tab once. Focus goes to the first link of the page, not to the plans. ### Why it matters `docs/design-system.md:314-315` promises that modals restore focus; here a keyboard or screen-reader user loses their place after every plan change and has to find the plans again from the top of the page.
cgalo5758 added the
kind
bug
area/member-uiaccessibility
labels 2026-10-30 22:51:33 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wiki-cafe/member-console#302