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 = ({
}))
}
/>
- )
- }
- />
-
+ )}
+ = ({
{a11yStatusValueToStoryIds['status-value:error'].length +
a11yStatusValueToStoryIds['status-value:warning'].length || null}
-
-
+
+
)}
-
+
);
};
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 ;
+const StyledButton = styled(Button)({
+ '&:focus-visible': {
+ // Prevent focus outline from being cut off by overflow: hidden
+ outlineOffset: -2,
+ },
+});
+
+const StyledToggleButton = styled(ToggleButton)({
+ '&:focus-visible': {
+ // Prevent focus outline from being cut off by overflow: hidden
+ outlineOffset: -2,
+ },
+});
+
+const ActionListButton = forwardRef>(
+ function ActionListButton(
+ { padding = 'small', size = 'medium', variant = 'ghost', ...props },
+ ref
+ ) {
+ return ;
}
);
-const ListboxAction = styled(ListboxButton)({
+const ActionListToggle = forwardRef>(
+ function ActionListToggle(
+ { padding = 'small', size = 'medium', variant = 'ghost', ...props },
+ ref
+ ) {
+ return ;
+ }
+);
+
+const ActionListAction = styled(ActionListButton)(({ theme }) => ({
flex: '0 1 100%',
textAlign: 'start',
justifyContent: 'space-between',
@@ -91,9 +124,20 @@ const ListboxAction = styled(ListboxButton)({
'&:hover': {
color: 'inherit',
},
-});
+ '& input:enabled:focus-visible': {
+ outline: 'none',
+ },
+ '&:has(input:focus-visible)': {
+ outline: `2px solid ${theme.color.secondary}`,
+ outlineOffset: -2,
+ },
+}));
+
+const ActionListLink = (
+ props: ComponentProps & React.AnchorHTMLAttributes
+) => ;
-const ListboxText = styled.div({
+const ActionListText = styled.div({
display: 'flex',
alignItems: 'center',
gap: 8,
@@ -121,7 +165,7 @@ const ListboxText = styled.div({
},
});
-const ListboxIcon = styled.div({
+const ActionListIcon = styled.div({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@@ -131,8 +175,8 @@ const ListboxIcon = styled.div({
color: 'var(--listbox-item-muted-color)',
});
-export const Listbox = Object.assign(
- styled.div(({ theme, onClick }) => ({
+export const ActionList = Object.assign(
+ styled.ul(({ theme, onClick }) => ({
listStyle: 'none',
margin: 0,
padding: 4,
@@ -143,11 +187,13 @@ export const Listbox = Object.assign(
},
})),
{
- Item: ListboxItem,
- HoverItem: ListboxHoverItem,
- Button: ListboxButton,
- Action: ListboxAction,
- Text: ListboxText,
- Icon: ListboxIcon,
+ Item: ActionListItem,
+ HoverItem: ActionListHoverItem,
+ Button: ActionListButton,
+ Toggle: ActionListToggle,
+ Action: ActionListAction,
+ Link: ActionListLink,
+ Text: ActionListText,
+ Icon: ActionListIcon,
}
);
diff --git a/code/core/src/components/components/Bar/Bar.tsx b/code/core/src/components/components/Bar/Bar.tsx
index 0994ff81e1e3..88dd97510557 100644
--- a/code/core/src/components/components/Bar/Bar.tsx
+++ b/code/core/src/components/components/Bar/Bar.tsx
@@ -131,6 +131,7 @@ export const FlexBar = ({ children, backgroundColor, className = '', ...rest }:
const [left, right] = Children.toArray(children);
return (
(
) => {
const Comp = asChild ? Slot : as;
- if (ariaLabel === undefined || ariaLabel === '') {
+ let deprecated = undefined;
+ if (!readOnly && (ariaLabel === undefined || ariaLabel === '')) {
+ deprecated = 'ariaLabel';
deprecate(
`The 'ariaLabel' prop on 'Button' will become mandatory in Storybook 11. Buttons with text content should set 'ariaLabel={false}' to indicate that they are accessible as-is. Buttons without text content must provide a meaningful 'ariaLabel' for accessibility. The button content is: ${props.children}.`
);
@@ -86,6 +88,7 @@ export const Button = forwardRef(
}
if (active !== undefined) {
+ deprecated = 'active';
deprecate(
'The `active` prop on `Button` is deprecated and will be removed in Storybook 11. Use specialized components like `ToggleButton` or `Select` instead.'
);
@@ -129,6 +132,7 @@ export const Button = forwardRef(
tooltip={finalTooltip}
>
((props, ref
'`IconButton` is deprecated and will be removed in Storybook 11, use `Button` instead.'
);
- return ;
+ return ;
});
IconButton.displayName = 'IconButton';
diff --git a/code/core/src/components/components/Listbox/Listbox.stories.tsx b/code/core/src/components/components/Listbox/Listbox.stories.tsx
deleted file mode 100644
index 2b4b27aea62f..000000000000
--- a/code/core/src/components/components/Listbox/Listbox.stories.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { CheckIcon, EllipsisIcon, PlayAllHollowIcon } from '@storybook/icons';
-
-import { Badge, Form, ProgressSpinner } from '../..';
-import preview from '../../../../../.storybook/preview';
-import { Shortcut } from '../../../manager/container/Menu';
-import { Listbox } from './Listbox';
-
-const meta = preview.meta({
- component: Listbox,
- 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/Modal/Modal.styled.tsx b/code/core/src/components/components/Modal/Modal.styled.tsx
index 3c93a26b3fb4..eb3ca76630e6 100644
--- a/code/core/src/components/components/Modal/Modal.styled.tsx
+++ b/code/core/src/components/components/Modal/Modal.styled.tsx
@@ -201,7 +201,7 @@ export const Close = ({ asChild, children, onClick, ...props }: CloseProps) => {
export const Dialog = {
Close: () => {
deprecate('Modal.Dialog.Close is deprecated, please use Modal.Close instead.');
- return ;
+ return ;
},
};
@@ -210,7 +210,7 @@ export const CloseButton = ({ ariaLabel, ...props }: React.ComponentProps
-
);
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}}
- />
-
- onToggle(true, !isExcluded)}
+
+
+
+ {isExcluded ? : isIncluded ? null : icon}
+ onToggle(!isChecked)}
+ data-tag={title}
+ aria-label={toggleLabel}
+ />
+
+
+
+ {title}
+ {isExcluded && (excluded)}
+
+
+ {isExcluded ? {count} : {count}}
+
+ onToggle(true, !isExcluded)}
>
- {isExcluded ? 'Include' : 'Exclude'}
-
-
+ {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 ? (
- setAllFilters(true)}
- >
-
- {filtersLabel}
-
- ) : (
- setAllFilters(false)}
- >
-
- {filtersLabel}
-
- )}
- {hasDefaultSelection && (
-
+
+ {isNothingSelectedYet ? (
+ setAllFilters(true)}
+ >
+
+ Select all
+
+ ) : (
+ setAllFilters(false)}
+ >
+
+ Clear filters
+
+ )}
+ {hasDefaultSelection && (
+
+
+
+ )}
+
+
+ )}
+ {links.map((group) => (
+ link.id).join('_')}>
+ {group.map((link) => (
+ {link.content}
+ ))}
+
+ ))}
+ {!hasUserTags && (
+
+
+
-
-
- )}
-
+
+
+
+
+ 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 RunButton = ({
+ children,
+ isRunning,
+ onRunAll,
+ ...props
+}: { children?: ReactNode; isRunning: boolean; onRunAll: () => void } & ComponentProps<
+ typeof ActionList.Button
+>) => (
+ {
+ e.stopPropagation();
+ onRunAll();
+ }}
+ {...props}
+ >
+
+
+
{children}
-
+
);
-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 && (
- {
e.stopPropagation();
clearStatuses();
@@ -344,7 +348,7 @@ export const TestingWidget = ({
}
>
-
+
)}
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 (