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