diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 846e510aec6d..7de9f16085e9 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,15 @@ +## 10.1.0-beta.5 + +- Checklist: Autocomplete "See what's new" on URL navigation - [#33167](https://github.com/storybookjs/storybook/pull/33167), thanks @ghengeveld! +- Core: Fix testing widget focus outline - [#33172](https://github.com/storybookjs/storybook/pull/33172), thanks @ghengeveld! +- Core: Rename `Listbox` component to `ActionList` and use it in `TagsFilterPanel` - [#33140](https://github.com/storybookjs/storybook/pull/33140), thanks @ghengeveld! +- UI: Add padding for ArgsTable shadow in TabbedArgsTable - [#33034](https://github.com/storybookjs/storybook/pull/33034), thanks @Sidnioulz! +- UI: Fix crashes in Select when passed falsy non-string options - [#33164](https://github.com/storybookjs/storybook/pull/33164), thanks @Sidnioulz! +- UI: Fix regression on addon panel empty content fontsize - [#33021](https://github.com/storybookjs/storybook/pull/33021), thanks @Sidnioulz! +- UI: Fix trivial RefBlocks ARIA violations - [#33026](https://github.com/storybookjs/storybook/pull/33026), thanks @Sidnioulz! +- UI: Refocus search input after clearing it - [#33165](https://github.com/storybookjs/storybook/pull/33165), thanks @Sidnioulz! +- UI: Rework default background of Color swatch for dark mode - [#33023](https://github.com/storybookjs/storybook/pull/33023), thanks @Sidnioulz! + ## 10.1.0-beta.4 - Angular: Migrate from RxJS to async/await in command builders and run Compodoc utility as spinner - [#33156](https://github.com/storybookjs/storybook/pull/33156), thanks @valentinpalkovic! diff --git a/code/addons/docs/src/blocks/components/ArgsTable/ArgsTable.tsx b/code/addons/docs/src/blocks/components/ArgsTable/ArgsTable.tsx index 5b7c943599c2..b42c5a949b77 100644 --- a/code/addons/docs/src/blocks/components/ArgsTable/ArgsTable.tsx +++ b/code/addons/docs/src/blocks/components/ArgsTable/ArgsTable.tsx @@ -106,6 +106,7 @@ export const TableWrapper = styled.table<{ // Makes border alignment consistent w/other DocBlocks marginInline: inAddonPanel || inTabPanel ? 0 : 1, + paddingInline: inTabPanel ? 3 : 0, tbody: { // Safari doesn't love shadows on tbody so we need to use a shadow filter. In order to do this, diff --git a/code/addons/docs/src/blocks/controls/Color.tsx b/code/addons/docs/src/blocks/controls/Color.tsx index 715b8601c50e..3c3610c21621 100644 --- a/code/addons/docs/src/blocks/controls/Color.tsx +++ b/code/addons/docs/src/blocks/controls/Color.tsx @@ -42,7 +42,8 @@ const Swatches = styled.div({ width: 200, }); -const swatchBackground = `url('data:image/svg+xml;charset=utf-8,')`; +const swatchBackground = (isDark: boolean) => + `url('data:image/svg+xml;charset=utf-8,')`; const SwatchColor = styled(Button)<{ selected?: boolean; value: string }>( ({ value, selected, theme }) => ({ @@ -56,7 +57,7 @@ const SwatchColor = styled(Button)<{ selected?: boolean; value: string }>( '&, &:hover': { background: 'unset', backgroundColor: 'unset', - backgroundImage: `linear-gradient(${value}, ${value}), ${swatchBackground}, linear-gradient(hsl(0 0 100 / .4), hsl(0 0 100 / .4))`, + backgroundImage: `linear-gradient(${value}, ${value}), ${swatchBackground(theme.base === 'dark')}`, }, }) ); diff --git a/code/addons/pseudo-states/src/manager/PseudoStateTool.tsx b/code/addons/pseudo-states/src/manager/PseudoStateTool.tsx index 1f90a36cc42c..1d816529be8a 100644 --- a/code/addons/pseudo-states/src/manager/PseudoStateTool.tsx +++ b/code/addons/pseudo-states/src/manager/PseudoStateTool.tsx @@ -36,7 +36,8 @@ export const PseudoStateTool = () => { multiSelect onChange={(selected) => { updateGlobals({ - [PARAM_KEY]: selected.reduce((acc, curr) => ({ ...acc, [curr]: true }), {}), + // We know curr is a string because we are using string values in options + [PARAM_KEY]: selected.reduce((acc, curr) => ({ ...acc, [curr as string]: true }), {}), }); }} /> diff --git a/code/addons/vitest/src/components/TestProviderRender.tsx b/code/addons/vitest/src/components/TestProviderRender.tsx index 2efb1d4a38b2..68a79a421aff 100644 --- a/code/addons/vitest/src/components/TestProviderRender.tsx +++ b/code/addons/vitest/src/components/TestProviderRender.tsx @@ -1,9 +1,9 @@ import React, { type ComponentProps, type FC } from 'react'; import { + ActionList, Button, Form, - ListItem, ProgressSpinner, ToggleButton, } from 'storybook/internal/components'; @@ -27,10 +27,11 @@ import type { StatusValueToStoryIds } from '../use-test-provider-state'; import { Description } from './Description'; import { TestStatusIcon } from './TestStatusIcon'; -const Container = styled.div({ +const Container = styled.div<{ inContextMenu?: boolean }>(({ inContextMenu }) => ({ display: 'flex', flexDirection: 'column', -}); + paddingBottom: inContextMenu ? 0 : 1, +})); const Heading = styled.div({ display: 'flex', @@ -57,8 +58,8 @@ const Actions = styled.div({ gap: 4, }); -const Extras = styled.div({ - marginBottom: 2, +const StyledActionList = styled(ActionList)({ + padding: 0, }); const Muted = styled.span(({ theme }) => ({ @@ -69,11 +70,6 @@ const Progress = styled(ProgressSpinner)({ margin: 4, }); -const Row = styled.div({ - display: 'flex', - gap: 4, -}); - const StopIcon = styled(StopAltIcon)({ width: 10, }); @@ -147,7 +143,7 @@ export const TestProviderRender: FC = ({ : ['unknown', 'Run tests to see accessibility results']; return ( - + {entry ? ( @@ -257,15 +253,20 @@ export const TestProviderRender: FC = ({ )} - - - } - /> - - + + {!entry && ( - - Coverage (unavailable) : 'Coverage'} - icon={ + + + @@ -319,34 +316,27 @@ export const TestProviderRender: FC = ({ })) } /> - } - /> - - {/* FIXME: aria labels were not 100% consistent with the tooltip logic. Double check this logic during review please! */} + + + {watching ? Coverage (unavailable) : 'Coverage'} + + {watching || (currentRun.triggeredBy && !FULL_RUN_TRIGGERS.includes(currentRun.triggeredBy)) ? ( - + ) : currentRun.coverageSummary ? ( - + ) : ( - + )} - + )} {hasA11yAddon && ( - - + {entry ? ( + Accessibility + ) : ( + + @@ -401,14 +389,12 @@ export const TestProviderRender: FC = ({ })) } /> - ) - } - /> - - + + )} - + ); }; diff --git a/code/core/src/backgrounds/components/Tool.tsx b/code/core/src/backgrounds/components/Tool.tsx index 1b6347ebbb2a..2b5716993582 100644 --- a/code/core/src/backgrounds/components/Tool.tsx +++ b/code/core/src/backgrounds/components/Tool.tsx @@ -54,7 +54,6 @@ interface PureProps { const Pure = memo(function PureTool(props: PureProps) { const { - item, length, updateGlobals, backgroundMap, @@ -104,7 +103,7 @@ const Pure = memo(function PureTool(props: PureProps) { tooltip={isLocked ? 'Background set by story parameters' : 'Change background'} defaultOptions={backgroundName} options={options} - onSelect={(selected) => update({ value: selected, grid: isGrid })} + onSelect={(selected) => update({ value: selected as string, grid: isGrid })} /> ) : null} diff --git a/code/core/src/components/components/ActionList/ActionList.stories.tsx b/code/core/src/components/components/ActionList/ActionList.stories.tsx new file mode 100644 index 000000000000..d2f011af0b59 --- /dev/null +++ b/code/core/src/components/components/ActionList/ActionList.stories.tsx @@ -0,0 +1,113 @@ +import { CheckIcon, EllipsisIcon, PlayAllHollowIcon } from '@storybook/icons'; + +import { Badge, Form, ProgressSpinner } from '../..'; +import preview from '../../../../../.storybook/preview'; +import { Shortcut } from '../../../manager/container/Menu'; +import { ActionList } from './ActionList'; + +const meta = preview.meta({ + component: ActionList, + decorators: [(Story) =>
{Story()}
], +}); + +export default meta; + +export const Default = meta.story({ + render: () => ( + + + Text item + + + + + + Action item + + + Cool + + + + Hover action + + + Cool + + + + With a button + Go + + + + With an inline button + + + 25% + + + + + + With a badge + Check it out + + + + + + With a checkbox + + + + + + Active with an icon + + + + + + Some very long text which will wrap when the container is too narrow + + + + + Some very long text which will ellipsize when the container is too narrow + + + + ), +}); + +export const Groups = meta.story({ + render: () => ( + <> + + + Alpha + + + Item + + + + + Bravo + + + Item + + + + + Charlie + + + Item + + + + ), +}); diff --git a/code/core/src/components/components/Listbox/Listbox.tsx b/code/core/src/components/components/ActionList/ActionList.tsx similarity index 56% rename from code/core/src/components/components/Listbox/Listbox.tsx rename to code/core/src/components/components/ActionList/ActionList.tsx index 0259b924ccb5..73c75393b077 100644 --- a/code/core/src/components/components/Listbox/Listbox.tsx +++ b/code/core/src/components/components/ActionList/ActionList.tsx @@ -4,8 +4,9 @@ import type { TransitionStatus } from 'react-transition-state'; import { styled } from 'storybook/theming'; import { Button } from '../Button/Button'; +import { ToggleButton } from '../ToggleButton/ToggleButton'; -const ListboxItem = styled.div<{ +const ActionListItem = styled.li<{ active?: boolean; transitionStatus?: TransitionStatus; }>( @@ -22,6 +23,11 @@ const ListboxItem = styled.div<{ color: active ? theme.color.secondary : theme.color.defaultText, '--listbox-item-muted-color': active ? theme.color.secondary : theme.color.mediumdark, + '&:not(:hover, :has(:focus-visible)) svg + input': { + position: 'absolute', + opacity: 0, + }, + '@supports (interpolate-size: allow-keywords)': { interpolateSize: 'allow-keywords', transition: 'all var(--transition-duration, 0.2s)', @@ -53,11 +59,11 @@ const ListboxItem = styled.div<{ ); /** - * A Listbox item that shows/hides child elements on hover based on the targetId. Child elements + * A ActionList item that shows/hides child elements on hover based on the targetId. Child elements * must have a `data-target-id` attribute matching the `targetId` prop to be affected by the hover * behavior. */ -const ListboxHoverItem = styled(ListboxItem)<{ targetId: string }>(({ targetId }) => ({ +const ActionListHoverItem = styled(ActionListItem)<{ targetId: string }>(({ targetId }) => ({ gap: 0, [`& [data-target-id="${targetId}"]`]: { inlineSize: 'auto', @@ -65,7 +71,8 @@ const ListboxHoverItem = styled(ListboxItem)<{ targetId: string }>(({ targetId } opacity: 1, '@supports (interpolate-size: allow-keywords)': { interpolateSize: 'allow-keywords', - transition: 'all var(--transition-duration, 0.2s)', + transitionProperty: 'inline-size, margin-left, opacity, padding-inline', + transitionDuration: 'var(--transition-duration, 0.2s)', }, }, [`&:not(:hover, :has(:focus-visible)) [data-target-id="${targetId}"]`]: { @@ -76,13 +83,39 @@ const ListboxHoverItem = styled(ListboxItem)<{ targetId: string }>(({ targetId } }, })); -const ListboxButton = forwardRef>( - function ListboxButton({ padding = 'small', size = 'medium', variant = 'ghost', ...props }, ref) { - return diff --git a/code/core/src/components/components/Modal/Modal.tsx b/code/core/src/components/components/Modal/Modal.tsx index 8d7efd17ccdd..80b839bbfae2 100644 --- a/code/core/src/components/components/Modal/Modal.tsx +++ b/code/core/src/components/components/Modal/Modal.tsx @@ -84,7 +84,9 @@ function BaseModal({ variant = 'dialog', ...props }: ModalProps) { + let deprecated = undefined; if (ariaLabel === undefined || ariaLabel === '') { + deprecated = 'ariaLabel'; deprecate('The `ariaLabel` prop on `Modal` will become mandatory in Storybook 11.'); // TODO in Storybook 11 // throw new Error( @@ -93,12 +95,14 @@ function BaseModal({ } if (onEscapeKeyDown !== undefined) { + deprecated = 'onEscapeKeyDown'; deprecate( 'The `onEscapeKeyDown` prop is deprecated and will be removed in Storybook 11. Use `dismissOnEscape` instead.' ); } if (onInteractOutside !== undefined) { + deprecated = 'onInteractOutside'; deprecate( 'The `onInteractOutside` prop is deprecated and will be removed in Storybook 11. Use `dismissOnInteractOutside` instead.' ); @@ -203,6 +207,7 @@ function BaseModal({ {/* We need to set the FocusScope ourselves somehow, Overlay won't set it. */} { + const selectButton = await canvas.findByRole('button'); + + await step('Select number value (42)', async () => { + await userEvent.click(selectButton); + await userEvent.click(await screen.findByRole('option', { name: 'Number (42)' })); + expect(args.onSelect).toHaveBeenLastCalledWith(42); + expect(args.onChange).toHaveBeenLastCalledWith([42]); + }); + + await step('Select boolean value (true)', async () => { + await userEvent.click(selectButton); + await userEvent.click(await screen.findByRole('option', { name: 'Boolean (true)' })); + expect(args.onSelect).toHaveBeenLastCalledWith(true); + expect(args.onChange).toHaveBeenLastCalledWith([true]); + }); + + await step('Select boolean value (false)', async () => { + await userEvent.click(selectButton); + await userEvent.click(await screen.findByRole('option', { name: 'Boolean (false)' })); + expect(args.onSelect).toHaveBeenLastCalledWith(false); + expect(args.onChange).toHaveBeenLastCalledWith([false]); + }); + + await step('Select null value', async () => { + await userEvent.click(selectButton); + await userEvent.click(await screen.findByRole('option', { name: 'Null' })); + expect(args.onSelect).toHaveBeenLastCalledWith(null); + expect(args.onChange).toHaveBeenLastCalledWith([null]); + }); + + await step('Select undefined value', async () => { + await userEvent.click(selectButton); + await userEvent.click(await screen.findByRole('option', { name: 'Undefined' })); + expect(args.onSelect).toHaveBeenLastCalledWith(undefined); + expect(args.onChange).toHaveBeenLastCalledWith([undefined]); + }); + + await step('Select number value (0 - falsy)', async () => { + await userEvent.click(selectButton); + await userEvent.click(await screen.findByRole('option', { name: 'Number (0)' })); + expect(args.onSelect).toHaveBeenLastCalledWith(0); + expect(args.onChange).toHaveBeenLastCalledWith([0]); + }); + }, +}); + +export const NonStringValuesMultiSelect = meta.story({ + name: 'Non-String Values (multi)', + args: { + options: nonStringOptions, + multiSelect: true, + onSelect: fn(), + onDeselect: fn(), + onChange: fn(), + }, + play: async ({ canvas, args, step }) => { + const selectButton = await canvas.findByRole('button'); + + await step('Select number (42)', async () => { + await userEvent.click(selectButton); + await userEvent.click(await screen.findByRole('option', { name: 'Number (42)' })); + expect(args.onSelect).toHaveBeenLastCalledWith(42); + expect(args.onChange).toHaveBeenLastCalledWith([42]); + }); + + await step('Add boolean (true)', async () => { + await userEvent.click(await screen.findByRole('option', { name: 'Boolean (true)' })); + expect(args.onSelect).toHaveBeenLastCalledWith(true); + expect(args.onChange).toHaveBeenLastCalledWith([42, true]); + }); + + await step('Add null', async () => { + await userEvent.click(await screen.findByRole('option', { name: 'Null' })); + expect(args.onSelect).toHaveBeenLastCalledWith(null); + expect(args.onChange).toHaveBeenLastCalledWith([42, true, null]); + }); + + await step('Add undefined', async () => { + await userEvent.click(await screen.findByRole('option', { name: 'Undefined' })); + expect(args.onSelect).toHaveBeenLastCalledWith(undefined); + expect(args.onChange).toHaveBeenLastCalledWith([42, true, null, undefined]); + }); + + await step('Deselect number (42)', async () => { + await userEvent.click(await screen.findByRole('option', { name: 'Number (42)' })); + expect(args.onDeselect).toHaveBeenLastCalledWith(42); + expect(args.onChange).toHaveBeenLastCalledWith([true, null, undefined]); + }); + + await step('Deselect undefined', async () => { + await userEvent.click(await screen.findByRole('option', { name: 'Undefined' })); + expect(args.onDeselect).toHaveBeenLastCalledWith(undefined); + expect(args.onChange).toHaveBeenLastCalledWith([true, null]); + }); + }, +}); + +export const DefaultOptionNumber = meta.story({ + name: 'Default Option - Number', + args: { + options: nonStringOptions, + defaultOptions: 42, + }, + play: async ({ canvas }) => { + const selectButton = await canvas.findByRole('button'); + await expect(selectButton).toHaveTextContent('Number (42)'); + }, +}); + +export const DefaultOptionZero = meta.story({ + name: 'Default Option - Zero', + args: { + options: nonStringOptions, + defaultOptions: 0, + }, + play: async ({ canvas }) => { + const selectButton = await canvas.findByRole('button'); + await expect(selectButton).toHaveTextContent('Number (0)'); + }, +}); + +export const DefaultOptionBooleanTrue = meta.story({ + name: 'Default Option - Boolean True', + args: { + options: nonStringOptions, + defaultOptions: true, + }, + play: async ({ canvas }) => { + const selectButton = await canvas.findByRole('button'); + await expect(selectButton).toHaveTextContent('Boolean (true)'); + }, +}); + +export const DefaultOptionBooleanFalse = meta.story({ + name: 'Default Option - Boolean False', + args: { + options: nonStringOptions, + defaultOptions: false, + }, + play: async ({ canvas }) => { + const selectButton = await canvas.findByRole('button'); + await expect(selectButton).toHaveTextContent('Boolean (false)'); + }, +}); + +export const DefaultOptionNull = meta.story({ + name: 'Default Option - Null', + args: { + options: nonStringOptions, + defaultOptions: null, + }, + play: async ({ canvas }) => { + const selectButton = await canvas.findByRole('button'); + await expect(selectButton).toHaveTextContent('Null'); + }, +}); + +export const DefaultOptionUndefinedDoesNotWork = meta.story({ + name: 'Default Option - Bare undefined does NOT select', + args: { + options: nonStringOptions, + defaultOptions: undefined, + children: 'Nothing selected', + }, + play: async ({ canvas }) => { + const selectButton = await canvas.findByRole('button'); + await expect(selectButton).toHaveTextContent('Nothing selected'); + await expect(selectButton).not.toHaveTextContent('Undefined'); + }, +}); + +export const DefaultOptionUndefinedInArrayWorks = meta.story({ + name: 'Default Option - [undefined] selects undefined option', + args: { + options: nonStringOptions, + defaultOptions: [undefined], + }, + play: async ({ canvas }) => { + const selectButton = await canvas.findByRole('button'); + await expect(selectButton).toHaveTextContent('Undefined'); + }, +}); + +export const DefaultOptionsMultipleNonStringValues = meta.story({ + name: 'Default Options - Multiple Non-String Values', + args: { + options: nonStringOptions, + defaultOptions: [42, false, null], + multiSelect: true, + }, + play: async ({ canvas }) => { + const selectButton = await canvas.findByRole('button'); + await expect(selectButton).toHaveTextContent('3'); + + await userEvent.click(selectButton); + const option42 = await screen.findByRole('option', { name: 'Number (42)' }); + const optionFalse = await screen.findByRole('option', { name: 'Boolean (false)' }); + const optionNull = await screen.findByRole('option', { name: 'Null' }); + + expect(option42).toHaveAttribute('aria-selected', 'true'); + expect(optionFalse).toHaveAttribute('aria-selected', 'true'); + expect(optionNull).toHaveAttribute('aria-selected', 'true'); + }, +}); + +const optionsWithUndefinedForReset = [ + { title: 'Apple', value: 'apple' }, + { title: 'Undefined Value', value: undefined }, + { title: 'Banana', value: 'banana' }, +]; + +export const ResetWithUndefinedOption = meta.story({ + name: 'Reset vs Undefined Option', + args: { + options: optionsWithUndefinedForReset, + children: 'Select fruit', + onReset: fn(), + onChange: fn(), + onSelect: fn(), + }, + play: async ({ canvas, args, step }) => { + const selectButton = await canvas.findByRole('button'); + + await step('Select a regular option first', async () => { + await userEvent.click(selectButton); + await userEvent.click(await screen.findByRole('option', { name: 'Apple' })); + expect(args.onSelect).toHaveBeenLastCalledWith('apple'); + expect(args.onChange).toHaveBeenLastCalledWith(['apple']); + await expect(selectButton).toHaveTextContent('Apple'); + }); + + await step('Select the undefined value option - it should work', async () => { + await userEvent.click(selectButton); + await userEvent.click(await screen.findByRole('option', { name: 'Undefined Value' })); + expect(args.onSelect).toHaveBeenLastCalledWith(undefined); + expect(args.onChange).toHaveBeenLastCalledWith([undefined]); + await expect(selectButton).toHaveTextContent('Undefined Value'); + }); + + await step('Click Reset - should clear, not select undefined option', async () => { + await userEvent.click(selectButton); + await userEvent.click(await screen.findByRole('option', { name: 'Reset selection' })); + expect(args.onReset).toHaveBeenCalledTimes(1); + expect(args.onChange).toHaveBeenLastCalledWith([]); + await expect(selectButton).toHaveTextContent('Select fruit'); + await expect(selectButton).not.toHaveTextContent('Undefined Value'); + }); + + await step('Can still select undefined value after reset', async () => { + await userEvent.click(selectButton); + await userEvent.click(await screen.findByRole('option', { name: 'Undefined Value' })); + expect(args.onSelect).toHaveBeenLastCalledWith(undefined); + expect(args.onChange).toHaveBeenLastCalledWith([undefined]); + await expect(selectButton).toHaveTextContent('Undefined Value'); + }); + }, +}); diff --git a/code/core/src/components/components/Select/Select.tsx b/code/core/src/components/components/Select/Select.tsx index ba059983559b..961e2990e9e6 100644 --- a/code/core/src/components/components/Select/Select.tsx +++ b/code/core/src/components/components/Select/Select.tsx @@ -14,8 +14,16 @@ import { Button, type ButtonProps } from '../Button/Button'; import { Form } from '../Form/Form'; import { Popover } from '../Popover/Popover'; import { SelectOption } from './SelectOption'; -import type { Option, ResetOption } from './helpers'; -import { Listbox, PAGE_STEP_SIZE } from './helpers'; +import type { InternalOption, Option, ResetOption, Value } from './helpers'; +import { + Listbox, + PAGE_STEP_SIZE, + externalToValue, + isLiteralValue, + optionOrResetToInternal, + optionToInternal, + valueToExternal, +} from './helpers'; export interface SelectProps extends Omit { @@ -54,7 +62,7 @@ export interface SelectProps options: Option[]; /** IDs of the preselected options. */ - defaultOptions?: string | string[]; + defaultOptions?: Value | Value[]; /** Whether the Select should render open. */ defaultOpen?: boolean; @@ -65,13 +73,13 @@ export interface SelectProps /** Custom text label for the reset option when it exists. */ resetLabel?: string; - onSelect?: (option: string) => void; - onDeselect?: (option: string) => void; - onChange?: (selected: string[]) => void; + onSelect?: (option: Value) => void; + onDeselect?: (option: Value) => void; + onChange?: (selected: Value[]) => void; } -function valueToId(parentId: string, { value }: ResetOption | Option): string { - return `${parentId}-opt-${value ?? 'sb-reset'}`; +function valueToId(parentId: string, { value }: InternalOption | ResetOption): string { + return `${parentId}-opt-${String(value) ?? 'sb-reset'}`; } const SelectedOptionCount = styled.span(({ theme }) => ({ @@ -83,16 +91,18 @@ const SelectedOptionCount = styled.span(({ theme }) => ({ function setSelectedFromDefault( options: SelectProps['options'], defaultOptions: SelectProps['defaultOptions'] -): Option[] { - if (!defaultOptions) { +): InternalOption[] { + if (defaultOptions === undefined) { return []; } - if (typeof defaultOptions === 'string') { - return options.filter((opt) => opt.value === defaultOptions); + if (isLiteralValue(defaultOptions)) { + return options.filter((opt) => opt.value === defaultOptions).map(optionToInternal); } - return options.filter((opt) => defaultOptions.some((def) => opt.value === def)); + return options + .filter((opt) => defaultOptions.some((def) => opt.value === def)) + .map(optionToInternal); } const StyledButton = styled(Button)( @@ -214,15 +224,15 @@ export const Select = forwardRef( }, [triggerRef]); // The last selected option(s), which will be used by the app. - const [selectedOptions, setSelectedOptions] = useState( + const [selectedOptions, setSelectedOptions] = useState( setSelectedFromDefault(calleeOptions, defaultOptions) ); // Selects an option (updating the selection state based on multiSelect). const handleSelectOption = useCallback( - (option: Option | ResetOption) => { + (option: InternalOption | ResetOption) => { // Reset option case. We check value === undefined for cleaner type handling in the other branch. - if (option.value === undefined) { + if (option.type === 'reset') { if (selectedOptions.length) { onChange?.([]); onReset?.(); @@ -230,25 +240,25 @@ export const Select = forwardRef( } } else if (multiSelect) { setSelectedOptions((previous) => { - let newSelected: Option[] = []; + let newSelected: InternalOption[] = []; const isSelected = previous?.some((opt) => opt.value === option.value); if (isSelected) { - onDeselect?.(option.value); + onDeselect?.(valueToExternal(option.value)); newSelected = previous?.filter((opt) => opt.value !== option.value) ?? []; } else { - onSelect?.(option.value); + onSelect?.(valueToExternal(option.value)); newSelected = [...(previous ?? []), option]; } - onChange?.(newSelected.map((opt) => opt.value)); + onChange?.(newSelected.map((opt) => valueToExternal(opt.value))); return newSelected; }); } else { setSelectedOptions((current) => { if (current.every((opt) => opt.value !== option.value)) { - onSelect?.(option.value); - onChange?.([option.value]); + onSelect?.(valueToExternal(option.value)); + onChange?.([valueToExternal(option.value)]); return [option]; } return current; @@ -263,6 +273,7 @@ export const Select = forwardRef( () => onReset ? { + type: 'reset', value: undefined, title: resetLabel ?? 'Reset selection', icon: , @@ -290,7 +301,7 @@ export const Select = forwardRef( }, [defaultOptions, calleeOptions]); // The active option in the listbox, which will receive focus when the listbox is open. - const [activeOption, setActiveOptionState] = useState
); diff --git a/code/core/src/manager/components/sidebar/RefIndicator.tsx b/code/core/src/manager/components/sidebar/RefIndicator.tsx index 6292b475622d..4d76d4f409ba 100644 --- a/code/core/src/manager/components/sidebar/RefIndicator.tsx +++ b/code/core/src/manager/components/sidebar/RefIndicator.tsx @@ -33,7 +33,7 @@ export interface CurrentVersionProps { versions: RefType['versions']; } -const IndicatorPlacement = styled.aside(({ theme }) => ({ +const IndicatorPlacement = styled.div(({ theme }) => ({ height: 16, display: 'flex', @@ -131,7 +131,7 @@ const SubtleSelect = styled(Select)(({ theme }) => ({ })); export const RefIndicator = React.memo( - forwardRef }>( + forwardRef }>( ({ state, ...ref }, forwardedRef) => { const api = useStorybookApi(); const { isMobile } = useLayout(); @@ -199,7 +199,8 @@ export const RefIndicator = React.memo( tooltip="Choose version" defaultOptions={currentVersion} onSelect={(item) => { - const href = ref.versions?.[item]; + // We only pass strings as version ids, so item is always a string + const href = ref.versions?.[item as string]; if (href) { api.changeRefVersion(ref.id, href); } diff --git a/code/core/src/manager/components/sidebar/Refs.tsx b/code/core/src/manager/components/sidebar/Refs.tsx index a46a6816b15b..33c8c18c4c38 100644 --- a/code/core/src/manager/components/sidebar/Refs.tsx +++ b/code/core/src/manager/components/sidebar/Refs.tsx @@ -94,7 +94,7 @@ export const Ref: FC = React.memo(function Ref(props) { } = props; const length = useMemo(() => (index ? Object.keys(index).length : 0), [index]); - const indicatorRef = useRef(null); + const indicatorRef = useRef(null); const isMain = refId === DEFAULT_REF_ID; const isLoadingInjected = diff --git a/code/core/src/manager/components/sidebar/Search.stories.tsx b/code/core/src/manager/components/sidebar/Search.stories.tsx index 0c7b15f95acd..934a335d08a4 100644 --- a/code/core/src/manager/components/sidebar/Search.stories.tsx +++ b/code/core/src/manager/components/sidebar/Search.stories.tsx @@ -57,7 +57,7 @@ export const Simple: StoryFn = () => {() => null} {() => null}; export const FilledIn: StoryFn = () => ( - + {() => } ); diff --git a/code/core/src/manager/components/sidebar/Search.tsx b/code/core/src/manager/components/sidebar/Search.tsx index b5f955b60b2b..0bcd4635d164 100644 --- a/code/core/src/manager/components/sidebar/Search.tsx +++ b/code/core/src/manager/components/sidebar/Search.tsx @@ -418,6 +418,7 @@ export const Search = React.memo(function Search({ onClick={() => { reset({ inputValue: '' }); closeMenu(); + inputRef.current?.focus(); }} > diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index 6c85835d920c..d16b5b1a51b2 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -193,14 +193,7 @@ export const Sidebar = React.memo(function Sidebar({ ) } searchFieldContent={ - indexJson && ( - - ) + indexJson && } {...lastViewedProps} > diff --git a/code/core/src/manager/components/sidebar/StatusButton.tsx b/code/core/src/manager/components/sidebar/StatusButton.tsx index f3403cf9a739..987a783cd25b 100644 --- a/code/core/src/manager/components/sidebar/StatusButton.tsx +++ b/code/core/src/manager/components/sidebar/StatusButton.tsx @@ -68,6 +68,7 @@ const StyledButton = styled(Button)<{ '&:focus': { color: theme.color.secondary, borderColor: theme.color.secondary, + outlineOffset: -2, '&:not(:focus-visible)': { borderColor: 'transparent', diff --git a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx index a2fc136196b2..e73c3a6e8200 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx @@ -20,8 +20,15 @@ const meta = { }), applyQueryParams: fn().mockName('api::applyQueryParams'), } as any, - isDevelopment: true, tagPresets: {}, + indexJson: { + v: 6, + entries: { + 'c1-s1': { tags: ['A', 'B', 'C', 'dev', 'play-fn'], type: 'story' } as any, + 'c1-test': { tags: ['test-fn'], type: 'story', subtype: 'test' } as any, + 'c1-doc': { tags: [], type: 'docs' } as any, + }, + }, }, } satisfies Meta; @@ -29,16 +36,7 @@ export default meta; type Story = StoryObj; -export const Closed: Story = { - args: { - indexJson: { - v: 6, - entries: { - 'c1-s1': { tags: ['A', 'B', 'C', 'dev'] } as any, - }, - }, - }, -}; +export const Closed: Story = {}; export const ClosedWithSelection: Story = { args: { @@ -58,6 +56,21 @@ export const Clear = { }, } satisfies Story; +export const NoUserTags = { + ...Clear, + args: { + ...Clear.args, + indexJson: { + v: 6, + entries: { + 'c1-s1': { tags: ['dev', 'play-fn'], type: 'story' } as any, + 'c1-test': { tags: ['test-fn'], type: 'story', subtype: 'test' } as any, + 'c1-doc': { tags: [], type: 'docs' } as any, + }, + }, + }, +} satisfies Story; + export const WithSelection = { ...ClosedWithSelection, play: Clear.play, @@ -95,10 +108,10 @@ export const Empty: Story = { play: Clear.play, }; +/** Production is equal to development now */ export const EmptyProduction: Story = { args: { ...Empty.args, - isDevelopment: false, }, play: Clear.play, }; diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx index 6f7b49bfd0ba..5cb200306bd8 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -27,12 +27,11 @@ const BUILT_IN_TAGS = new Set([ 'test-fn', ]); -// Temporary to prevent regressions until TagFilterPanel can be refactored. -const StyledIconButton = styled(Button)<{ active: boolean }>(({ active, theme }) => ({ +const StyledButton = styled(Button)<{ isHighlighted: boolean }>(({ isHighlighted, theme }) => ({ '&:focus-visible': { outlineOffset: 4, }, - ...(active && { + ...(isHighlighted && { background: theme.background.hoverable, color: theme.color.secondary, }), @@ -52,10 +51,6 @@ const remove = (set: Set, id: string) => { const equal = (left: Set, right: Set) => left.size === right.size && new Set([...left, ...right]).size === left.size; -const Wrapper = styled.div({ - position: 'relative', -}); - const TagSelected = styled(Badge)(({ theme }) => ({ position: 'absolute', top: 7, @@ -77,11 +72,10 @@ const TagSelected = styled(Badge)(({ theme }) => ({ export interface TagsFilterProps { api: API; indexJson: StoryIndex; - isDevelopment: boolean; tagPresets: TagsOptions; } -export const TagsFilter = ({ api, indexJson, isDevelopment, tagPresets }: TagsFilterProps) => { +export const TagsFilter = ({ api, indexJson, tagPresets }: TagsFilterProps) => { const filtersById = useMemo<{ [id: string]: Filter }>(() => { const userTagsCounts = Object.values(indexJson.entries).reduce<{ [key: Tag]: number }>( (acc, entry) => { @@ -228,11 +222,6 @@ export const TagsFilter = ({ api, indexJson, isDevelopment, tagPresets }: TagsFi [expanded, setExpanded] ); - // Hide the entire UI if there are no tags and it's a built Storybook - if (Object.keys(filtersById).length === 0 && !isDevelopment) { - return null; - } - return ( )} > - {includedFilters.size + excludedFilters.size > 0 && } - + ); }; diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx index ee4847fe22f2..f87eb224b4e4 100644 --- a/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx @@ -72,7 +72,6 @@ const meta = { api: { getDocsUrl: () => 'https://storybook.js.org/docs/', } as any, - isDevelopment: true, }, tags: ['hoho'], } satisfies Meta; @@ -89,10 +88,14 @@ export const BuiltInOnly: Story = { }, }; +/** + * Production is equal to development now. We want to avoid a completely empty TagsFilterPanel and + * we can't easily detect if there'll be items matching the built-in filters. Plus, onboarding users + * on custom tags is still useful in production. + */ export const BuiltInOnlyProduction: Story = { args: { ...BuiltInOnly.args, - isDevelopment: false, }, }; diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx index f3197f4ed85b..f6f15599a6e8 100644 --- a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx @@ -1,13 +1,6 @@ -import React, { useRef } from 'react'; +import React, { Fragment, useRef } from 'react'; -import { - Button, - Form, - ListItem, - TooltipLinkList, - TooltipNote, - TooltipProvider, -} from 'storybook/internal/components'; +import { ActionList, Form } from 'storybook/internal/components'; import type { API_PreparedIndexEntry } from 'storybook/internal/types'; import { @@ -37,44 +30,10 @@ export const groupByType = (filters: Filter[]) => const Wrapper = styled.div({ minWidth: 240, maxWidth: 300, -}); - -const Actions = styled.div(({ theme }) => ({ - display: 'flex', - justifyContent: 'space-between', - gap: 4, - padding: 4, - borderBottom: `1px solid ${theme.appBorderColor}`, -})); - -const TagRow = styled.div({ - display: 'flex', - - '& button:last-of-type': { - width: 64, - maxWidth: 64, - marginLeft: 4, - paddingLeft: 0, - paddingRight: 0, - fontWeight: 'normal', - transition: 'max-width 150ms', - }, - '&:not(:hover):not(:focus-within)': { - '& button:last-of-type': { - marginLeft: 0, - maxWidth: 0, - opacity: 0, - }, - '& svg + input': { - display: 'none', - }, - }, -}); - -const Label = styled.div({ + maxHeight: 15.5 * 32 + 8, // 15.5 items at 32px each + 8px padding overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', + overflowY: 'auto', + scrollbarWidth: 'thin', }); const MutedText = styled.span(({ theme }) => ({ @@ -98,7 +57,6 @@ interface TagsFilterPanelProps { toggleFilter: (key: string, selected: boolean, excluded?: boolean) => void; setAllFilters: (selected: boolean) => void; resetFilters: () => void; - isDevelopment: boolean; isDefaultSelection: boolean; hasDefaultSelection: boolean; } @@ -111,7 +69,6 @@ export const TagsFilterPanel = ({ toggleFilter, setAllFilters, resetFilters, - isDevelopment, isDefaultSelection, hasDefaultSelection, }: TagsFilterPanelProps) => { @@ -135,7 +92,8 @@ export const TagsFilterPanel = ({ const isIncluded = includedFilters.has(id); const isExcluded = excludedFilters.has(id); const isChecked = isIncluded || isExcluded; - const toggleTagLabel = `${isChecked ? 'Remove' : 'Add'} ${type} filter: ${title}`; + const toggleLabel = `${type} filter: ${isExcluded ? `exclude ${title}` : title}`; + const toggleTooltip = `${isChecked ? 'Remove' : 'Add'} ${type} filter: ${title}`; const invertButtonLabel = `${isExcluded ? 'Include' : 'Exclude'} ${type}: ${title}`; // for built-in filters (docs, play, test), don't show if there are no matches @@ -146,116 +104,119 @@ export const TagsFilterPanel = ({ return { id: `filter-${type}-${id}`, content: ( - - }> - onToggle(!isChecked)} - icon={ - <> - {isExcluded ? : isIncluded ? null : icon} - onToggle(!isChecked)} - data-tag={title} - aria-hidden={true} - tabIndex={-1} - /> - - } - aria-label={toggleTagLabel} - title={ - - } - right={isExcluded ? {count} : {count}} - /> - - - + {isExcluded ? 'Include' : 'Exclude'} + + ), }; }; const groups = groupByType(Object.values(filtersById)); - const links: Link[][] = Object.values(groups).map( - (group) => + const links: Link[][] = Object.values(groups) + .map((group) => group .sort((a, b) => a.id.localeCompare(b.id)) .map((filter) => renderLink(filter)) - .filter(Boolean) as Link[] - ); - - if (!groups.tag?.length && isDevelopment) { - links.push([ - { - id: 'tags-docs', - title: 'Learn how to add tags', - icon: , - right: , - href: api.getDocsUrl({ subpath: 'writing-stories/tags#custom-tags' }), - }, - ]); - } + .filter((value): value is Link => !!value) + ) + .filter((value): value is Link[] => value.length > 0); + const hasItems = links.length > 0; + const hasUserTags = Object.values(filtersById).some(({ type }) => type === 'tag'); const isNothingSelectedYet = includedFilters.size === 0 && excludedFilters.size === 0; - const filtersLabel = isNothingSelectedYet ? 'Select all' : 'Clear filters'; return ( - {Object.keys(filtersById).length > 0 && ( - - {isNothingSelectedYet ? ( - - ) : ( - - )} - {hasDefaultSelection && ( - - )} - + + + + + Learn how to add tags + + + + + + + )} - ); }; diff --git a/code/core/src/manager/components/sidebar/TestingWidget.tsx b/code/core/src/manager/components/sidebar/TestingWidget.tsx index 0978b371c69c..8f31c62116e3 100644 --- a/code/core/src/manager/components/sidebar/TestingWidget.tsx +++ b/code/core/src/manager/components/sidebar/TestingWidget.tsx @@ -1,8 +1,15 @@ -import type { ComponentProps } from 'react'; -import React, { type SyntheticEvent, useCallback, useEffect, useRef, useState } from 'react'; +import React, { + type ComponentProps, + type ReactNode, + type SyntheticEvent, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { once } from 'storybook/internal/client-logger'; -import { Button, Card, ToggleButton } from 'storybook/internal/components'; +import { ActionList, Card } from 'storybook/internal/components'; import type { Addon_Collection, Addon_TestProviderType, @@ -62,7 +69,7 @@ const Filters = styled.div({ gap: 4, }); -const CollapseToggle = styled(Button)({ +const CollapseToggle = styled(ActionList.Button)({ opacity: 0, transition: 'opacity 250ms', '&:focus, &:hover': { @@ -70,15 +77,36 @@ const CollapseToggle = styled(Button)({ }, }); -const RunButton = ({ children, ...props }: ComponentProps) => ( - + ); -const StatusButton = styled(ToggleButton)<{ pressed: boolean; status: 'negative' | 'warning' }>( - { minWidth: 28 }, +const StatusButton = styled(ActionList.Toggle)<{ + pressed: boolean; + status: 'negative' | 'warning'; +}>( + { minWidth: 28, outlineOffset: -2 }, ({ pressed, status, theme }) => !pressed && (theme.base === 'light' @@ -239,38 +267,17 @@ export const TestingWidget = ({ {hasTestProviders && ( { - e.stopPropagation(); - onRunAll(); - }} - > - {isRunning ? 'Running...' : 'Run tests'} + + {isRunning ? 'Running...' : 'Run tests'} } - fallback={ - { - e.stopPropagation(); - onRunAll(); - }} - /> - } + fallback={} /> )} {hasTestProviders && ( toggleCollapsed(e)} id="testing-module-collapse-toggle" ariaLabel={isCollapsed ? 'Expand testing module' : 'Collapse testing module'} @@ -329,11 +336,8 @@ export const TestingWidget = ({ )} {hasStatuses && ( - + )} diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index e240259acbff..108d4f2a4393 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -46,7 +46,7 @@ import { useLayout } from '../layout/LayoutProvider'; import { useContextMenu } from './ContextMenu'; import { IconSymbols, UseSymbol } from './IconSymbols'; import { StatusButton } from './StatusButton'; -import { StatusContext, useStatusSummary } from './StatusContext'; +import { StatusContext } from './StatusContext'; import { ComponentNode, DocumentNode, GroupNode, RootNode, StoryNode, TestNode } from './TreeNode'; import { CollapseIcon } from './components/CollapseIcon'; import type { Highlight, Item } from './types'; diff --git a/code/core/src/manager/components/useLocationHash.ts b/code/core/src/manager/components/useLocationHash.ts deleted file mode 100644 index 17b6b3f0eb73..000000000000 --- a/code/core/src/manager/components/useLocationHash.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, useState } from 'react'; - -const hashMonitor = { - currentHash: globalThis.window?.location.hash ?? '', - intervalId: null as ReturnType | null, - listeners: new Set<(hash: string) => void>(), - - start() { - if (this.intervalId === null) { - this.intervalId = setInterval(() => { - const newHash = globalThis.window.location.hash ?? ''; - if (newHash !== this.currentHash) { - this.currentHash = newHash; - this.listeners.forEach((listener) => listener(newHash)); - } - }, 100); - } - }, - stop() { - if (this.intervalId !== null) { - clearInterval(this.intervalId); - this.intervalId = null; - } - }, - subscribe(...listeners: Array<(hash: string) => void>) { - listeners.forEach((listener) => this.listeners.add(listener)); - this.start(); - return () => { - listeners.forEach((listener) => this.listeners.delete(listener)); - if (this.listeners.size === 0) { - this.stop(); - } - }; - }, -}; - -export const useLocationHash = () => { - const [hash, setHash] = useState(globalThis.window?.location.hash ?? ''); - useEffect(() => hashMonitor.subscribe(setHash), []); - return hash.slice(1); -}; diff --git a/code/core/src/manager/container/Menu.tsx b/code/core/src/manager/container/Menu.tsx index e01a5abb0060..08bc657f2711 100644 --- a/code/core/src/manager/container/Menu.tsx +++ b/code/core/src/manager/container/Menu.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react'; import React, { useCallback, useMemo } from 'react'; -import { Listbox, ProgressSpinner } from 'storybook/internal/components'; +import { ActionList, ProgressSpinner } from 'storybook/internal/components'; import { STORIES_COLLAPSE_ALL } from 'storybook/internal/core-events'; import { global } from '@storybook/global'; @@ -96,10 +96,10 @@ export const useMenu = ({ closeOnClick: true, icon: , right: progress < 100 && ( - + {progress}% - + ), }), [api, progress] @@ -218,9 +218,9 @@ export const useMenu = ({ closeOnClick: true, href: docsUrl, right: ( - + - + ), icon: , }; diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts index ec915c414def..4f855b346150 100644 --- a/code/core/src/manager/globals/exports.ts +++ b/code/core/src/manager/globals/exports.ts @@ -487,6 +487,7 @@ export default { 'A', 'AbstractToolbar', 'ActionBar', + 'ActionList', 'AddonPanel', 'Badge', 'Bar', @@ -515,7 +516,6 @@ export default { 'LI', 'Link', 'ListItem', - 'Listbox', 'Loader', 'Modal', 'ModalDecorator', diff --git a/code/core/src/manager/hooks/useLocation.ts b/code/core/src/manager/hooks/useLocation.ts new file mode 100644 index 000000000000..fe9b83dddd45 --- /dev/null +++ b/code/core/src/manager/hooks/useLocation.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react'; + +export const LocationMonitor = { + _currentHref: globalThis.window?.location.href ?? '', + _intervalId: null as ReturnType | null, + _listeners: new Set<(location: Location) => void>(), + + start() { + if (this._intervalId === null) { + this._intervalId = setInterval(() => { + const newLocation = globalThis.window.location; + if (newLocation.href !== this._currentHref) { + this._currentHref = newLocation.href; + this._listeners.forEach((listener) => listener(newLocation)); + } + }, 100); + } + }, + + stop() { + if (this._intervalId !== null) { + clearInterval(this._intervalId); + this._intervalId = null; + } + }, + + subscribe(...listeners: Array<(location: Location) => void>) { + listeners.forEach((listener) => this._listeners.add(listener)); + this.start(); + return () => { + listeners.forEach((listener) => this._listeners.delete(listener)); + if (this._listeners.size === 0) { + this.stop(); + } + }; + }, +}; + +export const useLocationHash = () => { + const [hash, setHash] = useState(globalThis.window?.location.hash ?? ''); + useEffect(() => LocationMonitor.subscribe((location) => setHash(location.hash)), []); + return hash.slice(1); +}; diff --git a/code/core/src/manager/settings/Checklist/Checklist.tsx b/code/core/src/manager/settings/Checklist/Checklist.tsx index 32225c67747f..9b57632c5703 100644 --- a/code/core/src/manager/settings/Checklist/Checklist.tsx +++ b/code/core/src/manager/settings/Checklist/Checklist.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; -import { Button, Collapsible, Listbox } from 'storybook/internal/components'; +import { ActionList, Button, Collapsible } from 'storybook/internal/components'; import { CheckIcon, @@ -15,7 +15,7 @@ import { styled } from 'storybook/theming'; import { Focus } from '../../components/Focus/Focus'; import type { ChecklistItem, useChecklist } from '../../components/sidebar/useChecklist'; -import { useLocationHash } from '../../components/useLocationHash'; +import { useLocationHash } from '../../hooks/useLocation'; type ChecklistSection = { id: string; @@ -318,7 +318,7 @@ export const Checklist = ({ const itemContent = content?.({ api }); return ( - + - + ); } )} diff --git a/code/core/src/shared/checklist-store/checklistData.tsx b/code/core/src/shared/checklist-store/checklistData.tsx index bc0103ae2321..7d86b71d220a 100644 --- a/code/core/src/shared/checklist-store/checklistData.tsx +++ b/code/core/src/shared/checklist-store/checklistData.tsx @@ -30,6 +30,7 @@ import { import { SUPPORTED_FRAMEWORKS } from '../../cli/AddonVitestService.constants'; import { ADDON_ID as ADDON_DOCS_ID } from '../../docs-tools/shared'; import { TourGuide } from '../../manager/components/TourGuide/TourGuide'; +import { LocationMonitor } from '../../manager/hooks/useLocation'; import type { initialState } from './checklistData.state'; const CodeWrapper = styled.div(({ theme }) => ({ @@ -365,11 +366,10 @@ export const Primary: Story = { criteria: "What's New page is opened", action: { label: 'Go', - onClick: ({ api, accept }) => { - api.navigate('/settings/whats-new'); - accept(); - }, + onClick: ({ api }) => api.navigate('/settings/whats-new'), }, + subscribe: ({ accept }) => + LocationMonitor.subscribe((l) => l.search.endsWith('/settings/whats-new') && accept()), }, ], }, diff --git a/code/core/src/toolbar/components/ToolbarMenuSelect.tsx b/code/core/src/toolbar/components/ToolbarMenuSelect.tsx index 43052b602dcc..886da8cb4acc 100644 --- a/code/core/src/toolbar/components/ToolbarMenuSelect.tsx +++ b/code/core/src/toolbar/components/ToolbarMenuSelect.tsx @@ -57,10 +57,9 @@ export const ToolbarMenuSelect: FC = withKeyboardCycle( const resetItem = items.find((item) => item.type === 'reset'); const resetLabel = resetItem?.title; const options = items - .filter((item) => item.type === 'item') - .filter((item): item is ToolbarItem & { value: string } => item.value !== undefined) + .filter((item): item is ToolbarItem => item.type === 'item') .map((item) => { - const itemTitle = item.title ?? item.value; + const itemTitle = item.title ?? item.value ?? 'Untitled'; const iconComponent = !item.hideIcon && item.icon ? ( @@ -93,7 +92,7 @@ export const ToolbarMenuSelect: FC = withKeyboardCycle( return (