From fe43b005798b854aff932027c55e640872ce0c81 Mon Sep 17 00:00:00 2001 From: Maks Pikov Date: Sun, 22 Mar 2026 22:19:48 +0000 Subject: [PATCH 1/3] refactor(docs): extract SetValueButton shared component from control components The 'Set X' button pattern (rendered when a control value is undefined) was duplicated verbatim across four control files: Boolean, Number, Text, and Object. The only differences were the button label and the onClick handler. Extract a shared SetValueButton component to eliminate the repetition. As a side effect, Object's button now consistently uses variant='outline' and size='medium', matching the other three controls. Closes #34211 --- .../docs/src/blocks/controls/Boolean.tsx | 18 ++++------ .../docs/src/blocks/controls/Number.tsx | 18 +++++----- .../docs/src/blocks/controls/Object.tsx | 16 ++++----- .../src/blocks/controls/SetValueButton.tsx | 33 +++++++++++++++++++ code/addons/docs/src/blocks/controls/Text.tsx | 20 +++++------ 5 files changed, 65 insertions(+), 40 deletions(-) create mode 100644 code/addons/docs/src/blocks/controls/SetValueButton.tsx diff --git a/code/addons/docs/src/blocks/controls/Boolean.tsx b/code/addons/docs/src/blocks/controls/Boolean.tsx index b94b9fe500ee..f983615fb43b 100644 --- a/code/addons/docs/src/blocks/controls/Boolean.tsx +++ b/code/addons/docs/src/blocks/controls/Boolean.tsx @@ -1,12 +1,11 @@ import type { FC } from 'react'; import React, { useCallback } from 'react'; -import { Button } from 'storybook/internal/components'; - import { opacify, transparentize } from 'polished'; import { styled } from 'storybook/theming'; -import { getControlId, getControlSetterButtonId } from './helpers'; +import { getControlId } from './helpers'; +import { SetValueButton } from './SetValueButton'; import type { BooleanConfig, BooleanValue, ControlProps } from './types'; const Label = styled.label(({ theme }) => ({ @@ -127,16 +126,13 @@ export const BooleanControl: FC = ({ const readonly = !!argType?.table?.readonly; if (value === undefined) { return ( - + /> ); } const controlId = getControlId(name, storyId); diff --git a/code/addons/docs/src/blocks/controls/Number.tsx b/code/addons/docs/src/blocks/controls/Number.tsx index a3f33db74de7..a6507539496d 100644 --- a/code/addons/docs/src/blocks/controls/Number.tsx +++ b/code/addons/docs/src/blocks/controls/Number.tsx @@ -1,11 +1,12 @@ import type { ChangeEvent, FC } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Button, Form } from 'storybook/internal/components'; +import { Form } from 'storybook/internal/components'; import { styled } from 'storybook/theming'; -import { getControlId, getControlSetterButtonId } from './helpers'; +import { getControlId } from './helpers'; +import { SetValueButton } from './SetValueButton'; import type { ControlProps, NumberConfig, NumberValue } from './types'; const Wrapper = styled.label({ @@ -99,16 +100,13 @@ export const NumberControl: FC = ({ if (value === undefined) { return ( - + /> ); } diff --git a/code/addons/docs/src/blocks/controls/Object.tsx b/code/addons/docs/src/blocks/controls/Object.tsx index a724dfb9d0a7..1816ce1a409c 100644 --- a/code/addons/docs/src/blocks/controls/Object.tsx +++ b/code/addons/docs/src/blocks/controls/Object.tsx @@ -8,7 +8,8 @@ import { AddIcon, EditIcon, SubtractIcon } from '@storybook/icons'; import { cloneDeep } from 'es-toolkit/object'; import { styled, useTheme } from 'storybook/theming'; -import { getControlId, getControlSetterButtonId } from './helpers'; +import { getControlId } from './helpers'; +import { SetValueButton } from './SetValueButton'; import { JsonTree } from './react-editable-json-tree'; import type { ControlProps, ObjectConfig, ObjectValue } from './types'; @@ -205,14 +206,13 @@ export const ObjectControl: FC = ({ name, storyId, value, onChange, if (!hasData) { return ( - + disabled={readonly} + /> ); } diff --git a/code/addons/docs/src/blocks/controls/SetValueButton.tsx b/code/addons/docs/src/blocks/controls/SetValueButton.tsx new file mode 100644 index 000000000000..a24389e9c129 --- /dev/null +++ b/code/addons/docs/src/blocks/controls/SetValueButton.tsx @@ -0,0 +1,33 @@ +import type { FC } from 'react'; +import React from 'react'; + +import { Button } from 'storybook/internal/components'; + +import { getControlSetterButtonId } from './helpers'; + +interface SetValueButtonProps { + name: string; + storyId?: string; + label: string; + onClick: () => void; + disabled?: boolean; +} + +export const SetValueButton: FC = ({ + name, + storyId, + label, + onClick, + disabled, +}) => ( + +); diff --git a/code/addons/docs/src/blocks/controls/Text.tsx b/code/addons/docs/src/blocks/controls/Text.tsx index 4723e9e5de68..2793a72d0a90 100644 --- a/code/addons/docs/src/blocks/controls/Text.tsx +++ b/code/addons/docs/src/blocks/controls/Text.tsx @@ -1,11 +1,12 @@ import type { ChangeEvent, FC } from 'react'; import React, { useCallback, useState } from 'react'; -import { Button, Form } from 'storybook/internal/components'; +import { Form } from 'storybook/internal/components'; import { styled } from 'storybook/theming'; -import { getControlId, getControlSetterButtonId } from './helpers'; +import { getControlId } from './helpers'; +import { SetValueButton } from './SetValueButton'; import type { ControlProps, TextConfig, TextValue } from './types'; export type TextProps = ControlProps & TextConfig; @@ -45,16 +46,13 @@ export const TextControl: FC = ({ if (value === undefined) { return ( - + disabled={readonly} + /> ); } From a9d487bb056ad0a8c2badb36993fe5f5682d88d0 Mon Sep 17 00:00:00 2001 From: Maks Pikov Date: Wed, 8 Apr 2026 21:13:08 +0000 Subject: [PATCH 2/3] refactor(docs): move SetValueButton into helpers.tsx, use children prop - Move SetValueButton component directly into helpers.tsx (renamed from .ts) - Replace label prop with children for more idiomatic React composition - Update all consumers (Boolean, Object, Text, Number) to use merged import - Delete separate SetValueButton.tsx file Addresses review feedback from JReinhold on #34263 --- .../docs/src/blocks/controls/Boolean.tsx | 6 ++-- .../docs/src/blocks/controls/Number.tsx | 6 ++-- .../docs/src/blocks/controls/Object.tsx | 6 ++-- .../src/blocks/controls/SetValueButton.tsx | 33 ------------------- code/addons/docs/src/blocks/controls/Text.tsx | 6 ++-- .../controls/{helpers.ts => helpers.tsx} | 32 ++++++++++++++++++ 6 files changed, 40 insertions(+), 49 deletions(-) delete mode 100644 code/addons/docs/src/blocks/controls/SetValueButton.tsx rename code/addons/docs/src/blocks/controls/{helpers.ts => helpers.tsx} (62%) diff --git a/code/addons/docs/src/blocks/controls/Boolean.tsx b/code/addons/docs/src/blocks/controls/Boolean.tsx index f983615fb43b..f9d1b967f482 100644 --- a/code/addons/docs/src/blocks/controls/Boolean.tsx +++ b/code/addons/docs/src/blocks/controls/Boolean.tsx @@ -4,8 +4,7 @@ import React, { useCallback } from 'react'; import { opacify, transparentize } from 'polished'; import { styled } from 'storybook/theming'; -import { getControlId } from './helpers'; -import { SetValueButton } from './SetValueButton'; +import { getControlId, SetValueButton } from './helpers'; import type { BooleanConfig, BooleanValue, ControlProps } from './types'; const Label = styled.label(({ theme }) => ({ @@ -129,10 +128,9 @@ export const BooleanControl: FC = ({ + >Set boolean ); } const controlId = getControlId(name, storyId); diff --git a/code/addons/docs/src/blocks/controls/Number.tsx b/code/addons/docs/src/blocks/controls/Number.tsx index a6507539496d..83d0bb38bea5 100644 --- a/code/addons/docs/src/blocks/controls/Number.tsx +++ b/code/addons/docs/src/blocks/controls/Number.tsx @@ -5,8 +5,7 @@ import { Form } from 'storybook/internal/components'; import { styled } from 'storybook/theming'; -import { getControlId } from './helpers'; -import { SetValueButton } from './SetValueButton'; +import { getControlId, SetValueButton } from './helpers'; import type { ControlProps, NumberConfig, NumberValue } from './types'; const Wrapper = styled.label({ @@ -103,10 +102,9 @@ export const NumberControl: FC = ({ + >Set number ); } diff --git a/code/addons/docs/src/blocks/controls/Object.tsx b/code/addons/docs/src/blocks/controls/Object.tsx index 1816ce1a409c..e70b1b20c7b3 100644 --- a/code/addons/docs/src/blocks/controls/Object.tsx +++ b/code/addons/docs/src/blocks/controls/Object.tsx @@ -8,8 +8,7 @@ import { AddIcon, EditIcon, SubtractIcon } from '@storybook/icons'; import { cloneDeep } from 'es-toolkit/object'; import { styled, useTheme } from 'storybook/theming'; -import { getControlId } from './helpers'; -import { SetValueButton } from './SetValueButton'; +import { getControlId, SetValueButton } from './helpers'; import { JsonTree } from './react-editable-json-tree'; import type { ControlProps, ObjectConfig, ObjectValue } from './types'; @@ -209,10 +208,9 @@ export const ObjectControl: FC = ({ name, storyId, value, onChange, + >Set object ); } diff --git a/code/addons/docs/src/blocks/controls/SetValueButton.tsx b/code/addons/docs/src/blocks/controls/SetValueButton.tsx deleted file mode 100644 index a24389e9c129..000000000000 --- a/code/addons/docs/src/blocks/controls/SetValueButton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { FC } from 'react'; -import React from 'react'; - -import { Button } from 'storybook/internal/components'; - -import { getControlSetterButtonId } from './helpers'; - -interface SetValueButtonProps { - name: string; - storyId?: string; - label: string; - onClick: () => void; - disabled?: boolean; -} - -export const SetValueButton: FC = ({ - name, - storyId, - label, - onClick, - disabled, -}) => ( - -); diff --git a/code/addons/docs/src/blocks/controls/Text.tsx b/code/addons/docs/src/blocks/controls/Text.tsx index 2793a72d0a90..3461d70f7b8c 100644 --- a/code/addons/docs/src/blocks/controls/Text.tsx +++ b/code/addons/docs/src/blocks/controls/Text.tsx @@ -5,8 +5,7 @@ import { Form } from 'storybook/internal/components'; import { styled } from 'storybook/theming'; -import { getControlId } from './helpers'; -import { SetValueButton } from './SetValueButton'; +import { getControlId, SetValueButton } from './helpers'; import type { ControlProps, TextConfig, TextValue } from './types'; export type TextProps = ControlProps & TextConfig; @@ -49,10 +48,9 @@ export const TextControl: FC = ({ + >Set string ); } diff --git a/code/addons/docs/src/blocks/controls/helpers.ts b/code/addons/docs/src/blocks/controls/helpers.tsx similarity index 62% rename from code/addons/docs/src/blocks/controls/helpers.ts rename to code/addons/docs/src/blocks/controls/helpers.tsx index b9895c7eb293..9ad558c55320 100644 --- a/code/addons/docs/src/blocks/controls/helpers.ts +++ b/code/addons/docs/src/blocks/controls/helpers.tsx @@ -1,3 +1,8 @@ +import type { FC, ReactNode } from 'react'; +import React from 'react'; + +import { Button } from 'storybook/internal/components'; + /** * Adds `control` prefix to make ID attribute more specific. Removes spaces because spaces are not * allowed in ID attributes @@ -31,3 +36,30 @@ export const getControlSetterButtonId = (value: string, storyId?: string) => { const base = value.replace(/\s+/g, '-'); return storyId ? `set-${storyId}-${base}` : `set-${base}`; }; + +interface SetValueButtonProps { + name: string; + storyId?: string; + children: ReactNode; + onClick: () => void; + disabled?: boolean; +} + +export const SetValueButton: FC = ({ + name, + storyId, + children, + onClick, + disabled, +}) => ( + +); From ae209b4928b619a6b317622bdf436c439dd80412 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Mon, 27 Apr 2026 13:17:32 +0200 Subject: [PATCH 3/3] fix format and tests --- code/addons/docs/src/blocks/controls/Boolean.tsx | 9 +++------ code/addons/docs/src/blocks/controls/Number.tsx | 9 +++------ code/addons/docs/src/blocks/controls/Object.tsx | 9 +++------ code/addons/docs/src/blocks/controls/Text.tsx | 9 +++------ code/addons/docs/src/blocks/controls/helpers.test.ts | 2 ++ 5 files changed, 14 insertions(+), 24 deletions(-) diff --git a/code/addons/docs/src/blocks/controls/Boolean.tsx b/code/addons/docs/src/blocks/controls/Boolean.tsx index 351f6e3d7849..11484bc300c6 100644 --- a/code/addons/docs/src/blocks/controls/Boolean.tsx +++ b/code/addons/docs/src/blocks/controls/Boolean.tsx @@ -131,12 +131,9 @@ export const BooleanControl: FC = ({ const readonly = !!argType?.table?.readonly; if (value === undefined) { return ( - Set boolean + + Set boolean + ); } const controlId = getControlId(name, storyId); diff --git a/code/addons/docs/src/blocks/controls/Number.tsx b/code/addons/docs/src/blocks/controls/Number.tsx index 83d0bb38bea5..e875e7bfcf4e 100644 --- a/code/addons/docs/src/blocks/controls/Number.tsx +++ b/code/addons/docs/src/blocks/controls/Number.tsx @@ -99,12 +99,9 @@ export const NumberControl: FC = ({ if (value === undefined) { return ( - Set number + + Set number + ); } diff --git a/code/addons/docs/src/blocks/controls/Object.tsx b/code/addons/docs/src/blocks/controls/Object.tsx index e70b1b20c7b3..5a5be06a6046 100644 --- a/code/addons/docs/src/blocks/controls/Object.tsx +++ b/code/addons/docs/src/blocks/controls/Object.tsx @@ -205,12 +205,9 @@ export const ObjectControl: FC = ({ name, storyId, value, onChange, if (!hasData) { return ( - Set object + + Set object + ); } diff --git a/code/addons/docs/src/blocks/controls/Text.tsx b/code/addons/docs/src/blocks/controls/Text.tsx index 3461d70f7b8c..0c8919b54545 100644 --- a/code/addons/docs/src/blocks/controls/Text.tsx +++ b/code/addons/docs/src/blocks/controls/Text.tsx @@ -45,12 +45,9 @@ export const TextControl: FC = ({ if (value === undefined) { return ( - Set string + + Set string + ); } diff --git a/code/addons/docs/src/blocks/controls/helpers.test.ts b/code/addons/docs/src/blocks/controls/helpers.test.ts index b5661e024238..9f4ad5562c4a 100644 --- a/code/addons/docs/src/blocks/controls/helpers.test.ts +++ b/code/addons/docs/src/blocks/controls/helpers.test.ts @@ -1,3 +1,5 @@ +// @vitest-environment happy-dom + import { describe, expect, it } from 'vitest'; import { getControlId, getControlSetterButtonId } from './helpers';