From 3276cef28be1db7c8e4ff1020df39bc2e4fcc510 Mon Sep 17 00:00:00 2001 From: Bill Collins Date: Tue, 9 Dec 2025 10:57:55 +0000 Subject: [PATCH 1/6] Fix globalTypes types --- code/core/src/csf/story.ts | 6 +- code/core/src/manager-api/modules/globals.ts | 8 +- code/core/src/manager-api/root.tsx | 3 +- .../src/manager-api/tests/globals.test.ts | 10 +- .../preview-api/modules/store/GlobalsStore.ts | 4 +- .../modules/store/StoryStore.test.ts | 6 +- .../store/csf/getValuesFromArgTypes.ts | 9 -- .../store/csf/getValuesFromGlobalTypes.ts | 9 ++ .../preview-api/modules/store/csf/index.ts | 2 +- .../modules/store/csf/normalizeInputTypes.ts | 7 +- .../store/csf/normalizeProjectAnnotations.ts | 2 - .../modules/store/csf/portable-stories.ts | 4 +- .../src/toolbar/components/ToolbarManager.tsx | 11 +- .../toolbar/components/ToolbarMenuSelect.tsx | 144 +++++++++--------- .../src/toolbar/hoc/withKeyboardCycle.tsx | 70 --------- code/core/src/toolbar/types.ts | 18 ++- .../utils/normalize-toolbar-arg-type.ts | 42 ++--- code/core/src/types/modules/csf.ts | 1 - code/core/src/types/modules/story.ts | 2 - 19 files changed, 142 insertions(+), 216 deletions(-) delete mode 100644 code/core/src/preview-api/modules/store/csf/getValuesFromArgTypes.ts create mode 100644 code/core/src/preview-api/modules/store/csf/getValuesFromGlobalTypes.ts delete mode 100644 code/core/src/toolbar/hoc/withKeyboardCycle.tsx diff --git a/code/core/src/csf/story.ts b/code/core/src/csf/story.ts index 0a128a1670a2..f75957e63f3f 100644 --- a/code/core/src/csf/story.ts +++ b/code/core/src/csf/story.ts @@ -1,5 +1,6 @@ import type { RemoveIndexSignature, Simplify, UnionToIntersection } from 'type-fest'; +import type { ToolbarArgType } from '../toolbar'; import type { SBScalarType, SBType } from './SBType'; import type { CoreTypes } from './core-annotations'; @@ -165,10 +166,7 @@ export interface Globals { [name: string]: any; } export interface GlobalTypes { - [name: string]: InputType; -} -export interface StrictGlobalTypes { - [name: string]: StrictInputType; + [name: string]: ToolbarArgType; } export interface AddonTypes { diff --git a/code/core/src/manager-api/modules/globals.ts b/code/core/src/manager-api/modules/globals.ts index bdf5c23ebe7c..4c706a19ee28 100644 --- a/code/core/src/manager-api/modules/globals.ts +++ b/code/core/src/manager-api/modules/globals.ts @@ -54,16 +54,16 @@ export interface SubAPI { export const init: ModuleFn = ({ store, fullAPI, provider }) => { const api: SubAPI = { getGlobals() { - return store.getState().globals as Globals; + return store.getState().globals!; }, getUserGlobals() { - return store.getState().userGlobals as Globals; + return store.getState().userGlobals!; }, getStoryGlobals() { - return store.getState().storyGlobals as Globals; + return store.getState().storyGlobals!; }, getGlobalTypes() { - return store.getState().globalTypes as GlobalTypes; + return store.getState().globalTypes!; }, updateGlobals(newGlobals) { // Only emit the message to the local ref diff --git a/code/core/src/manager-api/root.tsx b/code/core/src/manager-api/root.tsx index 7388657698a9..cb3d85e7250a 100644 --- a/code/core/src/manager-api/root.tsx +++ b/code/core/src/manager-api/root.tsx @@ -37,6 +37,7 @@ import type { API_TestEntry, ArgTypes, Args, + GlobalTypes, Globals, Parameters, StoryId, @@ -486,7 +487,7 @@ export function useGlobals(): [ return [api.getGlobals(), api.updateGlobals, api.getStoryGlobals(), api.getUserGlobals()]; } -export function useGlobalTypes(): ArgTypes { +export function useGlobalTypes(): GlobalTypes { return useStorybookApi().getGlobalTypes(); } diff --git a/code/core/src/manager-api/tests/globals.test.ts b/code/core/src/manager-api/tests/globals.test.ts index 8045ad2526c8..8328b1ce4934 100644 --- a/code/core/src/manager-api/tests/globals.test.ts +++ b/code/core/src/manager-api/tests/globals.test.ts @@ -61,13 +61,13 @@ describe('globals API', () => { channel.emit(SET_GLOBALS, { globals: { a: 'b' }, - globalTypes: { a: { type: { name: 'string' } } }, + globalTypes: { a: {} }, } satisfies SetGlobalsPayload); expect(store.getState()).toEqual({ userGlobals: { a: 'b' }, storyGlobals: {}, globals: { a: 'b' }, - globalTypes: { a: { type: { name: 'string' } } }, + globalTypes: { a: {} }, }); }); @@ -88,13 +88,13 @@ describe('globals API', () => { channel.emit(SET_GLOBALS, { globals: { a: 'b' }, - globalTypes: { a: { type: { name: 'string' } } }, + globalTypes: { a: {} }, } satisfies SetGlobalsPayload); expect(store.getState()).toEqual({ userGlobals: { a: 'b' }, storyGlobals: {}, globals: { a: 'b' }, - globalTypes: { a: { type: { name: 'string' } } }, + globalTypes: { a: {} }, }); expect(listener).toHaveBeenCalledWith({ @@ -138,7 +138,7 @@ describe('globals API', () => { getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'ref' } } as any); channel.emit(SET_GLOBALS, { globals: { a: 'b' }, - globalTypes: { a: { type: { name: 'string' } } }, + globalTypes: { a: {} }, } satisfies SetGlobalsPayload); expect(store.getState()).toEqual({ userGlobals: {}, diff --git a/code/core/src/preview-api/modules/store/GlobalsStore.ts b/code/core/src/preview-api/modules/store/GlobalsStore.ts index 008d39d4e6b9..4677b2d3acf4 100644 --- a/code/core/src/preview-api/modules/store/GlobalsStore.ts +++ b/code/core/src/preview-api/modules/store/GlobalsStore.ts @@ -2,7 +2,7 @@ import { logger } from 'storybook/internal/client-logger'; import type { GlobalTypes, Globals } from 'storybook/internal/types'; import { DEEPLY_EQUAL, deepDiff } from './args'; -import { getValuesFromArgTypes } from './csf/getValuesFromArgTypes'; +import { getValuesFromGlobalTypes } from './csf/getValuesFromGlobalTypes'; export class GlobalsStore { // We use ! here because TS doesn't analyse the .set() function to see if it actually get set @@ -27,7 +27,7 @@ export class GlobalsStore { this.allowedGlobalNames = new Set([...Object.keys(globals), ...Object.keys(globalTypes)]); - const defaultGlobals: Globals = getValuesFromArgTypes(globalTypes); + const defaultGlobals: Globals = getValuesFromGlobalTypes(globalTypes); this.initialGlobals = { ...defaultGlobals, ...globals }; this.globals = this.initialGlobals; diff --git a/code/core/src/preview-api/modules/store/StoryStore.test.ts b/code/core/src/preview-api/modules/store/StoryStore.test.ts index bbb7af05f19e..269df092f9bc 100644 --- a/code/core/src/preview-api/modules/store/StoryStore.test.ts +++ b/code/core/src/preview-api/modules/store/StoryStore.test.ts @@ -46,7 +46,7 @@ const importFn = vi.fn(async (path) => { const projectAnnotations: ProjectAnnotations = composeConfigs([ { initialGlobals: { a: 'b' }, - globalTypes: { a: { type: 'string' } }, + globalTypes: { a: { name: 'a' } }, argTypes: { a: { type: 'string' } }, render: vi.fn(), }, @@ -88,7 +88,7 @@ describe('StoryStore', () => { const store = new StoryStore(storyIndex, importFn, projectAnnotations); expect(store.projectAnnotations!.globalTypes).toEqual({ - a: { name: 'a', type: { name: 'string' } }, + a: { name: 'a' }, }); expect(store.projectAnnotations!.argTypes).toEqual({ a: { name: 'a', type: { name: 'string' } }, @@ -100,7 +100,7 @@ describe('StoryStore', () => { store.setProjectAnnotations(projectAnnotations); expect(store.projectAnnotations!.globalTypes).toEqual({ - a: { name: 'a', type: { name: 'string' } }, + a: { name: 'a' }, }); expect(store.projectAnnotations!.argTypes).toEqual({ a: { name: 'a', type: { name: 'string' } }, diff --git a/code/core/src/preview-api/modules/store/csf/getValuesFromArgTypes.ts b/code/core/src/preview-api/modules/store/csf/getValuesFromArgTypes.ts deleted file mode 100644 index a69a7062b8c6..000000000000 --- a/code/core/src/preview-api/modules/store/csf/getValuesFromArgTypes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ArgTypes } from 'storybook/internal/types'; - -export const getValuesFromArgTypes = (argTypes: ArgTypes = {}) => - Object.entries(argTypes).reduce((acc, [arg, { defaultValue }]) => { - if (typeof defaultValue !== 'undefined') { - acc[arg] = defaultValue; - } - return acc; - }, {} as ArgTypes); diff --git a/code/core/src/preview-api/modules/store/csf/getValuesFromGlobalTypes.ts b/code/core/src/preview-api/modules/store/csf/getValuesFromGlobalTypes.ts new file mode 100644 index 000000000000..8cea75ec4e1a --- /dev/null +++ b/code/core/src/preview-api/modules/store/csf/getValuesFromGlobalTypes.ts @@ -0,0 +1,9 @@ +import type { GlobalTypes, Globals } from 'storybook/internal/types'; + +export const getValuesFromGlobalTypes = (argTypes: GlobalTypes = {}): Globals => + Object.entries(argTypes).reduce((acc, [arg, { defaultValue }]) => { + if (typeof defaultValue !== 'undefined') { + acc[arg] = defaultValue; + } + return acc; + }, {}); diff --git a/code/core/src/preview-api/modules/store/csf/index.ts b/code/core/src/preview-api/modules/store/csf/index.ts index 953f9f5b64b8..b43144d461f9 100644 --- a/code/core/src/preview-api/modules/store/csf/index.ts +++ b/code/core/src/preview-api/modules/store/csf/index.ts @@ -5,7 +5,7 @@ export * from './prepareStory'; export * from './normalizeComponentAnnotations'; export * from './normalizeProjectAnnotations'; export * from './normalizeArrays'; -export * from './getValuesFromArgTypes'; +export * from './getValuesFromGlobalTypes'; export * from './composeConfigs'; export * from './stepRunners'; export * from './portable-stories'; diff --git a/code/core/src/preview-api/modules/store/csf/normalizeInputTypes.ts b/code/core/src/preview-api/modules/store/csf/normalizeInputTypes.ts index c2d08e576e2f..e36f81cc9c06 100644 --- a/code/core/src/preview-api/modules/store/csf/normalizeInputTypes.ts +++ b/code/core/src/preview-api/modules/store/csf/normalizeInputTypes.ts @@ -1,9 +1,7 @@ import type { ArgTypes, - GlobalTypes, InputType, StrictArgTypes, - StrictGlobalTypes, StrictInputType, } from 'storybook/internal/types'; @@ -34,6 +32,5 @@ export const normalizeInputType = (inputType: InputType, key: string): StrictInp return normalized; }; -export const normalizeInputTypes = ( - inputTypes: ArgTypes | GlobalTypes -): StrictArgTypes | StrictGlobalTypes => mapValues(inputTypes, normalizeInputType); +export const normalizeInputTypes = (inputTypes: ArgTypes): StrictArgTypes => + mapValues(inputTypes, normalizeInputType); diff --git a/code/core/src/preview-api/modules/store/csf/normalizeProjectAnnotations.ts b/code/core/src/preview-api/modules/store/csf/normalizeProjectAnnotations.ts index ecdd6046f18e..f4e00e0e0165 100644 --- a/code/core/src/preview-api/modules/store/csf/normalizeProjectAnnotations.ts +++ b/code/core/src/preview-api/modules/store/csf/normalizeProjectAnnotations.ts @@ -16,7 +16,6 @@ import { normalizeInputTypes } from './normalizeInputTypes'; // That makes sense to me as it avoids the need for both WP + Vite to call composeConfigs at the right time. export function normalizeProjectAnnotations({ argTypes, - globalTypes, argTypesEnhancers, decorators, loaders, @@ -27,7 +26,6 @@ export function normalizeProjectAnnotations({ }: ProjectAnnotations): NormalizedProjectAnnotations { return { ...(argTypes && { argTypes: normalizeInputTypes(argTypes as ArgTypes) }), - ...(globalTypes && { globalTypes: normalizeInputTypes(globalTypes) }), decorators: normalizeArrays(decorators), loaders: normalizeArrays(loaders), beforeEach: normalizeArrays(beforeEach), diff --git a/code/core/src/preview-api/modules/store/csf/portable-stories.ts b/code/core/src/preview-api/modules/store/csf/portable-stories.ts index 7b5d1aa6c7ee..60fa3d7450bb 100644 --- a/code/core/src/preview-api/modules/store/csf/portable-stories.ts +++ b/code/core/src/preview-api/modules/store/csf/portable-stories.ts @@ -32,7 +32,7 @@ import { import { ReporterAPI } from '../reporter-api'; import { composeConfigs } from './composeConfigs'; import { getCsfFactoryAnnotations } from './csf-factory-utils'; -import { getValuesFromArgTypes } from './getValuesFromArgTypes'; +import { getValuesFromGlobalTypes } from './getValuesFromGlobalTypes'; import { normalizeComponentAnnotations } from './normalizeComponentAnnotations'; import { normalizeProjectAnnotations } from './normalizeProjectAnnotations'; import { normalizeStory } from './normalizeStory'; @@ -131,7 +131,7 @@ export function composeStory { const globalTypes = useGlobalTypes(); - const globalIds = Object.keys(globalTypes).filter((id) => !!globalTypes[id].toolbar); + const hasToolbars = Object.keys(globalTypes).some((id) => !!globalTypes[id].toolbar); - if (!globalIds.length) { + if (!hasToolbars) { return null; } return ( <> - {globalIds.map((id) => { - const normalizedArgType = normalizeArgType(id, globalTypes[id] as ToolbarArgType); + {Object.keys(globalTypes).map((id) => { + const normalizedArgType = normalizeArgType(id, globalTypes[id]); - return ; + return normalizedArgType && ; })} ); diff --git a/code/core/src/toolbar/components/ToolbarMenuSelect.tsx b/code/core/src/toolbar/components/ToolbarMenuSelect.tsx index 886da8cb4acc..b2331d0b4ea3 100644 --- a/code/core/src/toolbar/components/ToolbarMenuSelect.tsx +++ b/code/core/src/toolbar/components/ToolbarMenuSelect.tsx @@ -7,8 +7,6 @@ import { useGlobals } from 'storybook/manager-api'; import { styled } from 'storybook/theming'; import { Icons } from '../../components/components/icon/icon'; -import type { WithKeyboardCycleProps } from '../hoc/withKeyboardCycle'; -import { withKeyboardCycle } from '../hoc/withKeyboardCycle'; import type { ToolbarItem, ToolbarMenuProps } from '../types'; import { getSelectedIcon, getSelectedTitle } from '../utils/get-selected'; @@ -26,84 +24,80 @@ const ToolbarMenuItemMiddle = styled('div')({ flex: 1, }); -type ToolbarMenuSelectProps = ToolbarMenuProps & WithKeyboardCycleProps; +export const ToolbarMenuSelect: FC = ({ + id, + name, + description, + toolbar: { icon: _icon, items, title: _title, preventDynamicIcon, dynamicTitle }, +}) => { + const [globals, updateGlobals, storyGlobals] = useGlobals(); -export const ToolbarMenuSelect: FC = withKeyboardCycle( - ({ - id, - name, - description, - toolbar: { icon: _icon, items, title: _title, preventDynamicIcon, dynamicTitle }, - }) => { - const [globals, updateGlobals, storyGlobals] = useGlobals(); + const currentValue = globals[id]; + const isOverridden = id in storyGlobals; + let icon = _icon; + let title = _title; - const currentValue = globals[id]; - const isOverridden = id in storyGlobals; - let icon = _icon; - let title = _title; - - if (!preventDynamicIcon) { - icon = getSelectedIcon({ currentValue, items }) || icon; - } + if (!preventDynamicIcon) { + icon = getSelectedIcon({ currentValue, items }) || icon; + } - if (dynamicTitle) { - title = getSelectedTitle({ currentValue, items }) || title; - } + if (dynamicTitle) { + title = getSelectedTitle({ currentValue, items }) || title; + } - if (!title && !icon) { - console.warn(`Toolbar '${name}' has no title or icon`); - } + if (!title && !icon) { + console.warn(`Toolbar '${name}' has no title or icon`); + } - const resetItem = items.find((item) => item.type === 'reset'); - const resetLabel = resetItem?.title; - const options = items - .filter((item): item is ToolbarItem => item.type === 'item') - .map((item) => { - const itemTitle = item.title ?? item.value ?? 'Untitled'; - const iconComponent = - !item.hideIcon && item.icon ? ( - - ) : undefined; + const resetItem = items.find((item) => item.type === 'reset'); + const resetLabel = resetItem?.title; + const options = items + .filter((item): item is ToolbarItem => item.type === 'item') + .map((item) => { + const itemTitle = item.title ?? item.value ?? 'Untitled'; + const iconComponent = + !item.hideIcon && item.icon ? ( + + ) : undefined; - if (item.right) { - return { - title: itemTitle, - value: item.value, - children: ( - - {iconComponent} - {item.title ?? item.value} - {item.right} - - ), - }; - } else { - return { - title: itemTitle, - value: item.value, - icon: iconComponent, - }; - } - }); + if (item.right) { + return { + title: itemTitle, + value: item.value, + children: ( + + {iconComponent} + {item.title ?? item.value} + {item.right} + + ), + }; + } else { + return { + title: itemTitle, + value: item.value, + icon: iconComponent, + }; + } + }); - // FIXME: for SB 10 we would want description to become an aria-description, and to add an - // ariaLabel prop to tools with an automigration switching current description to ariaLabel - const ariaLabel = description || title || name || id; + // FIXME: for SB 10 we would want description to become an aria-description, and to add an + // ariaLabel prop to tools with an automigration switching current description to ariaLabel + const ariaLabel = description || title || name || id; - return ( - - ); - } -); + return ( + + ); +}; diff --git a/code/core/src/toolbar/hoc/withKeyboardCycle.tsx b/code/core/src/toolbar/hoc/withKeyboardCycle.tsx deleted file mode 100644 index c552339126fb..000000000000 --- a/code/core/src/toolbar/hoc/withKeyboardCycle.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { useCallback, useEffect, useRef } from 'react'; - -import { useGlobals, useStorybookApi } from 'storybook/manager-api'; - -import type { ToolbarMenuProps } from '../types'; -import { createCycleValueArray } from '../utils/create-cycle-value-array'; -import { registerShortcuts } from '../utils/register-shortcuts'; - -export type WithKeyboardCycleProps = { - cycleValues?: string[]; -}; - -export const withKeyboardCycle = (Component: React.ComponentType) => { - const WithKeyboardCycle = (props: ToolbarMenuProps) => { - const { - id, - toolbar: { items, shortcuts }, - } = props; - - const api = useStorybookApi(); - const [globals, updateGlobals] = useGlobals(); - const cycleValues = useRef([]); - const currentValue = globals[id]; - - const reset = useCallback(() => { - updateGlobals({ [id]: '' }); - }, [updateGlobals]); - - const setNext = useCallback(() => { - const values = cycleValues.current; - const currentIndex = values.indexOf(currentValue); - const currentIsLast = currentIndex === values.length - 1; - - const newCurrentIndex = currentIsLast ? 0 : currentIndex + 1; - const newCurrent = cycleValues.current[newCurrentIndex]; - - updateGlobals({ [id]: newCurrent }); - }, [cycleValues, currentValue, updateGlobals]); - - const setPrevious = useCallback(() => { - const values = cycleValues.current; - const indexOf = values.indexOf(currentValue); - const currentIndex = indexOf > -1 ? indexOf : 0; - const currentIsFirst = currentIndex === 0; - - const newCurrentIndex = currentIsFirst ? values.length - 1 : currentIndex - 1; - const newCurrent = cycleValues.current[newCurrentIndex]; - - updateGlobals({ [id]: newCurrent }); - }, [cycleValues, currentValue, updateGlobals]); - - useEffect(() => { - if (shortcuts) { - registerShortcuts(api, id, { - next: { ...shortcuts.next, action: setNext }, - previous: { ...shortcuts.previous, action: setPrevious }, - reset: { ...shortcuts.reset, action: reset }, - }); - } - }, [api, id, shortcuts, setNext, setPrevious, reset]); - - useEffect(() => { - cycleValues.current = createCycleValueArray(items); - }, []); - - return ; - }; - - return WithKeyboardCycle; -}; diff --git a/code/core/src/toolbar/types.ts b/code/core/src/toolbar/types.ts index ce8fb93c734f..861551cbd412 100644 --- a/code/core/src/toolbar/types.ts +++ b/code/core/src/toolbar/types.ts @@ -26,7 +26,7 @@ export interface NormalizedToolbarConfig { /** The label to show for this toolbar item */ title?: string; /** Choose an icon to show for this toolbar item */ - icon: IconsProps['icon']; + icon?: IconsProps['icon']; /** Set to true to prevent default update of icon to match any present selected items icon */ preventDynamicIcon?: boolean; items: ToolbarItem[]; @@ -35,16 +35,22 @@ export interface NormalizedToolbarConfig { dynamicTitle?: boolean; } -export type NormalizedToolbarArgType = InputType & { +export type NormalizedToolbarArgType = { + name: string; + description: string; + defaultValue?: any; toolbar: NormalizedToolbarConfig; }; -export type ToolbarConfig = NormalizedToolbarConfig & { - items: string[] | ToolbarItem[]; +export type ToolbarConfig = Omit & { + items: (string | ToolbarItem)[]; }; -export type ToolbarArgType = InputType & { - toolbar: ToolbarConfig; +export type ToolbarArgType = { + name?: string; + description?: string; + defaultValue?: any; + toolbar?: ToolbarConfig; }; export type ToolbarMenuProps = NormalizedToolbarArgType & { id: string }; diff --git a/code/core/src/toolbar/utils/normalize-toolbar-arg-type.ts b/code/core/src/toolbar/utils/normalize-toolbar-arg-type.ts index effc80b3e450..8a9942d6a7cc 100644 --- a/code/core/src/toolbar/utils/normalize-toolbar-arg-type.ts +++ b/code/core/src/toolbar/utils/normalize-toolbar-arg-type.ts @@ -8,23 +8,29 @@ const defaultItemValues: ToolbarItem = { export const normalizeArgType = ( key: string, argType: ToolbarArgType -): NormalizedToolbarArgType => ({ - ...argType, - name: argType.name || key, - description: argType.description || key, - toolbar: { - ...argType.toolbar, - items: argType.toolbar.items.map((_item) => { - const item = typeof _item === 'string' ? { value: _item, title: _item } : _item; +): NormalizedToolbarArgType | null => { + const toolbar = argType.toolbar; + if (!toolbar) { + return null; + } + return { + ...argType, + name: argType.name || key, + description: argType.description || key, + toolbar: { + ...argType.toolbar, + items: toolbar.items.map((_item) => { + const item = typeof _item === 'string' ? { value: _item, title: _item } : _item; - // Cater for the special type "reset" which will reset value and also icon - // of toolbar button if any icon was present on toolbar to begin with - if (item.type === 'reset' && argType.toolbar.icon) { - item.icon = argType.toolbar.icon; - item.hideIcon = true; - } + // Cater for the special type "reset" which will reset value and also icon + // of toolbar button if any icon was present on toolbar to begin with + if (item.type === 'reset' && toolbar.icon) { + item.icon = toolbar.icon; + item.hideIcon = true; + } - return { ...defaultItemValues, ...item }; - }), - }, -}); + return { ...defaultItemValues, ...item }; + }), + }, + }; +}; diff --git a/code/core/src/types/modules/csf.ts b/code/core/src/types/modules/csf.ts index ad2891060eca..24cfd33b309a 100644 --- a/code/core/src/types/modules/csf.ts +++ b/code/core/src/types/modules/csf.ts @@ -62,7 +62,6 @@ export type { StoryName, StrictArgs, StrictArgTypes, - StrictGlobalTypes, StrictInputType, Tag, } from 'storybook/internal/csf'; diff --git a/code/core/src/types/modules/story.ts b/code/core/src/types/modules/story.ts index 3b5d204dfc9b..c84e7fcc6391 100644 --- a/code/core/src/types/modules/story.ts +++ b/code/core/src/types/modules/story.ts @@ -22,7 +22,6 @@ import type { StoryIdentifier, StoryName, StrictArgTypes, - StrictGlobalTypes, } from './csf'; // Store Types @@ -57,7 +56,6 @@ export type NormalizedProjectAnnotations 'decorators' | 'loaders' | 'runStep' | 'beforeAll' > & { argTypes?: StrictArgTypes; - globalTypes?: StrictGlobalTypes; decorators?: DecoratorFunction[]; loaders?: LoaderFunction[]; runStep: StepRunner; From 644b6b0dc888b086e33aeb8dc7608db2cb27674d Mon Sep 17 00:00:00 2001 From: Bill Collins Date: Tue, 9 Dec 2025 20:25:42 +0000 Subject: [PATCH 2/6] Reinstate keyboard shortcuts --- .../toolbar/components/ToolbarMenuSelect.tsx | 102 ++++++++++++------ .../toolbar/utils/create-cycle-value-array.ts | 11 -- code/core/src/toolbar/utils/get-selected.ts | 21 +--- .../src/toolbar/utils/register-shortcuts.ts | 6 +- 4 files changed, 73 insertions(+), 67 deletions(-) delete mode 100644 code/core/src/toolbar/utils/create-cycle-value-array.ts diff --git a/code/core/src/toolbar/components/ToolbarMenuSelect.tsx b/code/core/src/toolbar/components/ToolbarMenuSelect.tsx index b2331d0b4ea3..26902a412f29 100644 --- a/code/core/src/toolbar/components/ToolbarMenuSelect.tsx +++ b/code/core/src/toolbar/components/ToolbarMenuSelect.tsx @@ -3,12 +3,13 @@ import React from 'react'; import { Select } from 'storybook/internal/components'; -import { useGlobals } from 'storybook/manager-api'; +import { useGlobals, useStorybookApi } from 'storybook/manager-api'; import { styled } from 'storybook/theming'; import { Icons } from '../../components/components/icon/icon'; import type { ToolbarItem, ToolbarMenuProps } from '../types'; -import { getSelectedIcon, getSelectedTitle } from '../utils/get-selected'; +import { getSelectedItem } from '../utils/get-selected'; +import { registerShortcuts } from '../utils/register-shortcuts'; // We can't remove the Icons component just yet because there's no way for now to import icons // in the preview directly. Before having a better solution, we are going to keep the Icons component @@ -28,8 +29,9 @@ export const ToolbarMenuSelect: FC = ({ id, name, description, - toolbar: { icon: _icon, items, title: _title, preventDynamicIcon, dynamicTitle }, + toolbar: { icon: _icon, items, title: _title, preventDynamicIcon, dynamicTitle, shortcuts }, }) => { + const api = useStorybookApi(); const [globals, updateGlobals, storyGlobals] = useGlobals(); const currentValue = globals[id]; @@ -38,11 +40,11 @@ export const ToolbarMenuSelect: FC = ({ let title = _title; if (!preventDynamicIcon) { - icon = getSelectedIcon({ currentValue, items }) || icon; + icon = getSelectedItem({ currentValue, items })?.icon || icon; } if (dynamicTitle) { - title = getSelectedTitle({ currentValue, items }) || title; + title = getSelectedItem({ currentValue, items })?.title || title; } if (!title && !icon) { @@ -51,35 +53,69 @@ export const ToolbarMenuSelect: FC = ({ const resetItem = items.find((item) => item.type === 'reset'); const resetLabel = resetItem?.title; - const options = items - .filter((item): item is ToolbarItem => item.type === 'item') - .map((item) => { - const itemTitle = item.title ?? item.value ?? 'Untitled'; - const iconComponent = - !item.hideIcon && item.icon ? ( - - ) : undefined; + const options = React.useMemo( + () => + items + .filter((item): item is ToolbarItem => item.type === 'item') + .map((item) => { + const itemTitle = item.title ?? item.value ?? 'Untitled'; + const iconComponent = + !item.hideIcon && item.icon ? ( + + ) : undefined; - if (item.right) { - return { - title: itemTitle, - value: item.value, - children: ( - - {iconComponent} - {item.title ?? item.value} - {item.right} - - ), - }; - } else { - return { - title: itemTitle, - value: item.value, - icon: iconComponent, - }; - } - }); + if (item.right) { + return { + title: itemTitle, + value: item.value, + children: ( + + {iconComponent} + {item.title ?? item.value} + {item.right} + + ), + }; + } else { + return { + title: itemTitle, + value: item.value, + icon: iconComponent, + }; + } + }), + [items] + ); + + React.useEffect(() => { + if (shortcuts) { + const length = options.length; + void registerShortcuts(api, id, { + next: { + ...shortcuts.next, + action: () => { + const idx = options.findIndex((i) => i.value === globals[id]); + const nextIdx = idx < 0 ? 0 : (idx + 1) % length; + updateGlobals({ [id]: options[nextIdx].value }); + }, + }, + previous: { + ...shortcuts.previous, + action: () => { + const idx = options.findIndex((i) => i.value === globals[id]); + const previousIdx = idx < 0 ? length - 1 : (idx + length - 1) % length; + updateGlobals({ [id]: options[previousIdx].value }); + }, + }, + reset: { + ...shortcuts.reset, + action: () => { + updateGlobals({ [id]: undefined }); + }, + }, + }); + } + }, [api, id, shortcuts, globals, options, updateGlobals]); // FIXME: for SB 10 we would want description to become an aria-description, and to add an // ariaLabel prop to tools with an automigration switching current description to ariaLabel diff --git a/code/core/src/toolbar/utils/create-cycle-value-array.ts b/code/core/src/toolbar/utils/create-cycle-value-array.ts deleted file mode 100644 index e9d12981a2da..000000000000 --- a/code/core/src/toolbar/utils/create-cycle-value-array.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ToolbarItem, ToolbarItemType } from '../types'; - -const disallowedCycleableItemTypes: Array = ['reset']; - -export const createCycleValueArray = (items: ToolbarItem[]) => { - // Do not allow items in the cycle arrays that are conditional in placement - const valueArray = items - .filter((item) => !disallowedCycleableItemTypes.includes(item.type as ToolbarItemType)) - .map((item) => item.value); - return valueArray; -}; diff --git a/code/core/src/toolbar/utils/get-selected.ts b/code/core/src/toolbar/utils/get-selected.ts index 610e1fa9a68f..201d07dcb834 100644 --- a/code/core/src/toolbar/utils/get-selected.ts +++ b/code/core/src/toolbar/utils/get-selected.ts @@ -6,24 +6,5 @@ interface GetSelectedItemProps { } export const getSelectedItem = ({ currentValue, items }: GetSelectedItemProps) => { - const selectedItem = - currentValue != null && - items.find((item) => item.value === currentValue && item.type !== 'reset'); - return selectedItem; -}; - -export const getSelectedIcon = ({ currentValue, items }: GetSelectedItemProps) => { - const selectedItem = getSelectedItem({ currentValue, items }); - if (selectedItem) { - return selectedItem.icon; - } - return undefined; -}; - -export const getSelectedTitle = ({ currentValue, items }: GetSelectedItemProps) => { - const selectedItem = getSelectedItem({ currentValue, items }); - if (selectedItem) { - return selectedItem.title; - } - return undefined; + return items.find((item) => item.value === currentValue && item.type !== 'reset'); }; diff --git a/code/core/src/toolbar/utils/register-shortcuts.ts b/code/core/src/toolbar/utils/register-shortcuts.ts index 76309def898c..ae310afb615d 100644 --- a/code/core/src/toolbar/utils/register-shortcuts.ts +++ b/code/core/src/toolbar/utils/register-shortcuts.ts @@ -10,7 +10,7 @@ interface Shortcuts { } export const registerShortcuts = async (api: API, id: string, shortcuts: Shortcuts) => { - if (shortcuts && shortcuts.next) { + if (shortcuts.next) { await api.setAddonShortcut(TOOLBAR_ID, { label: shortcuts.next.label, defaultShortcut: shortcuts.next.keys, @@ -19,7 +19,7 @@ export const registerShortcuts = async (api: API, id: string, shortcuts: Shortcu }); } - if (shortcuts && shortcuts.previous) { + if (shortcuts.previous) { await api.setAddonShortcut(TOOLBAR_ID, { label: shortcuts.previous.label, defaultShortcut: shortcuts.previous.keys, @@ -28,7 +28,7 @@ export const registerShortcuts = async (api: API, id: string, shortcuts: Shortcu }); } - if (shortcuts && shortcuts.reset) { + if (shortcuts.reset) { await api.setAddonShortcut(TOOLBAR_ID, { label: shortcuts.reset.label, defaultShortcut: shortcuts.reset.keys, From 9ce7be2e5ce6d65652d9f9019de5f2e4c820eae6 Mon Sep 17 00:00:00 2001 From: Bill Collins Date: Tue, 23 Dec 2025 19:14:26 +0000 Subject: [PATCH 3/6] Update code/core/src/preview-api/modules/store/csf/getValuesFromGlobalTypes.ts Co-authored-by: Norbert de Langen --- .../preview-api/modules/store/csf/getValuesFromGlobalTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/core/src/preview-api/modules/store/csf/getValuesFromGlobalTypes.ts b/code/core/src/preview-api/modules/store/csf/getValuesFromGlobalTypes.ts index 8cea75ec4e1a..2de25f420312 100644 --- a/code/core/src/preview-api/modules/store/csf/getValuesFromGlobalTypes.ts +++ b/code/core/src/preview-api/modules/store/csf/getValuesFromGlobalTypes.ts @@ -1,7 +1,7 @@ import type { GlobalTypes, Globals } from 'storybook/internal/types'; -export const getValuesFromGlobalTypes = (argTypes: GlobalTypes = {}): Globals => - Object.entries(argTypes).reduce((acc, [arg, { defaultValue }]) => { +export const getValuesFromGlobalTypes = (globalTypes: GlobalTypes = {}): Globals => + Object.entries(globalTypes).reduce((acc, [arg, { defaultValue }]) => { if (typeof defaultValue !== 'undefined') { acc[arg] = defaultValue; } From 9bb0928039ddb5eabc8b3bd1e3aeeea745130b52 Mon Sep 17 00:00:00 2001 From: Bill Collins Date: Tue, 13 Jan 2026 14:56:09 +0000 Subject: [PATCH 4/6] Add loose index signature for compatibility with InputType --- code/core/src/toolbar/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/code/core/src/toolbar/types.ts b/code/core/src/toolbar/types.ts index 861551cbd412..b46197621d74 100644 --- a/code/core/src/toolbar/types.ts +++ b/code/core/src/toolbar/types.ts @@ -51,6 +51,11 @@ export type ToolbarArgType = { description?: string; defaultValue?: any; toolbar?: ToolbarConfig; + /** + * @deprecated This loose index signature has been added for compatibility with InputType, and + * will be removed in Storybook 11 + */ + [key: string]: any; }; export type ToolbarMenuProps = NormalizedToolbarArgType & { id: string }; From dc1a0dd7bc66115883ff07ca9ad2dff77e3e399f Mon Sep 17 00:00:00 2001 From: Bill Collins Date: Tue, 13 Jan 2026 15:01:26 +0000 Subject: [PATCH 5/6] Add typing to template preview.ts --- code/core/template/stories/preview.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/core/template/stories/preview.ts b/code/core/template/stories/preview.ts index ac0ad988e0eb..df651dc3d562 100644 --- a/code/core/template/stories/preview.ts +++ b/code/core/template/stories/preview.ts @@ -1,4 +1,4 @@ -import type { PartialStoryFn, StoryContext } from 'storybook/internal/types'; +import type { GlobalTypes, PartialStoryFn, StoryContext } from 'storybook/internal/types'; declare global { interface Window { @@ -105,4 +105,4 @@ export const globalTypes = { ], }, }, -}; +} satisfies GlobalTypes; From f22879a580abb31353f8c628b0366ea1316868c4 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 20 Jan 2026 09:39:19 +0100 Subject: [PATCH 6/6] Fix indentation in normalizeArgType function to ensure proper code formatting --- code/core/src/toolbar/utils/normalize-toolbar-arg-type.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/core/src/toolbar/utils/normalize-toolbar-arg-type.ts b/code/core/src/toolbar/utils/normalize-toolbar-arg-type.ts index 7cf75dcc7c85..9d11a0fa733a 100644 --- a/code/core/src/toolbar/utils/normalize-toolbar-arg-type.ts +++ b/code/core/src/toolbar/utils/normalize-toolbar-arg-type.ts @@ -27,8 +27,8 @@ export const normalizeArgType = ( if (item.type === 'reset' && toolbar.icon) { item.icon = toolbar.icon; item.hideIcon = true; - item.value = undefined; - } + item.value = undefined; + } return { ...defaultItemValues, ...item }; }),