Skip to content

fix(Toolbar): forward disabled to ToolbarButton + reactive focusable count#2735

Merged
zernonia merged 1 commit into
unovue:v2from
yan-ad:fix/toolbar-toggle-item-disabled-focusable
Jun 20, 2026
Merged

fix(Toolbar): forward disabled to ToolbarButton + reactive focusable count#2735
zernonia merged 1 commit into
unovue:v2from
yan-ad:fix/toolbar-toggle-item-disabled-focusable

Conversation

@yan-ad

@yan-ad yan-ad commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🔗 Resolves #2728

Type of change

  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)

📚 Description

When all ToolbarToggleItem children are disabled, the ToolbarRoot still receives tabindex="0" and remains reachable via keyboard Tab navigation.

Root cause: ToolbarToggleItem was not forwarding its disabled prop to ToolbarButton, so RovingFocusItem always received focusable=true regardless of the item's disabled state.

Additionally: RovingFocusItem only updated focusableItemsCount on mount/unmount — dynamic disabled changes after mount were not reflected.

Fixes:

  1. ToolbarToggleItem now forwards disabled prop to ToolbarButton
  2. RovingFocusItem now watches focusable prop and updates focusableItemsCount reactively
  3. Added tests for all-disabled and mixed-disabled toolbar scenarios

Summary by CodeRabbit

  • Bug Fixes

    • Fixed roving focus items to dynamically add/remove from the focus group when their focusable state changes.
    • Improved toolbar accessibility with correct tabindex management based on toggle item disabled states.
  • Tests

    • Added test coverage for toolbar tabindex behavior with disabled toggle items.

…count

ToolbarToggleItem was not passing its disabled prop to ToolbarButton, causing
RovingFocusItem to always register as focusable. This made ToolbarRoot
tabbable even when all toggle items were disabled.

Additionally, RovingFocusItem only updated focusableItemsCount on mount/unmount,
so dynamic disabled changes were not reflected reactively.

Fixes:
- ToolbarToggleItem forwards disabled to ToolbarButton
- RovingFocusItem watches focusable prop and updates count reactively
- Added tests for all-disabled and mixed-disabled toolbar scenarios

Closes unovue#2728
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

ToolbarToggleItem now forwards its disabled prop to ToolbarButton. RovingFocusItem adds a watch on props.focusable that reactively calls onFocusableItemAdd/onFocusableItemRemove when the value changes post-mount. Two integration tests verify the toolbar root's tabindex reflects the disabled state of all toggle items.

Changes

Disabled ToolbarToggleItem tabindex fix

Layer / File(s) Summary
Disabled prop forwarding and reactive focusable watcher
packages/core/src/Toolbar/ToolbarToggleItem.vue, packages/core/src/RovingFocus/RovingFocusItem.vue
ToolbarToggleItem adds :disabled="props.disabled" to ToolbarButton, ensuring RovingFocusItem receives focusable=false when disabled. RovingFocusItem imports watch and adds a watcher on props.focusable that calls onFocusableItemAdd or onFocusableItemRemove reactively, guarded against no-op transitions.
Integration tests for tabindex behavior
packages/core/src/Toolbar/Toolbar.test.ts
Adds a describe block with two test cases mounting a composed ToolbarRoot/ToolbarToggleGroup/ToolbarToggleItem tree via inline defineComponent, asserting tabindex="-1" when all items are disabled and tabindex="0" when at least one is enabled after nextTick().

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A toolbar once tab-stopped in shame,
Its disabled buttons all looked the same.
A watcher was placed, a prop was passed down,
The focusable count no longer wore a frown.
Now tabindex shifts as disabled items say "nay,"
And the rabbit hops on — all is fixed today! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main fixes: forwarding the disabled prop to ToolbarButton and making the focusable count reactive in RovingFocusItem.
Linked Issues check ✅ Passed All code requirements from issue #2728 are met: ToolbarToggleItem forwards disabled to ToolbarButton, RovingFocusItem reactively watches focusable changes, and tests verify disabled toggle items don't make toolbar tabbable.
Out of Scope Changes check ✅ Passed All changes directly address issue #2728: prop forwarding in ToolbarToggleItem, reactive watching in RovingFocusItem, and related test coverage. No out-of-scope modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented Jun 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/reka-ui@2735

commit: 81a7b82

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
packages/core/src/Toolbar/Toolbar.test.ts (1)

32-96: ⚡ Quick win

Consider adding a test for dynamic disabled state changes.

The current tests verify the static/initial disabled state, which covers the primary bug. To fully validate the reactive watcher added to RovingFocusItem, consider adding a test that:

  1. Mounts a toolbar with enabled items
  2. Programmatically changes items to disabled after mount
  3. Verifies the toolbar root's tabindex updates to "-1" reactively

This would increase confidence that the watcher correctly handles runtime state changes.

🧪 Example test structure
it('should update tabindex reactively when items are disabled after mount', async () => {
  const TestComponent = defineComponent({
    components: { ToolbarRoot, ToolbarToggleGroup, ToolbarToggleItem },
    setup() {
      const model = ref([])
      const allDisabled = ref(false)
      return { model, allDisabled }
    },
    template: `
      <ToolbarRoot aria-label="Test toolbar">
        <ToolbarToggleGroup v-model="model" type="multiple">
          <ToolbarToggleItem value="bold" :disabled="allDisabled">Bold</ToolbarToggleItem>
        </ToolbarToggleGroup>
      </ToolbarRoot>
    `,
  })

  const wrapper = mount(TestComponent, { attachTo: document.body })
  await nextTick()
  
  const root = wrapper.find('[role="toolbar"]')
  expect(root.attributes('tabindex')).toBe('0')
  
  // Dynamically disable
  await wrapper.vm.allDisabled = true
  await nextTick()
  expect(root.attributes('tabindex')).toBe('-1')
  
  wrapper.unmount()
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/Toolbar/Toolbar.test.ts` around lines 32 - 96, Add a new
test case after the existing tests in the describe block that validates reactive
disabled state changes. Create a TestComponent with a reactive ref (like
allDisabled) controlling the disabled property of ToolbarToggleItem, mount it
with an enabled item initially, verify the toolbar root has tabindex of '0',
then programmatically set the disabled state to true and await nextTick, and
finally verify the tabindex reactively updates to '-1' to confirm the watcher in
RovingFocusItem properly handles runtime state changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/core/src/Toolbar/Toolbar.test.ts`:
- Around line 32-96: Add a new test case after the existing tests in the
describe block that validates reactive disabled state changes. Create a
TestComponent with a reactive ref (like allDisabled) controlling the disabled
property of ToolbarToggleItem, mount it with an enabled item initially, verify
the toolbar root has tabindex of '0', then programmatically set the disabled
state to true and await nextTick, and finally verify the tabindex reactively
updates to '-1' to confirm the watcher in RovingFocusItem properly handles
runtime state changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e6fcd8bc-525d-4659-a78f-3f934f07a633

📥 Commits

Reviewing files that changed from the base of the PR and between c4678e1 and 81a7b82.

📒 Files selected for processing (3)
  • packages/core/src/RovingFocus/RovingFocusItem.vue
  • packages/core/src/Toolbar/Toolbar.test.ts
  • packages/core/src/Toolbar/ToolbarToggleItem.vue

@zernonia zernonia merged commit b8e383c into unovue:v2 Jun 20, 2026
7 checks passed
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