Skip to content

fix(SplitButton): make the menu half a keyboard-accessible menu button - #1278

Open
JayPokale wants to merge 1 commit into
ClickHouse:mainfrom
JayPokale:fix-split-button-keyboard
Open

JayPokale wants to merge 1 commit into
ClickHouse:mainfrom
JayPokale:fix-split-button-keyboard

Conversation

@JayPokale

@JayPokale JayPokale commented Oct 21, 2026 •

Copy link
Copy Markdown

Description

The menu half of SplitButton could only be used with a pointer (#1272). Dropdown.Trigger wraps its children in a div and slots the Radix trigger onto it, so the menu half had no role, was not in the tab order, and its only name was the icon's aria-label="chevron-down".

  • The menu half is now a BaseButton (type="button") passed to DropdownMenu.Trigger with asChild, following the WAI-ARIA APG Menu Button pattern. Radix gives it aria-haspopup="menu", aria-expanded and the Enter / Space / ArrowDown handling. SplitButton already renders Dropdown (the Radix root), so it uses the Radix trigger directly rather than changing Dropdown.Trigger for every consumer.
  • It is named "More options". The new optional menuAriaLabel prop sets another name (same convention as ariaLabel on CodeBlock and Tabs). The chevron icon is aria-hidden.
  • When the split button is disabled, the menu half gets disabled and aria-disabled="true" and is out of the tab order.
  • .split-button has overflow: hidden, which clipped the outside focus ring to a sliver. The menu half draws its ring inside (outline-offset: -2px) with an inner ring in --click-global-color-background-default, because in the dark theme the outline color is the same yellow as the primary button.

Links and tickets

Closes #1272

Good to know

  • Unfocused rendering is unchanged: I built Storybook from main and from this branch and compared screenshots of the Primary, Secondary, both Disabled, PrimaryWithIcon and FillWidth stories in the same browser; they are pixel-identical. The focus snapshot in tests/buttons/splitbutton.spec.ts focuses the first button (the main half), which this does not change. I did not run the Docker visual tests.
  • The main half's focus ring is clipped by the same overflow: hidden. I left it as it is to keep this PR to the menu half and the existing focus snapshot unchanged; happy to follow up.
  • yarn test locally has 9 failures in Icon, TextTruncate and InitCUIThemeScript; the same 9 fail on main with my Node 26 (CI uses Node 24). All SplitButton tests pass.

Checklist

  • Breaking changes? (add migration notes in changesets)
  • Visual changes? (specify in changesets) Only the focus ring of the menu half; described in the changeset.
  • Design review needed? The inner two-tone focus ring might want a look.

Contribution

  • Sufficient research before PR
  • Self-reviewed the PR
  • Manually tested the changes (when applies - visual confirmation in Storybook)
  • build and build-storybook work locally
  • Tests and Stories are aligned with the changes: 6 new unit tests (they fail on main), a FocusVisible story and a KeyboardNavigation story with a play function.

Accessibility

  • Keyboard - In the built Storybook: Tab reaches the main button, then the menu button; Enter, Space and ArrowDown open the menu and move focus to the first item; Escape closes it and returns focus to the menu button; Shift+Tab goes back. With disabled, Tab skips both halves.
  • Names — the menu button is "More options" (or menuAriaLabel); the icon is aria-hidden.
  • Vision — colors come from tokens; the focus ring shows in light and dark, for primary and secondary; the menu button is 27×30 px.

Checked: WCAG 2.2 2.1.1 Keyboard, 2.4.3 Focus Order, 2.4.7 Focus Visible, 2.5.8 Target Size (Minimum), 4.1.2 Name, Role, Value. axe-core (from the installed @storybook/addon-a11y dependency) reports 0 violations on the Primary, Secondary, PrimaryDisabled, PrimaryWithIcon and FocusVisible stories. Not tested: screen readers.

Screenshots

Keyboard focus on the menu half, light and dark, primary and secondary: the ring is inside the menu half; before this change the menu half could not receive keyboard focus. Unfocused states are pixel-identical to main.


Note

Low Risk
Scoped a11y and trigger implementation change on SplitButton with an optional prop and no breaking API; only behavioral change is improved keyboard/focus handling and a visible focus ring on the menu half.

Overview
Makes the menu (chevron) half of SplitButton a real, keyboard-focusable menu button instead of a pointer-only trigger, closing the accessibility gap where Dropdown.Trigger wrapped a non-focusable div.

The secondary control is now a BaseButton wired through Radix DropdownMenu.Trigger (asChild), so Tab order is main action → menu button, Enter / Space / ArrowDown open the menu, and assistive tech gets aria-haspopup="menu", aria-expanded, and the default name "More options" (overridable via new menuAriaLabel). The chevron is aria-hidden; when disabled, the menu half is disabled and skipped in the tab order. :focus-visible styling adds an inset focus ring on the menu half so it stays visible inside the clipped overflow: hidden container.

Adds unit tests, a FocusVisible story, and a KeyboardNavigation story with a play function. Unfocused appearance is unchanged aside from the menu-half focus ring.

Reviewed by Cursor Bugbot for commit d991f24. Bugbot is set up for automated code reviews on this repo. Configure here.

The menu half was a div (Dropdown.Trigger wraps its children in one), so Tab skipped it, it had no role, and its only
name was the icon's "chevron-down". Render it as a button with the Radix trigger: it is in the tab order, opens with
Enter, Space or ArrowDown, is named "More options" (menuAriaLabel overrides it), and is disabled with the split
button. Its focus ring is drawn inside, since the split button clips its overflow.

Closes ClickHouse#1272
@changeset-bot

changeset-bot Bot commented Oct 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d991f24

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clickhouse/click-ui Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

This branch has not been deployed

No deployments
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.

SplitButton: the menu half cannot be reached by keyboard and is announced as "chevron-down"

1 participant