Skip to content

Improve toolbar accessibility - #1173

Open
brunoprietog wants to merge 11 commits into
mainfrom
toolbar-a11y
Open

Improve toolbar accessibility#1173
brunoprietog wants to merge 11 commits into
mainfrom
toolbar-a11y

Conversation

@brunoprietog

@brunoprietog brunoprietog commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

The toolbar was only partly usable for keyboard and screen reader users. Arrow keys let focus escape the toolbar instead of staying within it, open menus didn't confine navigation to their items, and Escape didn't return focus predictably. Active states were exposed with aria-pressed, which isn't valid on menuitem elements, and the eighteen color swatches all shared the same "Aa" accessible name, so there was no way to tell them apart or know whether one changed the text or the background.

So this PR:

  • Confines arrow navigation to the toolbar and to whatever menu is open, so focus can't escape. The toolbar moves with Left/Right; open menus take all four arrows and wrap.
  • Makes Escape return focus to the editor by default, or to the button that opened the menu when it was opened from the keyboard.
  • Sends focus back to the editor after undo/redo instead of dropping it on the body, and marks disabled controls with aria-disabled so they stay reachable.
  • Keeps the focus ring visible when a menu is opened with the mouse, so the first item shows where focus landed instead of moving there silently.
  • Brings the table row and column more menus onto the same pattern. They were a native <details>, so they had no menu roles and arrow keys escaped to the surrounding controls; now the ToolbarDropdown is decoupled from lexxy-toolbar through a small host contract that lexxy-table-tools also implements, and each more menu is a trigger with aria-haspopup and a role="menu" panel of menuitem actions, with arrow navigation that stays inside, Escape back to the trigger, and focus returning to the editor after an action.
  • Marks active menu options with aria-checked instead of aria-pressed: menuitemradio for block formats, where one is always active, and menuitemcheckbox for color swatches, which toggle on and off.
  • Gives each color a readable label (Yellow, Magenta, Sand, and so on) and splits the swatches into "Text color" and "Background color" groups, so they're announced by name and purpose. The color config moves to { value, label } pairs and still accepts plain strings.
  • Names the formatting and color menus and shortens the overflow control to "More options".
  • Hides the decorative toolbar icons from the accessibility tree, so each button is announced by its name alone.
  • Adds tests for the navigation, Escape, the disabled state, the menu roles, the named color groups, the focus ring on mouse-opened menus, and the table more menus.

Basecamp card: https://app.basecamp.com/2914079/buckets/47229151/card_tables/cards/9995277043

Copilot AI review requested due to automatic review settings June 28, 2026 02:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 1, 2026 23:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 20 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • src/elements/toolbar_icons.js: Generated file

Comment thread src/helpers/accessibility_helper.js
Comment thread src/helpers/format_helper.js
Copilot AI review requested due to automatic review settings July 8, 2026 10:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 20 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • src/elements/toolbar_icons.js: Generated file

Comment thread src/helpers/accessibility_helper.js
Comment thread src/elements/dropdown/highlight.js
Copilot AI review requested due to automatic review settings July 9, 2026 05:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • src/elements/toolbar_icons.js: Generated file

Comment thread src/elements/dropdown/highlight.js
Copilot AI review requested due to automatic review settings July 9, 2026 11:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • src/elements/toolbar_icons.js: Generated file

Comment thread src/helpers/format_helper.js
Copilot AI review requested due to automatic review settings July 9, 2026 12:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • src/elements/toolbar_icons.js: Generated file

Comment thread src/helpers/format_helper.js
Arrow keys move along the bar with left/right and through an open menu
with all four arrows, wrapping and trapped so they never leak out. Escape
returns focus to the trigger when the menu was opened from the keyboard,
otherwise to the editor.
Disabled buttons use aria-disabled instead of the native attribute, so the
keyboard reaches them and focus never falls to the body when a button
disables itself. A command activated from inside a dropdown returns focus
to the editor instead of being stranded on the hidden panel.
aria-pressed is not valid on role="menuitem", so screen readers ignored
the active state of the format and color menus. Block formats become
menuitemradio and colors menuitemcheckbox, both exposing their state
through aria-checked. The highlight CSS follows the new attribute.
The highlight menu rendered all eighteen color swatches with the same
"Aa" accessible name, so a screen reader user couldn't tell them apart
or know whether a swatch changed the text or the background.

Each color now carries a readable label (Yellow, Magenta, Sand, and so
on), and the swatches split into "Text color" and "Background color"
groups. The color config moves from bare CSS values to { value, label }
pairs and still accepts plain strings, so existing configurations keep
working. The native toolbar's initialized event exposes the same label
next to its name and value.
The formatting and color menus had no accessible name, so a screen
reader announced them only as "menu". They now carry their trigger's
name, matching what the link dialog already did.

The overflow control drops "Show more toolbar buttons" for "More
options", shorter and in line with the other triggers. Now that it's
an icon button like the rest, it uses title instead of aria-label,
which was a leftover from when it was a summary element with visible
text.

The dummy's external-toolbar example still used that old
details/summary form, so it moves to lexxy-toolbar-dropdown to match
the real component, and both example toolbars adopt the new wording.
The toolbar's SVG icons carried no aria-hidden, so each one surfaced as
an anonymous img node inside its button. The button's name already
comes from its title, so the icon adds nothing but noise. Marking the
SVGs aria-hidden removes the stray img and leaves each button announced
by its name alone.
Opening a dropdown with the mouse left its first item focused without a
visible focus ring: a plain focus() moving in from a mouse click inherits
the mouse modality, so the browser paints no ring until a keyboard
interaction happens. Pass focusVisible so the ring shows right away.
Browsers that don't support the option ignore it and keep the current
behavior.
The row and column "more" menus were a native <details>, so they had no
menu roles, arrow keys escaped to the surrounding controls, and Escape
dropped out of the whole toolbar. Reuse the ToolbarDropdown the main
toolbar already uses: decouple it from lexxy-toolbar through a small host
contract that lexxy-table-tools also implements, and build each more menu
as a trigger with aria-haspopup and a role="menu" panel of menuitem
actions.

Opening now focuses the first item with a visible ring, arrows stay in
the menu and wrap, Escape closes it and returns to the trigger, and
activating an action returns focus to the editor, like the main toolbar.
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