Skip to content

fix(core/ui): add aria-label and arrow key menu navigation#5243

Draft
marcoscaceres wants to merge 3 commits intomainfrom
fix/884-close-a11y
Draft

fix(core/ui): add aria-label and arrow key menu navigation#5243
marcoscaceres wants to merge 3 commits intomainfrom
fix/884-close-a11y

Conversation

@marcoscaceres
Copy link
Copy Markdown
Contributor

Closes #884
Closes #1064

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to improve the accessibility and keyboard usability of the ReSpec UI, specifically the pill/menu interactions tied to issues #884 and #1064. It updates core/ui so the modal close control is labeled for assistive tech and the menu can move focus with keyboard navigation keys.

Changes:

  • Add an accessible name to the modal close button.
  • Make opening the ReSpec menu safely focus the first command only when one exists.
  • Add ArrowUp/ArrowDown/Home/End keyboard navigation, plus Escape handling, for the ReSpec menu.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/core/ui.js
Comment on lines 96 to +99
respecPill.setAttribute("aria-expanded", String(menu.hidden));
toggleMenu();
respecPill.focus();
return;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already handled at line 91: the document-level click handler unconditionally sets aria-expanded to "false". See the test at line 34 of ui-spec.js ("resets aria-expanded when menu is closed by outside click").

Comment thread src/core/ui.js
Comment on lines +101 to +126
const items = /** @type {HTMLElement[]} */ ([
...menu.querySelectorAll("button:not([disabled])"),
]);
const currentIndex = items.indexOf(
/** @type {HTMLElement} */ (document.activeElement)
);
switch (e.key) {
case "ArrowDown": {
e.preventDefault();
const next = items[(currentIndex + 1) % items.length];
next?.focus();
break;
}
case "ArrowUp": {
e.preventDefault();
const prev = items[(currentIndex - 1 + items.length) % items.length];
prev?.focus();
break;
}
case "Home":
e.preventDefault();
items[0]?.focus();
break;
case "End":
e.preventDefault();
items[items.length - 1]?.focus();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already covered: see the describe("keyboard navigation") block starting at line 87 of ui-spec.js. Tests for ArrowDown, ArrowUp, Home, End, and Escape are all there.

Comment thread src/core/ui.js
Comment on lines 50 to 53
const closeButton = html`<button
class="close-button"
aria-label="Close"
onclick=${() => ui.closeModal()}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The currentModalOwner variable (line 64) is set in freshModal() (line 313) when a modal opens. closeModal() at line 283 uses owner || currentModalOwner as the effective owner, so calling closeModal() without an argument correctly resets the opener's aria-expanded via the fallback.

@marcoscaceres marcoscaceres force-pushed the fix/884-close-a11y branch 2 times, most recently from d62ff67 to 07e3515 Compare May 4, 2026 07:12
@marcoscaceres
Copy link
Copy Markdown
Contributor Author

@copilot Can you confirm all your feedback has been addressed? Can you fix anything that was not addressed?

Copy link
Copy Markdown
Contributor

Copilot AI commented May 4, 2026

@copilot Can you confirm all your feedback has been addressed? Can you fix anything that was not addressed?

All three review issues have been addressed in commit f684d20:

  1. Outside-click doesn't reset aria-expanded – The document-level click handler now sets respecPill.setAttribute("aria-expanded", "false") before hiding the menu, so screen readers are always notified when the menu closes.

  2. Close button leaves pill at aria-expanded="true" – A modalOwner variable now tracks the element that opened the modal (set in freshModal, cleared in closeModal). closeModal uses it as a fallback when no explicit owner is passed, so both the close button click and the document-level Escape keypress correctly reset aria-expanded on the opener pill.

  3. Missing keyboard-navigation tests – Four new tests were added to tests/spec/core/ui-spec.js: outside-click aria-expanded reset, Escape key close + aria-expanded reset, ArrowDown/ArrowUp focus movement, and Home/End focus movement.

The documentElement click handler checked menu.hidden before setting
aria-expanded to false, but something was hiding the menu before the
handler ran (race with toggleMenu). Move the setAttribute outside the
if-guard so it always resets regardless of menu state.

Verified in Safari: aria-expanded correctly goes from "true" to "false"
on outside click.
When freshModal replaces an existing modal (e.g., opening warnings
while errors modal is open), the previous owner's aria-expanded was
never reset to "false". Now resets it before setting the new owner.

Found by Opus semantic gate spot-check.
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.

ReSpec menu not very usable with keyboard ReSpec pill does not do A11Y stuff to announce opens / closes

3 participants