Skip to content

feat(ui5-toolbar): align keyboard navigation with APG and improve nested item delegation#13231

Open
plamenivanov91 wants to merge 20 commits intomainfrom
tb-wacg-arrow-nav
Open

feat(ui5-toolbar): align keyboard navigation with APG and improve nested item delegation#13231
plamenivanov91 wants to merge 20 commits intomainfrom
tb-wacg-arrow-nav

Conversation

@plamenivanov91
Copy link
Copy Markdown
Contributor

@plamenivanov91 plamenivanov91 commented Mar 9, 2026

Implement boundary-aware keyboard delegation for toolbar content with mixed complexity, while preserving intuitive behavior for grouped controls and self-managed components.

What changed:

  • Improved toolbar roving navigation to resolve active toolbar items from nested focus targets.
  • Kept Arrow/Home/End behavior APG-aligned while delegating arrows to controls that own keyboard handling.
  • Added wrapper-level navigation to move within wrapper targets first and exit at boundaries.
  • Preserved radio-group behavior without changing radio internals.
  • Added boundary-wrap detection via event origin path to prevent double-processing.
  • Improved generic focus targeting by preferring first tabbable descendant when no focus API is exposed.
  • Added generic single-child fallback: if key is consumed but focus does not move, let toolbar continue.

Tests:

  • Added toolbar coverage for radio forward boundary handoff.
  • Added toolbar coverage for radio backward entry behavior.
  • Added toolbar coverage for middle-item radio no double advance.
  • Added toolbar coverage for checkbox internal traversal and boundary exit.
  • Added toolbar coverage for backward entry into multi-item wrappers.
  • Verified Toolbar spec: 42 passing.
  • Verified RadioButton spec: 22 passing.

Validation:

  • No new source errors introduced in toolbar keyboard navigation implementation.
  • Existing unrelated type warning in Toolbar Cypress file remains pre-existing and unchanged.

Add comprehensive keyboard navigation to toolbar following WCAG toolbar pattern:
- Arrow keys (Left/Right/Up/Down) navigate between toolbar items
- Home/End jump to first/last item
- Tab/Shift+Tab navigate within toolbar, exit at edges
- Roving tabindex pattern (single tab stop, toolbar is one stop)
- Focus entry: remembers last focused item on re-entry, defaults to first item on initial entry
- Proper handling of complex nested controls (ToolbarSelect)

Resolves focus loss when pressing Shift+Tab on complex toolbar children.
Ensures all toolbar items are reachable via keyboard navigation.

Optimizations:
- Extracted _focusItem() helper to reduce code duplication
- Simplified Tab/Shift+Tab navigation logic

Fixes: #12945
@ui5-webcomponents-bot
Copy link
Copy Markdown
Collaborator

ui5-webcomponents-bot commented Mar 9, 2026

@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview March 9, 2026 17:11 Inactive
@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview March 9, 2026 17:33 Inactive
@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview March 10, 2026 13:23 Inactive
@dobrinyonkov
Copy link
Copy Markdown
Contributor

Tested this PR. Good progress so far, but found a few issues we should fix before merge.

Most of them can be reproduced in the toolbar with toolbar items test page here

https://pr-13231--ui5-webcomponents-preview.netlify.app/packages/main/test/pages/toolbaritems

  1. Arrow navigation gets trapped on ToolbarSelect
  2. Arrow navigation does not work in the second sample
  3. Some of the items in the third sample are not reachable

RTL not handled
ArrowLeft always goes to the previous item and ArrowRight always goes to the next item. In RTL layouts this is backwards. ArrowLeft should go to the next item when the document is RTL. Other components in this repo like Menu swap directions based on this.effectiveDir. You can test this by setting dir="rtl" on the body

Do not check hardcoded tag names

Your _isFromComplexToolbarChild method checks element.tagName === "UI5-TOOLBAR-SELECT". This breaks when tag names are scoped.
BAD: element.tagName === "UI5-BUTTON"
GOOD: Use createInstanceChecker helper with duck-typing

Adviced is to use the createInstanceChecker but that we not work here so simple duck-type check would work

Add a property like handlesOwnKeyboardNavigation to ToolbarItemBase. Set it to true in ToolbarSelect. Check that property instead of the tag name:

// In ToolbarItemBase.ts
@property({ type: Boolean })
handlesOwnKeyboardNavigation = false;
// In ToolbarSelect.ts  
handlesOwnKeyboardNavigation = true; // Override in class

// In Toolbar.ts - check the property, not the tag
_shouldItemHandleOwnNavigation(item: ToolbarItemBase): boolean {
    return item.handlesOwnKeyboardNavigation;
}

Other complex toolbar items can opt-in without modifying the toolbar code.

Happy to discuss these over a call.

- fix rtl arrow direction mapping for left and right keys
- replace hardcoded tag checks with handlesOwnKeyboardNavigation on toolbar items
- opt in ui5-toolbar-select to own keyboard handling
- fix wrapped toolbar-item focus resolution and current-item detection
- skip non-focusable wrapped content from roving tabindex navigation
- add cypress coverage for wrapped items, non-focusable content, rtl behavior, and select traversal
@plamenivanov91
Copy link
Copy Markdown
Contributor Author

plamenivanov91 commented Mar 23, 2026

Tested this PR. Good progress so far, but found a few issues we should fix before merge.

....

Happy to discuss these over a call.

Thanks for the thorough review. I fixed the issues. We can also have a call if any improvements need to be done.

@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview March 23, 2026 09:30 Inactive
Improve WCAG toolbar keyboard behavior while preventing regressions
for nested and overflowed controls.

- add key-level ownership hook shouldHandleOwnKeyboardNavigation(event)
- use key-aware delegation in toolbar and overflow key handlers
- preserve native input/textarea/contenteditable navigation (Arrow/Home/End)
- narrow overflow preventDefault to toolbar-consumed navigation only
- refactor navigation item collection for standard and overflow paths
- update keyboard handling documentation in Toolbar
- add cypress regressions for input Home/End in toolbar and overflow
- fix toolbar cypress type check for itemsToOverflow membership assertion
@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview March 25, 2026 16:34 Inactive
@yanaminkova
Copy link
Copy Markdown
Contributor

I tested the PR and found the following issues that should be fixed:



In the toolbar items sample:
https://pr-13231--ui5-webcomponentspreview.netlify.app/packages/main/test/pages/toolbaritems



  1. Standard Toolbar with ToolbarSelect and ToolbarButton example

  • Focus the second select and press the up/down arrow keys - the focus moves to the previous/next toolbar item.

    This is incorrect - after changing the select’s value, the focus should remain within the select component instead of moving away.

  1. Toolbar with breadcrumbs (self-overflowed) component at the beginning example

  • Focus the breadcrumbs dropdown and press down/right arrow - the focus moves to the next interactive item.

    The arrow navigation should be reserved for navigating between breadcrumb items.

  1. Toolbar with Various Form Controls example
  • Focus CheckBox 1 and press down/right arrow - the focus jumps directly to the input field.
    CheckBox 2 and CheckBox 3 cannot be reached via arrow keys or Tab.
    The same issue occurs in reverse - focus the input and press up/left arrow - the focus moves back to CheckBox 1, skipping the other checkboxes.

  • Additionally, when the focus is within a group (radiobutton, checkbox), arrow key navigation should be limited to elements inside that group. Moving focus outside the group should only be possible using the Tab key.

In the toolbar sample:
https://pr-13231--ui5-webcomponents-preview.netlify.app/packages/main/test/pages/toolbar

  1. Two inline Toolbars with separator (with action on "User Menu" button) example

  • Focus the User Menu button and press up/right arrow - the focus gets stuck and no longer moves.

  1. Additionally, the text and title components are not focusable elements, so they should not receive focus or be included in the navigation flow.

@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview April 21, 2026 09:29 Inactive
Treat generic toolbar items as focus islands: arrow keys stay inside, while Tab and Shift+Tab move to the next or previous item in the same toolbar.
Add and update Cypress coverage to validate arrow, Home/End, RTL, and Tab navigation behavior, including checkbox-group to input transitions.
@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview April 21, 2026 16:34 Inactive
… coverage

Remove Tab item-roving, add arrow wrapping and input boundary behavior, refine ToolbarSelect Home/End handling, apply disabled-item aria/tabindex updates, and extend Cypress keyboard/a11y tests.
@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview April 24, 2026 07:38 Inactive
@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview May 4, 2026 09:42 Inactive
@plamenivanov91 plamenivanov91 changed the title feat(ui5-toolbar): implement WCAG-compliant keyboard navigation feat(ui5-toolbar): align keyboard navigation with APG and improve nested item delegation May 4, 2026
@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview May 4, 2026 10:57 Inactive
@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview May 4, 2026 18:21 Inactive
@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview May 4, 2026 18:31 Inactive
@ui5-webcomponents-bot ui5-webcomponents-bot temporarily deployed to preview May 4, 2026 18:48 Inactive
* @since 2.22.0
*/
@property({ type: Boolean })
handlesOwnKeyboardNavigation = false;
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.

This is declared as @property({ type: Boolean }) but it behaves like a class-level constant — ToolbarItem and ToolbarSelect set it in their class body and it never changes at runtime. Making it a DOM attribute means it shows up in HTML and consumers can accidentally set it from outside. This should be a plain class field or a getter.

_handleNavigationTarget(target: HTMLElement) {
const hostTarget = this._resolveNavigationHost(target);

if (hostTarget.tagName === "UI5-RADIO-BUTTON") {
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.

Hardcoded tag name checks break when tag names are scoped. Use duck-typing or a property-based check instead, same as the handlesOwnKeyboardNavigation approach we agreed on earlier.

return focusRef ? [focusRef] : [];
}

_storeOriginalTabIndex(target: HTMLElement) {
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.

This only stores if not already present. If a component dynamically changes its tabindex (e.g., becomes disabled/enabled), the stored "original" becomes stale. Consider refreshing on each render cycle.

}

let nextIndex = currentIndex;
const movingForward = (isRight(e) && !isRTL) || (isLeft(e) && isRTL) || isDown(e);
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.

IMO, ArrowUp/Down should not be used for toolbar navigation. They conflict with too many controls — selects, comboboxes, date pickers, anything with a dropdown. Only ArrowLeft/Right should move between toolbar items. This removes a whole class of delegation issues.


if (!radio.disabled && !radio.readonly && !radio.checked) {
if (radio.name) {
RadioButtonGroup.selectItem(radio as unknown as RadioButton, radio.name);
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.

This couples too tightly a generic container to a specific control's internals. A better approach would be to dispatch a synthetic arrow-key event to the radio button and let it handle its own selection, or have selection happen programatically with user pressing Space/Enter. We can discuss this with RadioButton owners

@dobrinyonkov
Copy link
Copy Markdown
Contributor

dobrinyonkov commented May 7, 2026

Tested the latest version. Found some more issues:

1. ArrowUp/Down steals focus from single-child ToolbarItem controls

Focus the ComboBox or DatePicker and press ArrowDown — instead of opening the dropdown/picker, the focus moves to the next toolbar item. Same with ArrowUp moving to the previous item.

IMO ArrowUp/Down as navigation keys bring too many issues — they conflict with dropdowns, selects, comboboxes, date pickers, and any control that uses vertical arrows for its own interaction. The toolbar should only use ArrowLeft/Right for item-to-item navigation. This aligns with how most toolbar implementations work and avoids the delegation complexity that causes issues 1–4.

2. Tab from DatePicker, then Shift+Tab skips the toolbar entirely

Steps:

  1. Focus the DatePicker
  2. Press Tab — focus leaves the toolbar (correct)
  3. Press Shift+Tab — focus skips the toolbar and lands on the element before it

The problem is that _onfocusin cannot match the DatePicker's deep active element back to the focusable items list. _lastFocusedItem is not updated and _applyRovingTabIndex leaves no element with tabindex="0" in the toolbar. The browser finds nothing focusable and skips the toolbar.

3. Disabled button traps navigation

If a button becomes disabled asynchronously/after the first rerender, arrow navigation gets stuck before or after it - you can't pass through it. The disabled item is not being skipped during navigation after state changes.

4. Last focused item becomes disabled - toolbar gets skipped

If the last focused element in a toolbar gets disabled after focus has left the toolbar, Tab/Shift+Tab skips the entire toolbar. The roving tabindex is left on an element that is now disabled and unfocusable, and no fallback is applied.

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.

4 participants