Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c4ed70a
fix(core): improve interactions panel accessibility
anchmelev Mar 12, 2026
d975247
fix(core): address interactions panel review
anchmelev Mar 12, 2026
6a996c2
fix(addon-docs): improve object control JSON editor accessibility
anchmelev Mar 11, 2026
621d6dd
fix(addon-docs): clear stale JSON parse errors in object control
anchmelev Mar 11, 2026
4844c65
test(addon-docs): move object control accessibility checks to stories
anchmelev Mar 17, 2026
0e1c17b
test(addon-docs): use switch role in object control stories
anchmelev Mar 19, 2026
49fba35
test(addon-docs): fix object control json editor interactions
anchmelev Mar 19, 2026
165dc74
fix(addon-docs): stabilize object control json editor a11y
anchmelev Mar 20, 2026
04cd2c4
refactor(addon-docs): simplify object control JSON editor interaction…
anchmelev Mar 24, 2026
3890b50
fix(addon-docs): preserve aria descriptions with tooltips
anchmelev Mar 25, 2026
7a66298
fix(addon-docs): improve object control error contrast
anchmelev Mar 25, 2026
b90f667
refactor(addon-docs): enhance object control JSON editor with layout …
anchmelev Mar 25, 2026
b45f4ef
refactor(addon-docs): adjust styling for object control JSON editor i…
anchmelev Mar 25, 2026
7409598
Merge branch 'next' into fix/issue-24150-object-control-json-a11y
anchmelev Mar 31, 2026
dc0cde3
Merge remote-tracking branch 'origin/next' into fix/issue-24150-objec…
anchmelev Apr 1, 2026
0a5e07c
Merge branch 'next' of https://github.com/storybookjs/storybook into …
anchmelev Apr 1, 2026
f65ea61
fix(docs): handle function values in object control
anchmelev Apr 2, 2026
f453baf
fix: restore green checks for object control branch
anchmelev Apr 2, 2026
f7b52b6
Merge branch 'next' of https://github.com/storybookjs/storybook into …
anchmelev Apr 2, 2026
afbdb47
Update Object control live announcement
Sidnioulz Apr 13, 2026
1ef15e2
Merge branch 'next' into fix/issue-24150-object-control-json-a11y
Sidnioulz Apr 13, 2026
d27df73
Add toHaveLiveRegion to Object stories and fix its types
Sidnioulz Apr 13, 2026
6a26397
Add new AllControlsObjectSet and AllControlsFilled stories with inter…
anchmelev Apr 15, 2026
ce61c5c
test: target object control by stable id
anchmelev Apr 15, 2026
28f018c
fix: restore object control raw editor spacing
anchmelev Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This file is the canonical instruction source for coding agents. Files like `CLA
Storybook is a large TypeScript monorepo. The git root is the repo root, the main code lives in `code/`, and build tooling lives in `scripts/`. The default branch is `next`.

- **Base branch**: `next` (all PRs should target `next`, not `main`)
- **Node.js**: `22.21.1` (see `.nvmrc`)
- **Node.js**: `22.22.1` (see `.nvmrc`)

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.

Do you know why this changed? This shouldn't have happened on that PR.

- **Package Manager**: Yarn Berry
- **Task orchestration**: NX plus the custom `yarn task` runner
- **CI environment**: Linux and Windows
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';

import { action } from 'storybook/actions';
import { expect, fireEvent } from 'storybook/test';
import { styled } from 'storybook/theming';

import * as ArgRow from './ArgRow.stories';
Expand Down Expand Up @@ -123,6 +124,46 @@ export const AllControls = {
},
};

export const AllControlsObjectSet: Story = {
args: AllControls.args,
play: async ({ canvas, canvasElement, step }) => {
await step('Switch the object control from empty to filled', async () => {
const setObjectButton = canvasElement.querySelector('#set-someObject') as HTMLButtonElement;

await fireEvent.click(setObjectButton);

const rawInput = canvas.getByRole('textbox', { name: 'Edit someObject as JSON' });
await expect(rawInput).toBeVisible();
await expect(rawInput).toHaveValue('{}');
});
},
};

export const AllControlsFilled: Story = {
args: {
...AllControls.args,
args: {
someArray: [1, 2, 3],
someBoolean: true,
someColor: '#ff4785',
someDate: new Date('2020-10-20T09:30:02'),
someObject: {
name: 'Storybook',
nested: { enabled: true, count: 2 },
},
someString: 'Filled string control',
number: 42,
range: 24,
radio: 'b',
inlineRadio: 'c',
check: ['a'],
inlineCheck: ['b', 'c'],
select: 'b',
multiSelect: ['a', 'c'],
},
},
};

const AddonPanelLayout = styled.div(({ theme }) => ({
fontSize: theme.typography.size.s2 - 1,
background: theme.background.content,
Expand Down
99 changes: 98 additions & 1 deletion code/addons/docs/src/blocks/controls/Object.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { destroyAnnouncer } from '@react-aria/live-announcer';

import { fn } from 'storybook/test';
import { expect, fireEvent, fn, waitFor, within } from 'storybook/test';

import { ObjectControl } from './Object';

Expand All @@ -12,6 +13,9 @@ const meta = {
name: 'object',
onChange: fn(),
},
beforeEach: async () => {
destroyAnnouncer();
},
} satisfies Meta<typeof ObjectControl>;

export default meta;
Expand Down Expand Up @@ -140,3 +144,96 @@ export const ArraySmallViewport: Story = {
chromatic: { viewports: [320] },
},
};

export const JsonEditorValidation: Story = {
args: {
value: { label: 'value' },
onChange: fn(),
},
play: async ({ args, canvasElement, step }) => {
const canvas = within(canvasElement);

await step('Open the raw JSON editor and verify it is described', async () => {
const editAsJsonButton = canvas.getByRole('switch', { name: 'Edit object as JSON' });

await expect(editAsJsonButton).toHaveAttribute('aria-describedby');
await fireEvent.click(editAsJsonButton);
await expect(
canvas.getByRole('textbox', { name: 'Edit object as JSON' })
).toBeInTheDocument();
});

await step('Show a parse error for invalid JSON', async () => {
const rawInput = canvas.getByRole('textbox', { name: 'Edit object as JSON' });

rawInput.focus();
await fireEvent.change(rawInput, { target: { value: '{"label":' } });
rawInput.blur();

await waitFor(() => {
expect(document.body).toHaveLiveRegion({
text: 'Invalid JSON: Unexpected end of JSON input',
});
});
await expect(rawInput).toHaveAttribute('aria-invalid', 'true');

const error = await canvas.findByText('Invalid JSON: Unexpected end of JSON input');
await expect(rawInput).toHaveAttribute('aria-describedby', error.getAttribute('id') ?? '');

await expect(args.onChange).not.toHaveBeenCalled();
});

await step('Clear the parse error after entering valid JSON', async () => {
const rawInput = canvas.getByRole('textbox', { name: 'Edit object as JSON' });

rawInput.focus();
await fireEvent.change(rawInput, { target: { value: '{"label":"updated"}' } });
rawInput.blur();

await waitFor(async () => {
await expect(document.body).not.toHaveLiveRegion();
});
await expect(rawInput).toHaveAttribute('aria-invalid', 'false');
await expect(rawInput).not.toHaveAttribute('aria-describedby');
await expect(args.onChange).toHaveBeenCalledWith({ label: 'updated' });
});
},
};

export const JsonEditorErrorReset: Story = {
args: {
value: { label: 'value' },
onChange: fn(),
},
play: async ({ canvas, step }) => {
await step('Create a parse error in the raw JSON editor', async () => {
const editAsJsonButton = canvas.getByRole('switch', { name: 'Edit object as JSON' });
await fireEvent.click(editAsJsonButton);

const rawInput = canvas.getByRole('textbox', { name: 'Edit object as JSON' });
rawInput.focus();
await fireEvent.change(rawInput, { target: { value: '{"label":' } });
rawInput.blur();

await waitFor(() => {
expect(document.body).toHaveLiveRegion({
text: 'Invalid JSON: Unexpected end of JSON input',
});
});
await expect(rawInput).toHaveAttribute('aria-invalid', 'true');
});

await step('Clear stale parse errors after closing and reopening the editor', async () => {
const editAsJsonButton = canvas.getByRole('switch', { name: 'Edit object as JSON' });
await fireEvent.click(editAsJsonButton);
await fireEvent.click(canvas.getByRole('switch', { name: 'Edit object as JSON' }));

const rawInput = canvas.getByRole('textbox', { name: 'Edit object as JSON' });
await waitFor(async () => {
await expect(document.body).not.toHaveLiveRegion();
});
await expect(rawInput).toHaveAttribute('aria-invalid', 'false');
await expect(rawInput).not.toHaveAttribute('aria-describedby');
});
},
};
29 changes: 29 additions & 0 deletions code/addons/docs/src/blocks/controls/Object.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// @vitest-environment happy-dom
import React from 'react';

import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { ThemeProvider, convert, themes } from 'storybook/theming';

import { ObjectControl } from './Object';

describe('Object control', () => {
it('renders function values without crashing', () => {
const onChange = vi.fn();

render(
<ThemeProvider theme={convert(themes.light)}>
<ObjectControl
name="object"
storyId="story--function"
value={vi.fn()}
onChange={onChange}
/>
</ThemeProvider>
);

const rawInput = screen.getByRole('textbox', { name: 'Edit object as JSON' });

expect((rawInput as HTMLTextAreaElement).value).toBe('');
});
});
Loading
Loading