Skip to content

Conversation

@varun-srinivasa
Copy link
Contributor

@varun-srinivasa varun-srinivasa commented Sep 15, 2025

💡 What is the current behavior?

GitHub Issue Number: #
Jira issue number: IX-3051

🆕 What is the new behavior?

  • Added new focus behavior as per figma design

🏁 Checklist

A pull request can only be merged if all of these conditions are met (where applicable):

  • 🦮 Accessibility (a11y) features were implemented
  • 🗺️ Internationalization (i18n) - no hard coded strings
  • 📲 Responsiveness - components handle viewport changes and content overflow gracefully
  • 📕 Add or update a Storybook story
  • 📄 Documentation was reviewed/updated siemens/ix-docs
  • 🧪 Unit tests were added/updated and pass (pnpm test)
  • 📸 Visual regression tests were added/updated and pass (Guide)
  • 🧐 Static code analysis passes (pnpm lint)
  • 🏗️ Successful compilation (pnpm build, changes pushed)

👨‍💻 Help & support

@changeset-bot
Copy link

changeset-bot bot commented Sep 15, 2025

⚠️ No Changeset found

Latest commit: b487a16

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@varun-srinivasa varun-srinivasa marked this pull request as ready for review September 25, 2025 05:05
if (index < 0) return;
const items = this.dropdownItems;
items.forEach((item, i) =>
item.setAttribute('tabindex', i === index ? '0' : '-1')
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need to change the tabIndex?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When the dropdown is closed by pressing the Tab key, we set the tabindex of all dropdown items to -1. This removes them from the natural tab order of the page. It ensures that a user cannot accidentally Tab into the items of a closed dropdown. Focus should move to the next focusable element outside of the split-button component, which is the standard and expected keyboard navigation behavior.

if (event.key !== 'Tab') {
return;
}
this.dropdownItems.forEach((item) => item.setAttribute('tabindex', '-1'));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need to set the tabIndex?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When Shift+Tab is pressed, the browser's default behavior is to move focus to the previous focusable element, which in this case is the split-button's dropdown trigger (the anchorButton).
However, the desired behavior is to close the dropdown and move focus to the element before the entire split-button component.
To achieve this, we temporarily set tabindex="-1" on the anchorButton, making it non-focusable. This allows the browser's default Shift+Tab action to skip the anchorButton and move focus to the correct preceding element.
The tabindex is immediately restored within a requestAnimationFrame callback so that the anchorButton is focusable again for future interactions.

return;
}
this.dropdownItems.forEach((item) => item.setAttribute('tabindex', '-1'));
this.dropdownElement!.show = false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this necessary?

Comment on lines 139 to 143
const actionButton = this.hostElement.shadowRoot?.querySelector(
'ix-button, ix-icon-button:not(.anchor)'
) as HTMLElement | null;

const anchorButton = this.hostElement.shadowRoot?.querySelector(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Getter would be better IMO.

@nuke-ellington
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces keyboard navigation for the split button's dropdown, which is a great enhancement for accessibility. The implementation uses an ArrowFocusController for arrow key navigation and a custom keydown handler for the Tab key. I've found a critical issue with the usage of ArrowFocusController that prevents the arrow key navigation from working, and another potential runtime error if the dropdown is empty. I've left detailed comments with suggestions on how to fix them. The new tests are well-structured and will be very helpful in verifying the fixes.

Comment on lines +115 to +119
this.arrowFocusController = new ArrowFocusController(
this.dropdownItems,
this.dropdownElement!,
(index: number) => this.focusDropdownItem(index)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The ArrowFocusController will not work as expected here. Its getActiveIndex method compares document.activeElement directly with the items in the list. However, your focusDropdownItem function focuses an inner <button> element inside the ix-dropdown-item's shadow DOM. This means document.activeElement will be this inner button, not the ix-dropdown-item element itself. As a result, getActiveIndex will always return -1, breaking the arrow key navigation.

To fix this, ArrowFocusController needs to be updated to correctly find the active item, for example by checking if an item contains(document.activeElement). Since utils/focus.ts is not part of this PR, you might need to include it or find a workaround.

The new tests in split-button-keyboard.ct.ts are also likely to fail because they expect the ix-dropdown-item host to be focused (ix-dropdown-item:focus), which is not what happens.

Comment on lines 169 to 172
const item = items[index];
requestAnimationFrame(() => {
item.shadowRoot?.querySelector('button')?.focus();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The code attempts to access item.shadowRoot without checking if item is defined. If the split button has no dropdown items, this.dropdownItems will be empty, and items[index] will be undefined. This will cause a runtime error on item.shadowRoot. Please add a guard to ensure item exists before using it.

    const item = items[index];
    if (item) {
      requestAnimationFrame(() => {
        item.shadowRoot?.querySelector('button')?.focus();
      });
    }

Copy link
Collaborator

@nuke-ellington nuke-ellington left a comment

Choose a reason for hiding this comment

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

Although not part of the Figma spec, ArrowDown/Up should also open the dropdown (compare WCAG).

@sonarqubecloud
Copy link

@danielleroux
Copy link
Collaborator

PR will be closed regarding general refactoring of keyboard navigation #2268

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.

3 participants