Skip to content

feat: support focusable disabled collection items via allowFocusWhenDisabled#10278

Open
Wonchang0314 wants to merge 1 commit into
adobe:mainfrom
Wonchang0314:feat/focusable-disabled-collection-items
Open

feat: support focusable disabled collection items via allowFocusWhenDisabled#10278
Wonchang0314 wants to merge 1 commit into
adobe:mainfrom
Wonchang0314:feat/focusable-disabled-collection-items

Conversation

@Wonchang0314

@Wonchang0314 Wonchang0314 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Closes #10276

📝 Summary

MenuItem, ListBoxItem, Tab, and TreeItem could not implement the ARIA APG Focusability of disabled controls pattern. A disabled collection item was stuck between two behaviors:

disabledBehavior focusable / navigable aria-disabled
'all' (default) ❌ removed from nav
'selection'

Neither produces a focusable item that is also marked aria-disabled (unlike Button, which decouples aria-disabled from focusability via allowFocusWhenDisabled).

This PR adds an opt-in, per-item allowFocusWhenDisabled prop to these four components. The item stays fully non-interactive (no selection, no action, aria-disabled still emitted) but remains reachable via roving tabindex / arrow-key navigation.

🔧 Approach

A single isDisabled(key) currently drives three concerns at once: aria-disabled, selection/action blocking, and removal from keyboard navigation. To support focusable-disabled items without changing any of that existing behavior, this PR leaves isDisabled untouched and adds one independent axis that only controls focusability:

  • react-stately: new optional MultipleSelectionManager.isFocusableWhenDisabled(key), backed by the new item prop.
  • useSelectableItem: keeps the roving tabindex/focus wiring for these items; selection/action stay blocked and aria-disabled stays set. (Native <a href> navigation is already prevented by the existing link onClick, so no extra handling is needed.)
  • List/Grid/Tabs keyboard delegates & useTab: stop skipping / un-tabbing focusable-disabled items.
  • react-aria-components: expose allowFocusWhenDisabled on MenuItem, ListBoxItem, Tab, TreeItem.

The decoupling is inspired by base-ui's focusableWhenDisabled and this library's own Button allowFocusWhenDisabled. Unlike base-ui, which makes composite items focusable-when-disabled by default, this is strictly opt-in so existing apps relying on disabled items being skipped are unaffected. (Type-to-select also reaches these items, consistent with them being focusable.)

✅ Pull Request Checklist:

📝 Test Instructions:

Automated (all green):

yarn jest packages/react-aria-components/test/{Menu,ListBox,Tabs,Tree}.test.*

Each component has new tests (covering both the item-level isDisabled prop and collection-level disabledKeys paths) asserting that an item with allowFocusWhenDisabled: (1) keeps aria-disabled="true", (2) is reachable via arrow-key navigation (not skipped), (3) cannot be selected or actioned.

Manual:

<Menu aria-label="Actions" disabledKeys={['rename']}>
  <MenuItem id="open">Open</MenuItem>
  <MenuItem id="rename" allowFocusWhenDisabled>Rename</MenuItem>
  <MenuItem id="delete">Delete</MenuItem>
</Menu>

Arrow-key down should land focus on "Rename"; a screen reader should announce it as disabled; pressing Enter should do nothing.

🧢 Your Project:

Community contribution

…isabled

MenuItem, ListBoxItem, Tab, and TreeItem previously could not implement the
ARIA APG "focusability of disabled controls" pattern: a disabled item either
got `aria-disabled` but was removed from keyboard navigation (disabledBehavior
'all'), or stayed navigable but dropped `aria-disabled` (disabledBehavior
'selection'). Neither produced a focusable item that is also marked
`aria-disabled`. Closes adobe#10276.

Add an opt-in per-item `allowFocusWhenDisabled` prop that decouples focusability
from the existing disabled semantics. The item stays non-interactive (no
selection or action, `aria-disabled` is still emitted) but remains reachable via
roving tabindex / arrow-key navigation. `isDisabled` is intentionally left
unchanged so `aria-disabled` emission and selection/action suppression keep
working; only a new orthogonal focusability axis is introduced.

- react-stately: add `MultipleSelectionManager.isFocusableWhenDisabled` (optional
  on the interface for external-implementer compat) backed by the item prop.
- useSelectableItem: keep the roving tabindex/focus wiring and avoid clearing the
  focused key for focusable-disabled items. Native link navigation is already
  blocked by the existing unconditional link onClick preventDefault, since
  pressProps is never merged for a disabled item.
- List/Grid/Tabs keyboard delegates: stop skipping focusable-disabled items.
- useTab: don't null out the roving tabindex for a focusable-disabled tab.
- react-aria-components: expose `allowFocusWhenDisabled` on MenuItem,
  ListBoxItem, Tab, and TreeItem.

Tests cover, per component: aria-disabled stays set, the item is reachable via
keyboard navigation, and selection/action remain blocked.
@Wonchang0314 Wonchang0314 force-pushed the feat/focusable-disabled-collection-items branch from a7cb2a4 to 9dafa04 Compare June 29, 2026 20:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

aria-disabled is not passed to MenuItem/ListBoxItem/Tab/TreeItem

1 participant