Skip to content

Commit 20869a1

Browse files
authored
Merge pull request storybookjs#35018 from cssinate/next
Docs: Fix Controls losing focus when subcomponents are defined
2 parents c843cef + 09d6b06 commit 20869a1

3 files changed

Lines changed: 78 additions & 15 deletions

File tree

code/addons/docs/src/blocks/blocks/Controls.stories.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { PlayFunctionContext } from 'storybook/internal/csf';
44

55
import type { Meta, StoryObj } from '@storybook/react-vite';
66

7-
import { expect, within } from 'storybook/test';
7+
import { expect, userEvent, within } from 'storybook/test';
88

99
import * as ExampleStories from '../examples/ControlsParameters.stories';
1010
import * as SubcomponentsExampleStories from '../examples/ControlsWithSubcomponentsParameters.stories';
@@ -120,6 +120,24 @@ export const SubcomponentsOfStory: Story = {
120120
},
121121
};
122122

123+
/**
124+
* When a component declares subcomponents, editing a control on the main component tab should not
125+
* remount the input and drop focus. This verifies the fix for
126+
* https://github.com/storybookjs/storybook/issues/29028
127+
*/
128+
export const SubcomponentsRetainControlFocus: Story = {
129+
args: {
130+
of: SubcomponentsExampleStories.NoParameters,
131+
},
132+
play: async ({ canvasElement }) => {
133+
const canvas = within(canvasElement);
134+
const input = await canvas.findByDisplayValue('b');
135+
await userEvent.click(input);
136+
await userEvent.type(input, 'x');
137+
await expect(document.activeElement).toBe(input);
138+
},
139+
};
140+
123141
export const SubcomponentsIncludeProp: Story = {
124142
args: {
125143
of: SubcomponentsExampleStories.NoParameters,

code/addons/docs/src/blocks/components/ArgsTable/TabbedArgsTable.stories.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
import React, { useState } from 'react';
2+
13
import { TabsView } from 'storybook/internal/components';
24

35
import type { Meta, StoryObj } from '@storybook/react-vite';
46

7+
import { expect, userEvent, within } from 'storybook/test';
8+
9+
import * as ArgRow from './ArgRow.stories';
510
import { ArgsTable } from './ArgsTable';
611
import { Compact, Normal, Sections } from './ArgsTable.stories';
12+
import type { TabbedArgsTableProps } from './TabbedArgsTable';
713
import { TabbedArgsTable } from './TabbedArgsTable';
14+
import type { Args } from './types';
815

916
const meta = {
1017
component: TabbedArgsTable,
@@ -57,3 +64,42 @@ export const WithContentAround: StoryObj<typeof meta> = {
5764
</>
5865
),
5966
};
67+
68+
/**
69+
* When multiple tabs are shown, editing a control on the first tab should not remount the input and
70+
* drop focus. This verifies the fix for https://github.com/storybookjs/storybook/issues/29028
71+
*/
72+
export const RetainControlFocusWithTabs: StoryObj<typeof meta> = {
73+
args: {
74+
tabs: {
75+
Main: {
76+
rows: {
77+
someString: ArgRow.String.args.row,
78+
},
79+
},
80+
Other: {
81+
rows: {
82+
numberType: ArgRow.Number.args.row,
83+
},
84+
},
85+
},
86+
args: { someString: 'hello' },
87+
},
88+
render: function Render(props) {
89+
const [storyArgs, setStoryArgs] = useState<Args>({ someString: 'hello' });
90+
return (
91+
<TabbedArgsTable
92+
{...(props as TabbedArgsTableProps)}
93+
args={storyArgs}
94+
updateArgs={(updated) => setStoryArgs((prev) => ({ ...prev, ...updated }))}
95+
/>
96+
);
97+
},
98+
play: async ({ canvasElement }) => {
99+
const canvas = within(canvasElement);
100+
const input = await canvas.findByDisplayValue('hello');
101+
await userEvent.click(input);
102+
await userEvent.type(input, 'x');
103+
await expect(document.activeElement).toBe(input);
104+
},
105+
};

code/addons/docs/src/blocks/components/ArgsTable/TabbedArgsTable.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,19 @@ export const TabbedArgsTable: FC<TabbedArgsTableProps> = ({ tabs, ...props }) =>
2525
return <ArgsTable {...entries[0][1]} {...props} />;
2626
}
2727

28-
const tabsFromEntries = entries.map(([label, table], index) => ({
29-
id: `prop_table_div_${label}`,
30-
title: label,
31-
children: () => {
32-
/**
33-
* The first tab is the main component, controllable if in the Controls block All other tabs
34-
* are subcomponents, never controllable, so we filter out the props indicating
35-
* controllability Essentially all subcomponents always behave like ArgTypes, never Controls
36-
*/
37-
const argsTableProps = index === 0 ? props : { sort: props.sort };
38-
39-
return <ArgsTable inTabPanel key={`prop_table_${label}`} {...table} {...argsTableProps} />;
40-
},
41-
}));
28+
const tabsFromEntries = entries.map(([label, table], index) => {
29+
// The first tab is the main component, controllable if in the Controls block. All other tabs
30+
// are subcomponents, never controllable, so we filter out the props indicating
31+
// controllability. Pass a stable element (not an inline function component) so React
32+
// reconciles the tab panel instead of remounting it on every args update.
33+
const argsTableProps = index === 0 ? props : { sort: props.sort };
34+
35+
return {
36+
id: `prop_table_div_${label}`,
37+
title: label,
38+
children: <ArgsTable inTabPanel {...table} {...argsTableProps} />,
39+
};
40+
});
4241

4342
return <StyledTabsView tabs={tabsFromEntries} />;
4443
};

0 commit comments

Comments
 (0)