diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index ea0d64b89a04..6280affeb4b3 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,7 @@ +## 10.4.0-alpha.1 + +- Docs: Ensure unique control id attributes across multiple Controls blocks - [#34021](https://github.com/storybookjs/storybook/pull/34021), thanks @TheSeydiCharyyev! + ## 10.4.0-alpha.0 diff --git a/code/addons/docs/src/blocks/blocks/Controls.stories.tsx b/code/addons/docs/src/blocks/blocks/Controls.stories.tsx index b029147036a9..0a312c1e2f55 100644 --- a/code/addons/docs/src/blocks/blocks/Controls.stories.tsx +++ b/code/addons/docs/src/blocks/blocks/Controls.stories.tsx @@ -4,7 +4,7 @@ import type { PlayFunctionContext } from 'storybook/internal/csf'; import type { Meta, StoryObj } from '@storybook/react-vite'; -import { within } from 'storybook/test'; +import { expect, within } from 'storybook/test'; import * as ExampleStories from '../examples/ControlsParameters.stories'; import * as SubcomponentsExampleStories from '../examples/ControlsWithSubcomponentsParameters.stories'; @@ -159,3 +159,25 @@ export const EmptyArgTypes: Story = { of: EmptyArgTypesStories.Default, }, }; + +/** + * When multiple Controls blocks for different stories are on the same docs page, each control + * should have a unique id attribute (scoped by storyId). This verifies the fix for + * https://github.com/storybookjs/storybook/issues/26144 + */ +export const MultipleControlsOnSamePage: Story = { + render: () => ( + <> + + + + ), + play: async ({ canvasElement }) => { + const allIds = Array.from(canvasElement.querySelectorAll('[id^="control-"]')).map( + (el) => el.id + ); + const uniqueIds = new Set(allIds); + await expect(allIds.length).toBeGreaterThan(0); + await expect(uniqueIds.size).toBe(allIds.length); + }, +}; diff --git a/code/addons/docs/src/blocks/blocks/Controls.tsx b/code/addons/docs/src/blocks/blocks/Controls.tsx index a1b784662f40..5110ba6687c0 100644 --- a/code/addons/docs/src/blocks/blocks/Controls.tsx +++ b/code/addons/docs/src/blocks/blocks/Controls.tsx @@ -70,6 +70,7 @@ const ControlsImpl: FC = (props) => { } return ( = (props) => { globals={globals} updateArgs={updateArgs} resetArgs={resetArgs} + storyId={story.id} /> ); }; diff --git a/code/addons/docs/src/blocks/components/ArgsTable/ArgControl.tsx b/code/addons/docs/src/blocks/components/ArgsTable/ArgControl.tsx index dd6f7a997544..97d3013b4bd8 100644 --- a/code/addons/docs/src/blocks/components/ArgsTable/ArgControl.tsx +++ b/code/addons/docs/src/blocks/components/ArgsTable/ArgControl.tsx @@ -21,6 +21,7 @@ export interface ArgControlProps { arg: any; updateArgs: (args: Args) => void; isHovered: boolean; + storyId?: string; } const Controls: Record> = { @@ -43,7 +44,7 @@ const Controls: Record> = { const NoControl = () => <>-; -export const ArgControl: FC = ({ row, arg, updateArgs, isHovered }) => { +export const ArgControl: FC = ({ row, arg, updateArgs, isHovered, storyId }) => { const { key, control } = row; const [isFocused, setFocused] = useState(false); @@ -84,7 +85,15 @@ export const ArgControl: FC = ({ row, arg, updateArgs, isHovere } // row.name is a display name and not a suitable DOM input id or name - i might contain whitespace etc. // row.key is a hash key and therefore a much safer choice - const props = { name: key, argType: row, value: boxedValue.value, onChange, onBlur, onFocus }; + const props = { + name: key, + storyId, + argType: row, + value: boxedValue.value, + onChange, + onBlur, + onFocus, + }; const Control = Controls[control.type] || NoControl; return ; }; diff --git a/code/addons/docs/src/blocks/components/ArgsTable/ArgRow.tsx b/code/addons/docs/src/blocks/components/ArgsTable/ArgRow.tsx index c6ccb0099ae2..83ad20c476ac 100644 --- a/code/addons/docs/src/blocks/components/ArgsTable/ArgRow.tsx +++ b/code/addons/docs/src/blocks/components/ArgsTable/ArgRow.tsx @@ -21,6 +21,7 @@ interface ArgRowProps { compact?: boolean; expandable?: boolean; initialExpandedArgs?: boolean; + storyId?: string; } const Name = styled.span({ fontWeight: 'bold' }); diff --git a/code/addons/docs/src/blocks/components/ArgsTable/ArgsTable.tsx b/code/addons/docs/src/blocks/components/ArgsTable/ArgsTable.tsx index 874edbc2ccc2..1f1dfc4394a5 100644 --- a/code/addons/docs/src/blocks/components/ArgsTable/ArgsTable.tsx +++ b/code/addons/docs/src/blocks/components/ArgsTable/ArgsTable.tsx @@ -204,6 +204,7 @@ export interface ArgsTableOptionProps { initialExpandedArgs?: boolean; isLoading?: boolean; sort?: SortType; + storyId?: string; } interface ArgsTableDataProps { rows: ArgTypes; @@ -327,6 +328,7 @@ export const ArgsTable: FC = (props) => { initialExpandedArgs, sort = 'none', isLoading, + storyId, } = props; if ('error' in props) { @@ -380,7 +382,7 @@ export const ArgsTable: FC = (props) => { } const expandable = Object.keys(groups.sections).length > 0; - const common = { updateArgs, compact, inAddonPanel, initialExpandedArgs }; + const common = { updateArgs, compact, inAddonPanel, initialExpandedArgs, storyId }; return ( diff --git a/code/addons/docs/src/blocks/controls/Boolean.tsx b/code/addons/docs/src/blocks/controls/Boolean.tsx index 584ed2a6d0e0..b94b9fe500ee 100644 --- a/code/addons/docs/src/blocks/controls/Boolean.tsx +++ b/code/addons/docs/src/blocks/controls/Boolean.tsx @@ -116,6 +116,7 @@ export type BooleanProps = ControlProps & BooleanConfig; */ export const BooleanControl: FC = ({ name, + storyId, value, onChange, onBlur, @@ -130,7 +131,7 @@ export const BooleanControl: FC = ({ ariaLabel={false} variant="outline" size="medium" - id={getControlSetterButtonId(name)} + id={getControlSetterButtonId(name, storyId)} onClick={onSetFalse} disabled={readonly} > @@ -138,7 +139,7 @@ export const BooleanControl: FC = ({ ); } - const controlId = getControlId(name); + const controlId = getControlId(name, storyId); const parsedValue = typeof value === 'string' ? parse(value) : value; diff --git a/code/addons/docs/src/blocks/controls/Color.tsx b/code/addons/docs/src/blocks/controls/Color.tsx index 0758f7b8611c..2f923c0aeda3 100644 --- a/code/addons/docs/src/blocks/controls/Color.tsx +++ b/code/addons/docs/src/blocks/controls/Color.tsx @@ -360,6 +360,7 @@ const usePresets = ( export type ColorControlProps = ControlProps & ColorConfig; export const ColorControl: FC = ({ name, + storyId, value: initialValue, onChange, onFocus, @@ -377,7 +378,7 @@ export const ColorControl: FC = ({ const Picker = ColorPicker[colorSpace]; const readOnly = !!argType?.table?.readonly; - const controlId = getControlId(name); + const controlId = getControlId(name, storyId); return ( diff --git a/code/addons/docs/src/blocks/controls/Date.tsx b/code/addons/docs/src/blocks/controls/Date.tsx index 3568cf940f5c..55fe0063bb71 100644 --- a/code/addons/docs/src/blocks/controls/Date.tsx +++ b/code/addons/docs/src/blocks/controls/Date.tsx @@ -66,7 +66,15 @@ const FlexSpaced = styled.fieldset({ }); export type DateProps = ControlProps & DateConfig; -export const DateControl: FC = ({ name, value, onChange, onFocus, onBlur, argType }) => { +export const DateControl: FC = ({ + name, + storyId, + value, + onChange, + onFocus, + onBlur, + argType, +}) => { const [valid, setValid] = useState(true); const dateRef = useRef(); const timeRef = useRef(); @@ -114,7 +122,7 @@ export const DateControl: FC = ({ name, value, onChange, onFocus, onB setValid(!!time); }; - const controlId = getControlId(name); + const controlId = getControlId(name, storyId); return ( diff --git a/code/addons/docs/src/blocks/controls/Files.tsx b/code/addons/docs/src/blocks/controls/Files.tsx index 1569790641da..85f2d839701a 100644 --- a/code/addons/docs/src/blocks/controls/Files.tsx +++ b/code/addons/docs/src/blocks/controls/Files.tsx @@ -40,6 +40,7 @@ function revokeOldUrls(urls: string[]) { export const FilesControl: FC = ({ onChange, name, + storyId, accept = 'image/*', value, argType, @@ -63,7 +64,7 @@ export const FilesControl: FC = ({ } }, [value, name]); - const controlId = getControlId(name); + const controlId = getControlId(name, storyId); return ( <> diff --git a/code/addons/docs/src/blocks/controls/Number.tsx b/code/addons/docs/src/blocks/controls/Number.tsx index 947a9f7afca7..a3f33db74de7 100644 --- a/code/addons/docs/src/blocks/controls/Number.tsx +++ b/code/addons/docs/src/blocks/controls/Number.tsx @@ -27,6 +27,7 @@ const FormInput = styled(Form.Input)(({ theme }) => ({ export const NumberControl: FC = ({ name, + storyId, value, onChange, min, @@ -102,7 +103,7 @@ export const NumberControl: FC = ({ ariaLabel={false} variant="outline" size="medium" - id={getControlSetterButtonId(name)} + id={getControlSetterButtonId(name, storyId)} onClick={onForceVisible} disabled={readonly} > @@ -115,7 +116,7 @@ export const NumberControl: FC = ({ ) => { export type ObjectProps = ControlProps & ObjectConfig; -export const ObjectControl: FC = ({ name, value, onChange, argType }) => { +export const ObjectControl: FC = ({ name, storyId, value, onChange, argType }) => { const theme = useTheme(); const data = useMemo(() => value && cloneDeep(value), [value]); const hasData = data !== null && data !== undefined; @@ -208,7 +208,7 @@ export const ObjectControl: FC = ({ name, value, onChange, argType