Skip to content

fix(addon-docs): improve object control JSON editor accessibility#34108

Open
anchmelev wants to merge 2 commits intostorybookjs:nextfrom
anchmelev:fix/issue-24150-object-control-json-a11y
Open

fix(addon-docs): improve object control JSON editor accessibility#34108
anchmelev wants to merge 2 commits intostorybookjs:nextfrom
anchmelev:fix/issue-24150-object-control-json-a11y

Conversation

@anchmelev
Copy link

@anchmelev anchmelev commented Mar 11, 2026

Part of #24150

What I did

Improved accessibility of the Object control JSON editor in docs controls:

  • Added an explicit sr-only label for the raw JSON textarea.
  • Added aria-invalid and conditional aria-describedby for parse errors.
  • Added a polite live region (role="status", aria-live="polite") to announce JSON parse errors.
  • Added ariaDescription to the “Edit as JSON” toggle button.
  • Added unit tests for the new accessibility behavior.

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

  1. Run Storybook UI for this repo (cd code && yarn storybook:ui).
  2. Open the Object control story (e.g. .../blocks-controls-object--object).
  3. Click the “Edit object as JSON” toggle button.
  4. Enter invalid JSON (e.g. {"label":) and blur the textarea.
  5. Verify an error message appears and is announced as a live status update.
  6. Verify textarea has invalid state (aria-invalid=true) and references the error via aria-describedby.
  7. Enter valid JSON (e.g. {"label":"updated"}) and blur again.
  8. Verify the error message disappears and invalid/error associations are cleared.

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Summary by CodeRabbit

  • Accessibility
    • Improved error messaging and ARIA associations for raw JSON editing; error states reset when toggling the raw editor.
    • Added hidden live status and aria-busy support to the interactions list, with clearer status announcements for running/errored states.
  • New Features
    • Interaction items and nested steps now expose clearer labels and expand/collapse semantics.
  • Tests
    • Added test coverage for object control accessibility and for interactions panel behavior and announcements.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

Adds accessibility and error UI for the ObjectControl raw JSON editor, includes tests for ARIA behavior and error handling, and updates interaction/interaction-panel components to improve accessible labeling, status announcements, and navigation state handling.

Changes

Cohort / File(s) Summary
ObjectControl (implementation & tests)
code/addons/docs/src/blocks/controls/Object.tsx, code/addons/docs/src/blocks/controls/Object.test.tsx
Adds RawInputWrapper and ErrorMessage UI around the raw JSON editor, generates controlId/jsonErrorId, applies aria-invalid and aria-describedby, resets parse errors on mode/content changes, expands toggle button ARIA description. Adds tests validating aria-describedby, aria-invalid, error rendering, prevention of onChange for invalid JSON, and onChange for valid JSON.
Interaction component
code/core/src/component-testing/components/Interaction.tsx
Refactors header and icon usage: swaps chevrons, converts container to li, renames isInteractiveisNavigationDisabled, updates hover/disable logic, and introduces getInteractionLabel/getInteractionStatusText with status-to-text mapping used in ARIA labels.
InteractionsPanel (implementation & tests)
code/core/src/component-testing/components/InteractionsPanel.tsx, code/core/src/component-testing/components/InteractionsPanel.test.tsx
Wraps interaction list in an accessible section with hidden heading and live status, uses useId() for labeling, adds aria-busy control and status announcement text for rendering/playing/errored states, and introduces tests verifying list structure, nested-step toggles, run-status announcements, and error alerts.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs


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.

Copy link
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

🤖 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/addons/docs/src/blocks/controls/Object.tsx`:
- Around line 233-257: parseError persists across remounts of the uncontrolled
RawInput, so when the textarea is rebuilt from jsonString the old error state
stays shown; fix this by clearing the parseError when the raw editor mounts or
when jsonString/forceVisible changes. Add a useEffect in the component that
contains rawJSONForm (or in the same component using parseError state) that
calls setParseError(undefined) with a dependency on jsonString and/or
forceVisible (or runs once on mount) so that when RawInput (key={jsonString}) is
remounted the aria-invalid and ErrorMessage are cleared until new validation
runs via updateRaw.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b967165c-5819-42ca-af3c-495ca6cf461b

📥 Commits

Reviewing files that changed from the base of the PR and between 1ba2d07 and fffdb1e.

📒 Files selected for processing (2)
  • code/addons/docs/src/blocks/controls/Object.test.tsx
  • code/addons/docs/src/blocks/controls/Object.tsx

Copy link
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.

🧹 Nitpick comments (3)
code/core/src/component-testing/components/InteractionsPanel.test.tsx (1)

16-39: Keep calls in sync with the overridden interaction state.

createProps() hardcodes calls from CallStates.DONE, but the running/errored path only swaps interactions. That leaves callsById out of sync with the rendered rows and can mask regressions in MethodCall or exception rendering.

♻️ One way to keep the fixture coherent
-const createProps = (overrides: Partial<InteractionsPanelProps> = {}): InteractionsPanelProps => ({
+const createProps = (
+  callState: CallStates = CallStates.DONE,
+  overrides: Partial<InteractionsPanelProps> = {}
+): InteractionsPanelProps => ({
   storyUrl: 'http://localhost:6006/?path=/story/core-component-test-basics--step',
   status: 'completed',
   controls: {
     start: vi.fn(),
     back: vi.fn(),
     goto: vi.fn(),
     next: vi.fn(),
     end: vi.fn(),
     rerun: vi.fn(),
   },
   controlStates: {
     detached: false,
     start: true,
     back: true,
     goto: true,
     next: true,
     end: true,
   },
-  interactions: getInteractions(CallStates.DONE),
-  calls: new Map(getCalls(CallStates.DONE).map((call) => [call.id, call])),
+  interactions: getInteractions(callState),
+  calls: new Map(getCalls(callState).map((call) => [call.id, call])),
   api: { openInEditor: vi.fn() } as unknown as API,
   ...overrides,
 });
-      createProps({
-        status: 'playing',
-        interactions: getInteractions(CallStates.ACTIVE),
-      })
+      createProps(CallStates.ACTIVE, { status: 'playing' })
-          {...createProps({
-            status: 'errored',
-            interactions: getInteractions(CallStates.ERROR),
-          })}
+          {...createProps(CallStates.ERROR, { status: 'errored' })}

Also applies to: 83-102

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

In `@code/core/src/component-testing/components/InteractionsPanel.test.tsx` around
lines 16 - 39, createProps currently builds interactions from a
possibly-overridden CallStates but always constructs calls from CallStates.DONE,
causing calls (the Map passed as calls/callsById) to diverge from interactions;
update createProps so calls are derived from the same state as interactions
(e.g., compute const state = overrides.state ?? CallStates.DONE or infer state
from overrides.interactions, then call getCalls(state) when building calls) so
getCalls and getInteractions use the same CallStates and the MethodCall rows and
exception rendering remain in sync.
code/core/src/component-testing/components/InteractionsPanel.tsx (1)

145-156: Derive the spoken error state from the same sources as the visible error UI.

statusAnnouncement only keys off hasException, but this component can also surface failures via hasResultMismatch, caughtException, and unhandledErrors. A shared hasErrors flag here would keep the live-region copy aligned with what the panel actually renders.

♻️ Possible simplification
-    const statusAnnouncement =
+    const hasErrors =
+      hasException ||
+      hasResultMismatch ||
+      !!caughtException ||
+      (unhandledErrors?.length ?? 0) > 0;
+    const statusAnnouncement =
       status === 'rendering'
         ? 'Component test is rendering.'
         : status === 'playing'
           ? 'Component test is running.'
           : status === 'errored'
             ? 'Component test failed.'
             : status === 'aborted'
               ? 'Component test was aborted.'
-              : hasException
+              : hasErrors
                 ? 'Component test completed with errors.'
                 : 'Component test completed successfully.';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/component-testing/components/InteractionsPanel.tsx` around
lines 145 - 156, Create a unified error flag (e.g., hasErrors) in the
InteractionsPanel and set it to true when any of hasException,
hasResultMismatch, caughtException, or unhandledErrors is truthy; then update
statusAnnouncement to use hasErrors instead of only hasException so the
spoken/live-region state matches the visible error UI (update the logic around
statusAnnouncement and reference the existing symbols: statusAnnouncement,
hasException, hasResultMismatch, caughtException, unhandledErrors, hasErrors).
code/core/src/component-testing/components/Interaction.tsx (1)

142-151: Expand aria-label for non-step calls to include path and args context.

aria-label currently falls back to only call.method for non-step calls, discarding the path and args that <MethodCall /> renders visually. For example, a call like userEvent.click(button Click) gets the aria-label "click", losing all context about the path and target. The same fallback is also used by the nested-toggle label at lines 258-260.

Consider building the aria-label from call.path and call.args for non-step calls to match the visual rendering.

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

In `@code/core/src/component-testing/components/Interaction.tsx` around lines 142
- 151, getInteractionLabel currently returns only call.method for non-step
calls, losing the path and args context that <MethodCall /> shows; update
getInteractionLabel(Call) to build a descriptive aria-label by concatenating
call.method with a representation of call.path (e.g., join path segments) and
call.args (stringify/map args to readable tokens) so aria-label matches the
visual "<MethodCall />" output, and apply the same construction for the
nested-toggle label that uses getInteractionLabel so both places include path
and args context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@code/core/src/component-testing/components/Interaction.tsx`:
- Around line 142-151: getInteractionLabel currently returns only call.method
for non-step calls, losing the path and args context that <MethodCall /> shows;
update getInteractionLabel(Call) to build a descriptive aria-label by
concatenating call.method with a representation of call.path (e.g., join path
segments) and call.args (stringify/map args to readable tokens) so aria-label
matches the visual "<MethodCall />" output, and apply the same construction for
the nested-toggle label that uses getInteractionLabel so both places include
path and args context.

In `@code/core/src/component-testing/components/InteractionsPanel.test.tsx`:
- Around line 16-39: createProps currently builds interactions from a
possibly-overridden CallStates but always constructs calls from CallStates.DONE,
causing calls (the Map passed as calls/callsById) to diverge from interactions;
update createProps so calls are derived from the same state as interactions
(e.g., compute const state = overrides.state ?? CallStates.DONE or infer state
from overrides.interactions, then call getCalls(state) when building calls) so
getCalls and getInteractions use the same CallStates and the MethodCall rows and
exception rendering remain in sync.

In `@code/core/src/component-testing/components/InteractionsPanel.tsx`:
- Around line 145-156: Create a unified error flag (e.g., hasErrors) in the
InteractionsPanel and set it to true when any of hasException,
hasResultMismatch, caughtException, or unhandledErrors is truthy; then update
statusAnnouncement to use hasErrors instead of only hasException so the
spoken/live-region state matches the visible error UI (update the logic around
statusAnnouncement and reference the existing symbols: statusAnnouncement,
hasException, hasResultMismatch, caughtException, unhandledErrors, hasErrors).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b47552d2-4bac-4599-a363-ef54da681c57

📥 Commits

Reviewing files that changed from the base of the PR and between 1fd2f95 and 2e6bbc4.

📒 Files selected for processing (3)
  • code/core/src/component-testing/components/Interaction.tsx
  • code/core/src/component-testing/components/InteractionsPanel.test.tsx
  • code/core/src/component-testing/components/InteractionsPanel.tsx

@anchmelev anchmelev force-pushed the fix/issue-24150-object-control-json-a11y branch from 2e6bbc4 to 1fd2f95 Compare March 12, 2026 02:11
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.

1 participant