Skip to content

UI: Make disabled Buttons keyboard-focusable#34166

Merged
Sidnioulz merged 6 commits into
nextfrom
sidnioulz/issue-31678-aria-disabled-finish
Mar 17, 2026
Merged

UI: Make disabled Buttons keyboard-focusable#34166
Sidnioulz merged 6 commits into
nextfrom
sidnioulz/issue-31678-aria-disabled-finish

Conversation

@Sidnioulz
Copy link
Copy Markdown
Member

@Sidnioulz Sidnioulz commented Mar 16, 2026

Closes #31678

What I did

Finishing up #32325 as it came up in the SB 10.3 QA.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

  • Visit the Button Disabled story
  • Try out the testing widget to verify disabled buttons are disabled
  • In particular, verify you can focus in the "Watch mode" button while tests are running, and the tooltip appears to tell you why it's unavailable

Documentation

ø

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • Bug Fixes

    • Buttons now expose aria-disabled when inactive and prevent clicks when disabled or read-only, improving accessibility and interaction behavior.
  • Tests

    • Updated tests and interactive story checks to assert aria-disabled ('true') instead of relying on the DOM disabled state.
  • Documentation

    • Test guidance updated to show using toHaveAttribute('aria-disabled', 'true') in examples.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Mar 16, 2026

View your CI Pipeline Execution ↗ for commit 609f377

Command Status Duration Result
nx run-many -t compile -c production --parallel=1 ✅ Succeeded 5m 13s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-17 15:36:35 UTC

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 746e98aa-da89-4c38-bb9b-c76eb4f47007

📥 Commits

Reviewing files that changed from the base of the PR and between 92c9867 and 585d934.

📒 Files selected for processing (1)
  • code/e2e-tests/addon-toolbars.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/e2e-tests/addon-toolbars.spec.ts

📝 Walkthrough

Walkthrough

Replaced native disabled checks with ARIA-based checks: Button now uses an internal $disabled prop and sets aria-disabled (and suppresses onClick when disabled/readOnly). Stories, playtests, e2e tests, and docs updated to assert aria-disabled="true" (or its negation) instead of using toBeDisabled().

Changes

Cohort / File(s) Summary
Button implementation
code/core/src/components/components/Button/Button.tsx
Introduced $disabled prop on Button/StyledButton, set aria-disabled when disabled/readOnly, prevent onClick when disabled/readOnly, and updated styling logic to use $disabled.
Button stories
code/core/src/components/components/Button/Button.stories.tsx
Updated Disabled story: added onClick: fn() plus render and play blocks that assert aria-disabled, verify click prevention, and check focusability.
Stories / story-play tests
code/addons/onboarding/src/features/IntentSurvey/IntentSurvey.stories.tsx, code/core/src/component-testing/components/test-fn.stories.tsx, code/renderers/react/template/stories/test-fn.stories.tsx, code/core/src/manager/components/sidebar/TagsFilter.stories.tsx, code/addons/onboarding/...
Replaced toBeDisabled()/enabled checks with toHaveAttribute('aria-disabled','true') or not.toHaveAttribute('aria-disabled','true') across multiple story play/test flows.
E2E / integration tests
code/e2e-tests/addon-toolbars.spec.ts, code/e2e-tests/addon-viewport.spec.ts, test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts, test-storybooks/portable-stories-kitchen-sink/react-vitest-3/e2e-tests/component-testing.spec.ts
Switched assertions from toBeDisabled()/enabled checks to explicit aria-disabled attribute assertions; one selector adjustment noted (label/title usage in a test).
Shared test data / checklist
code/core/src/shared/checklist-store/checklistData.tsx
Replaced toBeDisabled() with toHaveAttribute('aria-disabled','true') in checklist-related tests/data.
Docs
docs/writing-tests/interaction-testing.mdx, docs/api/csf/csf-next.mdx
Replaced examples and documentation entry using toBeDisabled() with examples using toHaveAttribute('aria-disabled','true') and updated sample code accordingly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
📝 Coding Plan
  • Generate coding plan for human review comments

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
code/core/src/components/components/Button/Button.stories.tsx (1)

289-321: Well-structured play function covering key accessibility requirements.

The test steps verify:

  1. aria-disabled attribute is set correctly
  2. Click handler is not invoked when disabled
  3. Button remains focusable for keyboard accessibility

Minor note: Line 316 redeclares button which shadows the outer variable from line 304. This works but is redundant.

♻️ Optional cleanup
     await step('Disabled button should be focusable for accessibility', async () => {
-      const button = canvas.getByRole('button', { name: 'Disabled Button' });
       button.focus();
       expect(button).toHaveFocus();
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/components/components/Button/Button.stories.tsx` around lines
289 - 321, In the Disabled story's play function remove the redundant
redeclaration of the button variable that shadows the outer one: use the
existing const button = canvas.getByRole('button', { name: 'Disabled Button' })
declared earlier in play rather than declaring another button inside the final
step; update the final step to reuse the outer button (and keep the
focus/assertion logic unchanged) in the play function of the Disabled story to
avoid shadowing.
code/core/src/components/components/Button/Button.tsx (1)

263-317: Consider disabling hover/active styles for disabled buttons.

The hover/active/focus-visible styles are applied when !readOnly but don't account for $disabled. Disabled buttons will show hover background changes, though they do have cursor: not-allowed and opacity: 0.5 as visual indicators.

This is likely acceptable since the visual indicators are sufficient, but for consistency with readOnly behavior, you could extend the condition:

♻️ Optional: Disable hover styles for disabled buttons
-    ...(!readOnly && {
+    ...(!readOnly && !$disabled && {
       '&:hover': {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/components/components/Button/Button.tsx` around lines 263 -
317, The hover/active/focus-visible block currently gated only by !readOnly
should also skip when the button is disabled; update the conditional around that
style block in Button.tsx to check both !readOnly and that the disabled
flag/prop (e.g., $disabled or disabled) is false, and ensure the same gating
applies to the '&:hover', '&:active' and '&:focus-visible' entries (and the
'.sb-bar &:focus-visible, .sb-list &:focus-visible' override) so disabled
buttons do not receive hover/active/focus-visible visual changes while keeping
cursor: not-allowed and opacity intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts`:
- Around line 241-242: The assertion for the test button is currently written
without awaiting the Playwright assertion and ends with a comma, so it never
executes; update the assertion that checks watchModeButton to prepend await to
the expect(...) call and replace the trailing comma with a semicolon so the
async assertion runs (i.e., await
expect(watchModeButton).toHaveAttribute("aria-disabled", "true");). Ensure this
change is applied where watchModeButton is asserted in the
component-testing.spec.ts test.

---

Nitpick comments:
In `@code/core/src/components/components/Button/Button.stories.tsx`:
- Around line 289-321: In the Disabled story's play function remove the
redundant redeclaration of the button variable that shadows the outer one: use
the existing const button = canvas.getByRole('button', { name: 'Disabled Button'
}) declared earlier in play rather than declaring another button inside the
final step; update the final step to reuse the outer button (and keep the
focus/assertion logic unchanged) in the play function of the Disabled story to
avoid shadowing.

In `@code/core/src/components/components/Button/Button.tsx`:
- Around line 263-317: The hover/active/focus-visible block currently gated only
by !readOnly should also skip when the button is disabled; update the
conditional around that style block in Button.tsx to check both !readOnly and
that the disabled flag/prop (e.g., $disabled or disabled) is false, and ensure
the same gating applies to the '&:hover', '&:active' and '&:focus-visible'
entries (and the '.sb-bar &:focus-visible, .sb-list &:focus-visible' override)
so disabled buttons do not receive hover/active/focus-visible visual changes
while keeping cursor: not-allowed and opacity intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fbd3405a-234b-4fb2-93cd-baaf1251aafc

📥 Commits

Reviewing files that changed from the base of the PR and between 546aece and cff53d2.

📒 Files selected for processing (6)
  • code/addons/onboarding/src/features/IntentSurvey/IntentSurvey.stories.tsx
  • code/core/src/components/components/Button/Button.stories.tsx
  • code/core/src/components/components/Button/Button.tsx
  • code/e2e-tests/addon-toolbars.spec.ts
  • code/e2e-tests/addon-viewport.spec.ts
  • test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test-storybooks/portable-stories-kitchen-sink/react-vitest-3/e2e-tests/component-testing.spec.ts (1)

47-54: ⚠️ Potential issue | 🟠 Major

Harden restore flow to avoid cross-test contamination on partial restore failure.

If fs.writeFile fails for one entry (Line 49), the loop aborts and modifiedFiles.clear() never runs (Line 51). That can leak modified files into subsequent tests.

Suggested fix
 const restoreAllFiles = async () => {
-  for (const [filePath, originalContent] of modifiedFiles.entries()) {
-    await fs.writeFile(filePath, originalContent);
-  }
-  modifiedFiles.clear();
+  const errors: Array<{ filePath: string; error: unknown }> = [];
+  try {
+    for (const [filePath, originalContent] of modifiedFiles.entries()) {
+      try {
+        await fs.writeFile(filePath, originalContent);
+      } catch (error) {
+        errors.push({ filePath, error });
+      }
+    }
+  } finally {
+    modifiedFiles.clear();
+  }
+
+  if (errors.length) {
+    throw new Error(`Failed to restore ${errors.length} modified file(s)`);
+  }
   // the file change causes a HMR event, which causes a browser reload,and that can take a few seconds
   await new Promise((resolve) => setTimeout(resolve, 2000));
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test-storybooks/portable-stories-kitchen-sink/react-vitest-3/e2e-tests/component-testing.spec.ts`
around lines 47 - 54, The restoreAllFiles function can abort on a single
fs.writeFile failure and never clear modifiedFiles, leaking state; change its
implementation to attempt writing all entries even if some fail (either by
iterating with per-file try/catch or using Promise.allSettled over write
promises), collect or aggregate any errors, and ensure modifiedFiles.clear()
always runs (put clear and the HMR delay in a finally block) so subsequent tests
aren’t contaminated; if writes failed, surface an aggregated error or log the
failures after clearing the map.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@test-storybooks/portable-stories-kitchen-sink/react-vitest-3/e2e-tests/component-testing.spec.ts`:
- Around line 138-144: The click that expands the story isn’t awaited, causing
race conditions; update the expansion step by awaiting the interaction on the
testStoryElement (i.e., replace the unawaited testStoryElement.click() with an
awaited interaction) and optionally ensure the element’s expanded state by
awaiting getAttribute('aria-expanded') or using a wait-for condition after the
awaited click to guarantee expansion before proceeding.

---

Outside diff comments:
In
`@test-storybooks/portable-stories-kitchen-sink/react-vitest-3/e2e-tests/component-testing.spec.ts`:
- Around line 47-54: The restoreAllFiles function can abort on a single
fs.writeFile failure and never clear modifiedFiles, leaking state; change its
implementation to attempt writing all entries even if some fail (either by
iterating with per-file try/catch or using Promise.allSettled over write
promises), collect or aggregate any errors, and ensure modifiedFiles.clear()
always runs (put clear and the HMR delay in a finally block) so subsequent tests
aren’t contaminated; if writes failed, surface an aggregated error or log the
failures after clearing the map.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97208389-92f5-46c1-ae4d-726939741f8f

📥 Commits

Reviewing files that changed from the base of the PR and between cff53d2 and a604b3d.

📒 Files selected for processing (2)
  • code/core/src/manager/components/sidebar/TagsFilter.stories.tsx
  • test-storybooks/portable-stories-kitchen-sink/react-vitest-3/e2e-tests/component-testing.spec.ts

Comment on lines 138 to 144
const testStoryElement = await page.getByRole('button', {
name: 'Test',
exact: true,
});
if ((await testStoryElement.getAttribute("aria-expanded")) !== "true") {
if ((await testStoryElement.getAttribute('aria-expanded')) !== 'true') {
testStoryElement.click();
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify un-awaited Playwright action calls in spec files.
rg -nP --type=ts -C2 '(?<!await\s)\b\w+\.(click|check|uncheck|fill|hover|press)\(' -g '*.spec.ts'

Repository: storybookjs/storybook

Length of output: 9135


🏁 Script executed:

sed -n '130,150p' test-storybooks/portable-stories-kitchen-sink/react-vitest-3/e2e-tests/component-testing.spec.ts

Repository: storybookjs/storybook

Length of output: 880


Await the expansion click to prevent flaky ordering.

Line 144: testStoryElement.click() is not awaited. The next operations can run before the expansion completes.

Suggested fix
    if ((await testStoryElement.getAttribute('aria-expanded')) !== 'true') {
-      testStoryElement.click();
+      await testStoryElement.click();
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const testStoryElement = await page.getByRole('button', {
name: 'Test',
exact: true,
});
if ((await testStoryElement.getAttribute("aria-expanded")) !== "true") {
if ((await testStoryElement.getAttribute('aria-expanded')) !== 'true') {
testStoryElement.click();
}
const testStoryElement = await page.getByRole('button', {
name: 'Test',
exact: true,
});
if ((await testStoryElement.getAttribute('aria-expanded')) !== 'true') {
await testStoryElement.click();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test-storybooks/portable-stories-kitchen-sink/react-vitest-3/e2e-tests/component-testing.spec.ts`
around lines 138 - 144, The click that expands the story isn’t awaited, causing
race conditions; update the expansion step by awaiting the interaction on the
testStoryElement (i.e., replace the unawaited testStoryElement.click() with an
awaited interaction) and optionally ensure the element’s expanded state by
awaiting getAttribute('aria-expanded') or using a wait-for condition after the
awaited click to guarantee expansion before proceeding.

@Sidnioulz Sidnioulz force-pushed the sidnioulz/issue-31678-aria-disabled-finish branch from a604b3d to 92c9867 Compare March 17, 2026 10:47
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts (1)

241-242: ⚠️ Potential issue | 🔴 Critical

Await this assertion and remove the comma operator.

Line 242 is currently not awaited and is comma-chained, so the disabled-state check can be bypassed/flaky.

Suggested patch
-    // The test button will be disabled as tests are running
-    expect(watchModeButton).toHaveAttribute("aria-disabled", "true"),
+    // The test button will be disabled as tests are running
+    await expect(watchModeButton).toHaveAttribute("aria-disabled", "true");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts`
around lines 241 - 242, The assertion for the disabled state is currently
comma-chained and not awaited, which can make the check flaky; change the
expression so you await the assertion on watchModeButton (await
expect(watchModeButton).toHaveAttribute("aria-disabled","true")) and remove the
trailing comma operator so the test actually waits for the attribute check to
complete.
🧹 Nitpick comments (1)
code/renderers/react/template/stories/test-fn.stories.tsx (1)

81-82: Use toBeEnabled() for this native button test path.

Line 81 and Line 91 only check that aria-disabled is not "true", which can still pass if the native disabled attribute is set. Since this file uses a plain <button>, toBeEnabled() is the stronger assertion.

Suggested patch
-    await expect(button).not.toHaveAttribute('aria-disabled', 'true');
+    await expect(button).toBeEnabled();
...
-  await expect(button).not.toHaveAttribute('aria-disabled', 'true');
+  await expect(button).toBeEnabled();

Also applies to: 91-92

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/renderers/react/template/stories/test-fn.stories.tsx` around lines 81 -
82, Replace the weak attribute assertion with the stronger DOM-enabled assertion
in the test: change the two occurrences where the test does await
expect(button).not.toHaveAttribute('aria-disabled', 'true') to await
expect(button).toBeEnabled() so the plain <button> is verified by its actual
enabled/disabled state; update both occurrences in test-fn.stories.tsx (the
assertions referencing the button element) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/core/src/component-testing/components/test-fn.stories.tsx`:
- Around line 99-100: Replace the three assertions that check aria-disabled on
the native <button> element with the proper enabled-state matcher: locate the
assertions using the button variable (the lines containing await
expect(button).not.toHaveAttribute('aria-disabled', 'true')) and change them to
use await expect(button).toBeEnabled() so the test verifies the native disabled
behavior (respecting the disabled attribute and disabled fieldset ancestors)
rather than the aria-disabled attribute.

---

Duplicate comments:
In
`@test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts`:
- Around line 241-242: The assertion for the disabled state is currently
comma-chained and not awaited, which can make the check flaky; change the
expression so you await the assertion on watchModeButton (await
expect(watchModeButton).toHaveAttribute("aria-disabled","true")) and remove the
trailing comma operator so the test actually waits for the attribute check to
complete.

---

Nitpick comments:
In `@code/renderers/react/template/stories/test-fn.stories.tsx`:
- Around line 81-82: Replace the weak attribute assertion with the stronger
DOM-enabled assertion in the test: change the two occurrences where the test
does await expect(button).not.toHaveAttribute('aria-disabled', 'true') to await
expect(button).toBeEnabled() so the plain <button> is verified by its actual
enabled/disabled state; update both occurrences in test-fn.stories.tsx (the
assertions referencing the button element) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c45a6947-e854-4cf4-919e-9669f81a49cf

📥 Commits

Reviewing files that changed from the base of the PR and between a604b3d and 92c9867.

📒 Files selected for processing (8)
  • code/core/src/component-testing/components/test-fn.stories.tsx
  • code/core/src/manager/components/sidebar/TagsFilter.stories.tsx
  • code/core/src/shared/checklist-store/checklistData.tsx
  • code/renderers/react/template/stories/test-fn.stories.tsx
  • docs/api/csf/csf-next.mdx
  • docs/writing-tests/interaction-testing.mdx
  • test-storybooks/portable-stories-kitchen-sink/react-vitest-3/e2e-tests/component-testing.spec.ts
  • test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts

Comment on lines +99 to 100
await expect(button).not.toHaveAttribute('aria-disabled', 'true');
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the target file
cat -n code/core/src/component-testing/components/test-fn.stories.tsx | head -130 | tail -40

Repository: storybookjs/storybook

Length of output: 1609


🏁 Script executed:

# Read the beginning of the file to understand the component being tested
head -90 code/core/src/component-testing/components/test-fn.stories.tsx | tail -50

Repository: storybookjs/storybook

Length of output: 1293


🏁 Script executed:

# Read from the very top of the file to find imports and component definitions
head -60 code/core/src/component-testing/components/test-fn.stories.tsx

Repository: storybookjs/storybook

Length of output: 1474


🏁 Script executed:

# Check the test framework setup - look for jest-dom or testing-library matchers
grep -r "toBeEnabled\|jest-dom\|@testing-library" code/core/src/component-testing --include="*.ts" --include="*.tsx" -A 2 -B 2

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

# Check what's exported from storybook/test
find code -name "*.ts" -o -name "*.tsx" | xargs grep -l "export.*expect\|from.*storybook/test" | head -5

Repository: storybookjs/storybook

Length of output: 360


🏁 Script executed:

# Find the storybook/test export
find code -path "*storybook/test*" -type f | head -10

Repository: storybookjs/storybook

Length of output: 153


🏁 Script executed:

# Search for storybook/test definition
rg "export.*expect|vitest|@vitest" code --type ts --type tsx -A 3 | head -50

Repository: storybookjs/storybook

Length of output: 92


🏁 Script executed:

# Search for storybook/test setup
rg "export.*expect" code -g "*.ts" -g "*.tsx" -A 3 | head -80

Repository: storybookjs/storybook

Length of output: 691


🏁 Script executed:

# Check the test/index.ts to see what rawExpect is
cat code/core/src/test/index.ts

Repository: storybookjs/storybook

Length of output: 1658


🏁 Script executed:

# Check expect.ts to see what's exported
cat code/core/src/test/expect.ts

Repository: storybookjs/storybook

Length of output: 4374


🏁 Script executed:

# Verify toBeEnabled is part of `@testing-library/jest-dom` matchers
grep -r "toBeEnabled" code --include="*.ts" --include="*.tsx" -B 2 -A 2 | head -40

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

# Check package.json for `@testing-library/jest-dom` version
grep -r "@testing-library/jest-dom" code --include="package.json"

Repository: storybookjs/storybook

Length of output: 191


🌐 Web query:

@testing-library/jest-dom 6.9.1 toBeEnabled matcher

💡 Result:

In @testing-library/jest-dom v6.9.1, toBeEnabled() is available and is defined as the logical inverse of toBeDisabled() (i.e. it’s equivalent to expect(element).not.toBeDisabled() but avoids double-negation). It considers an element disabled when it’s a disable-able form control with the disabled attribute or it’s inside a disabled ancestor that can disable descendants (e.g. a <fieldset disabled>), and therefore considers it enabled when those conditions are not true. It does not take aria-disabled into account. [1]

Example:

import '@testing-library/jest-dom'

expect(screen.getByRole('button')).toBeEnabled()

The 6.9.1 release itself is a small bugfix release (unrelated to toBeEnabled behavior). [2]

Sources: [1] [2]


Replace aria-disabled assertions with toBeEnabled() for native buttons.

Lines 99, 109, and 123 check not.toHaveAttribute('aria-disabled', 'true'), but for native <button> elements, the actual enabled state is determined by the native disabled attribute or disabled fieldset ancestor, not the aria-disabled accessibility attribute. Use toBeEnabled() instead to verify the button's actual enabled behavior.

Suggested patch
-    await expect(button).not.toHaveAttribute('aria-disabled', 'true');
+    await expect(button).toBeEnabled();
...
-    await expect(button).not.toHaveAttribute('aria-disabled', 'true');
+    await expect(button).toBeEnabled();
...
-    await expect(button).not.toHaveAttribute('aria-disabled', 'true');
+    await expect(button).toBeEnabled();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await expect(button).not.toHaveAttribute('aria-disabled', 'true');
}
await expect(button).toBeEnabled();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/component-testing/components/test-fn.stories.tsx` around lines
99 - 100, Replace the three assertions that check aria-disabled on the native
<button> element with the proper enabled-state matcher: locate the assertions
using the button variable (the lines containing await
expect(button).not.toHaveAttribute('aria-disabled', 'true')) and change them to
use await expect(button).toBeEnabled() so the test verifies the native disabled
behavior (respecting the disabled attribute and disabled fieldset ancestors)
rather than the aria-disabled attribute.

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.

[Bug]: Buttons should be aria-disabled instead of disabled

4 participants