diff --git a/CHANGELOG.md b/CHANGELOG.md index 9826390c44b2..e731ddb2702c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 8.0.2 + +- Addon Docs: Fix [Object object] displayName in some JSX components - [#26566](https://github.com/storybookjs/storybook/pull/26566), thanks @yannbf! +- CLI: Add yarn1 package manager fallback for init in empty directory - [#26500](https://github.com/storybookjs/storybook/pull/26500), thanks @valentinpalkovic! +- CSF: Make sure loaders/decorators can be used as array - [#26514](https://github.com/storybookjs/storybook/pull/26514), thanks @kasperpeulen! +- Controls: Fix disable condition in ArgControl component - [#26567](https://github.com/storybookjs/storybook/pull/26567), thanks @valentinpalkovic! +- UI: Add key property to list children in Highlight component - [#26471](https://github.com/storybookjs/storybook/pull/26471), thanks @valentinpalkovic! +- UI: Fix theming of elements inside bars - [#26527](https://github.com/storybookjs/storybook/pull/26527), thanks @valentinpalkovic! +- UI: Improve empty state of addon panel - [#26481](https://github.com/storybookjs/storybook/pull/26481), thanks @yannbf! + ## 8.0.1 - Controls: Fix type summary when table.type unset - [#26283](https://github.com/storybookjs/storybook/pull/26283), thanks @shilman! diff --git a/code/addons/a11y/src/components/Report/index.tsx b/code/addons/a11y/src/components/Report/index.tsx index d231cc4cf0e2..83bcb1705d32 100644 --- a/code/addons/a11y/src/components/Report/index.tsx +++ b/code/addons/a11y/src/components/Report/index.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react'; import React, { Fragment } from 'react'; -import { Placeholder } from '@storybook/components'; +import { EmptyTabContent } from '@storybook/components'; import type { Result } from 'axe-core'; import { Item } from './Item'; @@ -18,7 +18,7 @@ export const Report: FC = ({ items, empty, type }) => ( {items && items.length ? ( items.map((item) => ) ) : ( - {empty} + )} ); diff --git a/code/addons/interactions/src/components/EmptyState.tsx b/code/addons/interactions/src/components/EmptyState.tsx index d4fa62c144a4..0cb5ecba69e2 100644 --- a/code/addons/interactions/src/components/EmptyState.tsx +++ b/code/addons/interactions/src/components/EmptyState.tsx @@ -1,43 +1,11 @@ import React, { useEffect, useState } from 'react'; -import { Link } from '@storybook/components'; +import { Link, EmptyTabContent } from '@storybook/components'; import { DocumentIcon, VideoIcon } from '@storybook/icons'; -import { Consumer, useStorybookApi } from '@storybook/manager-api'; +import { useStorybookApi } from '@storybook/manager-api'; import { styled } from '@storybook/theming'; import { DOCUMENTATION_LINK, TUTORIAL_VIDEO_LINK } from '../constants'; -const Wrapper = styled.div(({ theme }) => ({ - height: '100%', - display: 'flex', - padding: 0, - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'column', - gap: 15, - background: theme.background.content, -})); - -const Content = styled.div({ - display: 'flex', - flexDirection: 'column', - gap: 4, - maxWidth: 415, -}); - -const Title = styled.div(({ theme }) => ({ - fontWeight: theme.typography.weight.bold, - fontSize: theme.typography.size.s2 - 1, - textAlign: 'center', - color: theme.textColor, -})); - -const Description = styled.div(({ theme }) => ({ - fontWeight: theme.typography.weight.regular, - fontSize: theme.typography.size.s2 - 1, - textAlign: 'center', - color: theme.textMutedColor, -})); - const Links = styled.div(({ theme }) => ({ display: 'flex', fontSize: theme.typography.size.s2 - 1, @@ -73,27 +41,25 @@ export const Empty = () => { if (isLoading) return null; return ( - - - Interaction testing - + Interaction tests allow you to verify the functional aspects of UIs. Write a play function for your story and you'll see it run here. - - - - - Watch 8m video - - - - {({ state }) => ( - - Read docs - - )} - - - + + } + footer={ + + + Watch 8m video + + + + Read docs + + + } + /> ); }; diff --git a/code/lib/cli/src/initiate.ts b/code/lib/cli/src/initiate.ts index b5504f2f751e..4231df2df247 100644 --- a/code/lib/cli/src/initiate.ts +++ b/code/lib/cli/src/initiate.ts @@ -242,7 +242,7 @@ export async function doInitiate( > { const { packageManager: pkgMgr } = options; - const packageManager = JsPackageManagerFactory.getPackageManager({ + let packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr, }); @@ -276,6 +276,13 @@ export async function doInitiate( // Check if the current directory is empty. if (options.force !== true && currentDirectoryIsEmpty(packageManager.type)) { + // Initializing Storybook in an empty directory with yarn1 + // will very likely fail due to different kind of hoisting issues + // which doesn't get fixed anymore in yarn1. + // We will fallback to npm in this case. + if (packageManager.type === 'yarn1') { + packageManager = JsPackageManagerFactory.getPackageManager({ force: 'npm' }); + } // Prompt the user to create a new project from our list. await scaffoldNewProject(packageManager.type, options); diff --git a/code/lib/preview-api/src/modules/store/csf/composeConfigs.test.ts b/code/lib/preview-api/src/modules/store/csf/composeConfigs.test.ts index 238885f44ba3..147038a5a8d2 100644 --- a/code/lib/preview-api/src/modules/store/csf/composeConfigs.test.ts +++ b/code/lib/preview-api/src/modules/store/csf/composeConfigs.test.ts @@ -176,6 +176,34 @@ describe('composeConfigs', () => { }); }); + it('allows single array to be written without array', () => { + expect( + composeConfigs([ + { + argsEnhancers: ['1', '2'], + argTypesEnhancers: ['1', '2'], + loaders: '1', + }, + { + argsEnhancers: '3', + argTypesEnhancers: '3', + loaders: ['2', '3'], + }, + ]) + ).toEqual({ + parameters: {}, + decorators: [], + args: {}, + argsEnhancers: ['1', '2', '3'], + argTypes: {}, + argTypesEnhancers: ['1', '2', '3'], + globals: {}, + globalTypes: {}, + loaders: ['1', '2', '3'], + runStep: expect.any(Function), + }); + }); + it('combines decorators in reverse file order', () => { expect( composeConfigs([ diff --git a/code/lib/preview-api/src/modules/store/csf/composeConfigs.ts b/code/lib/preview-api/src/modules/store/csf/composeConfigs.ts index 862f8cbcd501..e5785a6a3f01 100644 --- a/code/lib/preview-api/src/modules/store/csf/composeConfigs.ts +++ b/code/lib/preview-api/src/modules/store/csf/composeConfigs.ts @@ -3,6 +3,7 @@ import { global } from '@storybook/global'; import { combineParameters } from '../parameters'; import { composeStepRunners } from './stepRunners'; +import { normalizeArrays } from './normalizeArrays'; export function getField( moduleExportList: ModuleExports[], @@ -16,10 +17,10 @@ export function getArrayField( field: string, options: { reverseFileOrder?: boolean } = {} ): TFieldType[] { - return getField(moduleExportList, field).reduce( - (a: any, b: any) => (options.reverseFileOrder ? [...b, ...a] : [...a, ...b]), - [] - ); + return getField(moduleExportList, field).reduce((prev: any, cur: any) => { + const normalized = normalizeArrays(cur); + return options.reverseFileOrder ? [...normalized, ...prev] : [...prev, ...normalized]; + }, []); } export function getObjectField>( diff --git a/code/lib/theming/src/themes/dark.ts b/code/lib/theming/src/themes/dark.ts index 4cb8f19f29bd..173e735ef366 100644 --- a/code/lib/theming/src/themes/dark.ts +++ b/code/lib/theming/src/themes/dark.ts @@ -25,7 +25,7 @@ const theme: ThemeVars = { textMutedColor: '#798186', // Toolbar default and active colors - barTextColor: '#798186', + barTextColor: color.mediumdark, barHoverColor: color.secondary, barSelectedColor: color.secondary, barBg: '#292C2E', diff --git a/code/package.json b/code/package.json index 26b599d2100d..b51e2439afc9 100644 --- a/code/package.json +++ b/code/package.json @@ -295,5 +295,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.0.2" } diff --git a/code/renderers/react/src/docs/jsxDecorator.test.tsx b/code/renderers/react/src/docs/jsxDecorator.test.tsx index 6ed0f0eda179..b8868265333e 100644 --- a/code/renderers/react/src/docs/jsxDecorator.test.tsx +++ b/code/renderers/react/src/docs/jsxDecorator.test.tsx @@ -130,11 +130,22 @@ describe('renderJsx', () => { } ); - expect(renderJsx(createElement(MyExoticComponentRef, {}, 'I am forwardRef!'), {})) + expect(renderJsx(I am forwardRef!)) .toMatchInlineSnapshot(` - + I am forwardRef! - + + `); + + // if docgenInfo is present, it should use the displayName from there + (MyExoticComponentRef as any).__docgenInfo = { + displayName: 'ExoticComponent', + }; + expect(renderJsx(I am forwardRef!)) + .toMatchInlineSnapshot(` + + I am forwardRef! + `); }); @@ -143,11 +154,20 @@ describe('renderJsx', () => { return
{props.children}
; }); - expect(renderJsx(createElement(MyMemoComponentRef, {}, 'I am memo!'), {})) - .toMatchInlineSnapshot(` - + expect(renderJsx(I am memo!)).toMatchInlineSnapshot(` + + I am memo! + + `); + + // if docgenInfo is present, it should use the displayName from there + (MyMemoComponentRef as any).__docgenInfo = { + displayName: 'MyMemoComponentRef', + }; + expect(renderJsx(I am memo!)).toMatchInlineSnapshot(` + I am memo! - + `); }); diff --git a/code/renderers/react/src/docs/jsxDecorator.tsx b/code/renderers/react/src/docs/jsxDecorator.tsx index 5b1391246c9d..e6b1934c2e6e 100644 --- a/code/renderers/react/src/docs/jsxDecorator.tsx +++ b/code/renderers/react/src/docs/jsxDecorator.tsx @@ -29,7 +29,8 @@ const toPascalCase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1) * @returns {string | null} A displayName for the Symbol in case elementType is a Symbol; otherwise, null. */ export const getReactSymbolName = (elementType: any): string => { - const symbolDescription: string = elementType.toString().replace(/^Symbol\((.*)\)$/, '$1'); + const elementName = elementType.$$typeof || elementType; + const symbolDescription: string = elementName.toString().replace(/^Symbol\((.*)\)$/, '$1'); const reactComponentName = symbolDescription .split('.') @@ -124,16 +125,28 @@ export const renderJsx = (code: React.ReactElement, options?: JSXOptions) => { } else { displayNameDefaults = { // To get exotic component names resolving properly - displayName: (el: any): string => - el.type.displayName || typeof el.type === 'symbol' - ? getReactSymbolName(el.type) - : null || - getDocgenSection(el.type, 'displayName') || - (el.type.name !== '_default' ? el.type.name : null) || - (typeof el.type === 'function' ? 'No Display Name' : null) || - (isForwardRef(el.type) ? el.type.render.name : null) || - (isMemo(el.type) ? el.type.type.name : null) || - el.type, + displayName: (el: any): string => { + if (el.type.displayName) { + return el.type.displayName; + } else if (getDocgenSection(el.type, 'displayName')) { + return getDocgenSection(el.type, 'displayName'); + } else if ( + typeof el.type === 'symbol' || + (el.type.$$typeof && typeof el.type.$$typeof === 'symbol') + ) { + return getReactSymbolName(el.type); + } else if (el.type.name && el.type.name !== '_default') { + return el.type.name; + } else if (typeof el.type === 'function') { + return 'No Display Name'; + } else if (isForwardRef(el.type)) { + return el.type.render.name; + } else if (isMemo(el.type)) { + return el.type.type.name; + } else { + return el.type; + } + }, }; } diff --git a/code/ui/.storybook/main.ts b/code/ui/.storybook/main.ts index 3defc0046331..d1af3ccb4f68 100644 --- a/code/ui/.storybook/main.ts +++ b/code/ui/.storybook/main.ts @@ -53,7 +53,8 @@ const config: StorybookConfig = { '@storybook/addon-interactions', '@storybook/addon-storysource', '@storybook/addon-designs', - '@chromaui/addon-visual-tests', + '@storybook/addon-a11y', + '@chromatic-com/storybook', ], build: { test: { diff --git a/code/ui/blocks/src/components/ArgsTable/ArgControl.tsx b/code/ui/blocks/src/components/ArgsTable/ArgControl.tsx index 6d51fe88d318..ed8ee973d11e 100644 --- a/code/ui/blocks/src/components/ArgsTable/ArgControl.tsx +++ b/code/ui/blocks/src/components/ArgsTable/ArgControl.tsx @@ -65,8 +65,8 @@ export const ArgControl: FC = ({ row, arg, updateArgs, isHovere const onBlur = useCallback(() => setFocused(false), []); const onFocus = useCallback(() => setFocused(true), []); - if (!control || control.disabled) { - const canBeSetup = control?.disabled !== true && row?.type?.name !== 'function'; + if (!control || control.disable) { + const canBeSetup = control?.disable !== true && row?.type?.name !== 'function'; return isHovered && canBeSetup ? ( ({ - color: theme.barTextColor, margin: '-4px -12px -4px 0', })); diff --git a/code/ui/blocks/src/components/ArgsTable/Empty.tsx b/code/ui/blocks/src/components/ArgsTable/Empty.tsx index c4269a605f95..2f2c1bb40445 100644 --- a/code/ui/blocks/src/components/ArgsTable/Empty.tsx +++ b/code/ui/blocks/src/components/ArgsTable/Empty.tsx @@ -1,8 +1,8 @@ import type { FC } from 'react'; import React, { useEffect, useState } from 'react'; import { styled } from '@storybook/theming'; -import { Link } from '@storybook/components'; -import { DocumentIcon, SupportIcon, VideoIcon } from '@storybook/icons'; +import { Link, EmptyTabContent } from '@storybook/components'; +import { DocumentIcon, VideoIcon } from '@storybook/icons'; interface EmptyProps { inAddonPanel?: boolean; @@ -22,27 +22,6 @@ const Wrapper = styled.div<{ inAddonPanel?: boolean }>(({ inAddonPanel, theme }) boxShadow: 'rgba(0, 0, 0, 0.10) 0 1px 3px 0', })); -const Content = styled.div({ - display: 'flex', - flexDirection: 'column', - gap: 4, - maxWidth: 415, -}); - -const Title = styled.div(({ theme }) => ({ - fontWeight: theme.typography.weight.bold, - fontSize: theme.typography.size.s2 - 1, - textAlign: 'center', - color: theme.textColor, -})); - -const Description = styled.div(({ theme }) => ({ - fontWeight: theme.typography.weight.regular, - fontSize: theme.typography.size.s2 - 1, - textAlign: 'center', - color: theme.textMutedColor, -})); - const Links = styled.div(({ theme }) => ({ display: 'flex', fontSize: theme.typography.size.s2 - 1, @@ -73,39 +52,47 @@ export const Empty: FC = ({ inAddonPanel }) => { return ( - - - {inAddonPanel + <EmptyTabContent + title={ + inAddonPanel ? 'Interactive story playground' - : "Args table with interactive controls couldn't be auto-generated"} - - - Controls give you an easy to use interface to test your components. Set your story args - and you'll see controls appearing here automatically. - - - - {inAddonPanel && ( + : "Args table with interactive controls couldn't be auto-generated" + } + description={ <> - - Watch 5m video - - - - Read docs - + Controls give you an easy to use interface to test your components. Set your story args + and you'll see controls appearing here automatically. - )} - {!inAddonPanel && ( - - Learn how to set that up - - )} - + } + footer={ + + {inAddonPanel && ( + <> + + Watch 5m video + + + + Read docs + + + )} + {!inAddonPanel && ( + + Learn how to set that up + + )} + + } + /> ); }; diff --git a/code/ui/components/src/components/Button/Button.tsx b/code/ui/components/src/components/Button/Button.tsx index 37b64a786493..6e77f668b8df 100644 --- a/code/ui/components/src/components/Button/Button.tsx +++ b/code/ui/components/src/components/Button/Button.tsx @@ -170,6 +170,36 @@ const StyledButton = styled('button', { if (variant === 'ghost' && active) return theme.background.hoverable; return 'transparent'; })(), + ...(variant === 'ghost' + ? { + // This is a hack to apply bar styles to the button as soon as it is part of a bar + // It is a temporary solution until we have implemented Theming 2.0. + '.sb-bar &': { + background: (() => { + if (active) return transparentize(0.9, theme.barTextColor); + return 'transparent'; + })(), + color: (() => { + if (active) return theme.barSelectedColor; + return theme.barTextColor; + })(), + '&:hover': { + color: theme.barHoverColor, + background: transparentize(0.86, theme.barHoverColor), + }, + + '&:active': { + color: theme.barSelectedColor, + background: transparentize(0.9, theme.barSelectedColor), + }, + + '&:focus': { + boxShadow: `${rgba(theme.barHoverColor, 1)} 0 0 0 1px inset`, + outline: 'none', + }, + }, + } + : {}), color: (() => { if (variant === 'solid') return theme.color.lightest; if (variant === 'outline') return theme.input.color; diff --git a/code/ui/components/src/components/bar/bar.tsx b/code/ui/components/src/components/bar/bar.tsx index 27c595c65a8a..9320e2a97045 100644 --- a/code/ui/components/src/components/bar/bar.tsx +++ b/code/ui/components/src/components/bar/bar.tsx @@ -93,10 +93,10 @@ export interface FlexBarProps extends ComponentProps { backgroundColor?: string; } -export const FlexBar = ({ children, backgroundColor, ...rest }: FlexBarProps) => { +export const FlexBar = ({ children, backgroundColor, className, ...rest }: FlexBarProps) => { const [left, right] = Children.toArray(children); return ( - + {left} diff --git a/code/ui/components/src/components/bar/button.tsx b/code/ui/components/src/components/bar/button.tsx index 68ce83036b59..db838d581336 100644 --- a/code/ui/components/src/components/bar/button.tsx +++ b/code/ui/components/src/components/bar/button.tsx @@ -108,7 +108,7 @@ export const TabButton = styled(ButtonOrLink, { shouldForwardProp: isPropValid } '&:focus': { outline: '0 none', - borderBottomColor: theme.color.secondary, + borderBottomColor: theme.barSelectedColor, }, }), ({ active, textColor, theme }) => @@ -120,6 +120,9 @@ export const TabButton = styled(ButtonOrLink, { shouldForwardProp: isPropValid } : { color: textColor || theme.barTextColor, borderBottomColor: 'transparent', + '&:hover': { + color: theme.barHoverColor, + }, } ); TabButton.displayName = 'TabButton'; diff --git a/code/ui/components/src/components/tabs/EmptyTabContent.stories.tsx b/code/ui/components/src/components/tabs/EmptyTabContent.stories.tsx new file mode 100644 index 000000000000..3ef2da755f34 --- /dev/null +++ b/code/ui/components/src/components/tabs/EmptyTabContent.stories.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { EmptyTabContent } from './EmptyTabContent'; +import { DocumentIcon } from '@storybook/icons'; +import { Link } from '@storybook/components'; +import type { Meta, StoryObj } from '@storybook/react'; + +export default { + component: EmptyTabContent, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +type Story = StoryObj; + +export const OnlyTitle: Story = { + args: { + title: 'Nothing found', + }, +}; + +export const TitleAndDescription: Story = { + args: { + title: 'Nothing found', + description: 'Sorry, there is nothing to display here.', + }, +}; + +export const TitleAndFooter: Story = { + args: { + title: 'Nothing found', + footer: ( + + See the docs + + ), + }, +}; + +export const TitleDescriptionAndFooter: Story = { + args: { + title: 'Nothing found', + description: 'Sorry, there is nothing to display here.', + footer: ( + + See the docs + + ), + }, +}; diff --git a/code/ui/components/src/components/tabs/EmptyTabContent.tsx b/code/ui/components/src/components/tabs/EmptyTabContent.tsx new file mode 100644 index 000000000000..eec65f6183d7 --- /dev/null +++ b/code/ui/components/src/components/tabs/EmptyTabContent.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { styled } from '@storybook/theming'; + +const Wrapper = styled.div(({ theme }) => ({ + height: '100%', + display: 'flex', + padding: 30, + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + gap: 15, + background: theme.background.content, +})); + +const Content = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: 4, + maxWidth: 415, +}); + +const Title = styled.div(({ theme }) => ({ + fontWeight: theme.typography.weight.bold, + fontSize: theme.typography.size.s2 - 1, + textAlign: 'center', + color: theme.textColor, +})); + +const Description = styled.div(({ theme }) => ({ + fontWeight: theme.typography.weight.regular, + fontSize: theme.typography.size.s2 - 1, + textAlign: 'center', + color: theme.textMutedColor, +})); + +interface Props { + title: React.ReactNode; + description?: React.ReactNode; + footer?: React.ReactNode; +} + +export const EmptyTabContent = ({ title, description, footer }: Props) => { + return ( + + + {title} + {description && {description}} + + {footer} + + ); +}; diff --git a/code/ui/components/src/components/tabs/tabs.hooks.tsx b/code/ui/components/src/components/tabs/tabs.hooks.tsx index f5fbd7272fe6..d932b974b81e 100644 --- a/code/ui/components/src/components/tabs/tabs.hooks.tsx +++ b/code/ui/components/src/components/tabs/tabs.hooks.tsx @@ -22,11 +22,14 @@ const CollapseIcon = styled.span<{ isActive: boolean }>(({ theme, isActive }) => const AddonButton = styled(TabButton)<{ preActive: boolean }>(({ active, theme, preActive }) => { return ` - color: ${preActive || active ? theme.color.secondary : theme.color.mediumdark}; + color: ${preActive || active ? theme.barSelectedColor : theme.barTextColor}; + .addon-collapsible-icon { + color: ${preActive || active ? theme.barSelectedColor : theme.barTextColor}; + } &:hover { - color: ${theme.color.secondary}; + color: ${theme.barHoverColor}; .addon-collapsible-icon { - color: ${theme.color.secondary}; + color: ${theme.barHoverColor}; } } `; diff --git a/code/ui/components/src/components/tabs/tabs.stories.tsx b/code/ui/components/src/components/tabs/tabs.stories.tsx index 46a332a87f1f..a3c40fd8a9d9 100644 --- a/code/ui/components/src/components/tabs/tabs.stories.tsx +++ b/code/ui/components/src/components/tabs/tabs.stories.tsx @@ -1,9 +1,9 @@ import { expect } from '@storybook/test'; -import React, { Fragment } from 'react'; +import React from 'react'; import { action } from '@storybook/addon-actions'; import type { Meta, StoryObj } from '@storybook/react'; import { within, fireEvent, waitFor, screen, userEvent, findByText } from '@storybook/test'; -import { CPUIcon, MemoryIcon } from '@storybook/icons'; +import { BottomBarIcon, CloseIcon } from '@storybook/icons'; import { Tabs, TabsState, TabWrapper } from './tabs'; import type { ChildrenList } from './tabs.helpers'; import { IconButton } from '../IconButton/IconButton'; @@ -260,7 +260,27 @@ export const StatelessBordered = { ), } satisfies Story; +const AddonTools = () => ( +
+ + + + + + +
+); + export const StatelessWithTools = { + args: { + tools: , + }, render: (args) => ( - - - - - - - - } {...args} > {content} ), -} satisfies Story; +} satisfies StoryObj; export const StatelessAbsolute = { parameters: { @@ -303,7 +313,7 @@ export const StatelessAbsolute = { {content} ), -} satisfies Story; +} satisfies StoryObj; export const StatelessAbsoluteBordered = { parameters: { @@ -323,9 +333,13 @@ export const StatelessAbsoluteBordered = { {content} ), -} satisfies Story; +} satisfies StoryObj; -export const StatelessEmpty = { +export const StatelessEmptyWithTools = { + args: { + ...StatelessWithTools.args, + showToolsWhenEmpty: true, + }, parameters: { layout: 'fullscreen', }, @@ -340,4 +354,25 @@ export const StatelessEmpty = { {...args} /> ), -} satisfies Story; +} satisfies StoryObj; + +export const StatelessWithCustomEmpty = { + args: { + ...StatelessEmptyWithTools.args, + emptyState:
I am custom!
, + }, + parameters: { + layout: 'fullscreen', + }, + render: (args) => ( + + ), +} satisfies StoryObj; diff --git a/code/ui/components/src/components/tabs/tabs.tsx b/code/ui/components/src/components/tabs/tabs.tsx index 0e3484eab4ba..5b0cbb2b5612 100644 --- a/code/ui/components/src/components/tabs/tabs.tsx +++ b/code/ui/components/src/components/tabs/tabs.tsx @@ -1,14 +1,14 @@ import type { FC, PropsWithChildren, ReactElement, ReactNode, SyntheticEvent } from 'react'; -import React, { useMemo, Component, Fragment, memo } from 'react'; +import React, { useMemo, Component, memo } from 'react'; import { styled } from '@storybook/theming'; import { sanitize } from '@storybook/csf'; import type { Addon_RenderOptions } from '@storybook/types'; -import { Placeholder } from '../placeholder/placeholder'; import { TabButton } from '../bar/button'; import { FlexBar } from '../bar/bar'; import { childrenToList, VisuallyHidden } from './tabs.helpers'; import { useList } from './tabs.hooks'; +import { EmptyTabContent } from './EmptyTabContent'; const ignoreSsrWarning = '/* emotion-disable-server-rendering-unsafe-selector-warning-please-do-not-use-this-the-warning-exists-for-a-reason */'; @@ -119,6 +119,8 @@ export interface TabsProps { }>[]; id?: string; tools?: ReactNode; + showToolsWhenEmpty?: boolean; + emptyState?: ReactNode; selected?: string; actions?: { onSelect: (id: string) => void; @@ -140,6 +142,8 @@ export const Tabs: FC = memo( backgroundColor, id: htmlId, menuName, + emptyState, + showToolsWhenEmpty, }) => { const idList = childrenToList(children) .map((i) => i.id) @@ -157,7 +161,13 @@ export const Tabs: FC = memo( const { visibleList, tabBarRef, tabRefs, AddonTab } = useList(list); - return list.length ? ( + const EmptyContent = emptyState ?? ; + + if (!showToolsWhenEmpty && list.length === 0) { + return EmptyContent; + } + + return ( @@ -190,15 +200,13 @@ export const Tabs: FC = memo( {tools} - {list.map(({ id, active, render }) => { - return React.createElement(render, { key: id, active }, null); - })} + {list.length + ? list.map(({ id, active, render }) => { + return React.createElement(render, { key: id, active }, null); + }) + : EmptyContent} - ) : ( - - Nothing found - ); } ); diff --git a/code/ui/components/src/index.ts b/code/ui/components/src/index.ts index f43d08b5d8f5..ffe4a08d699f 100644 --- a/code/ui/components/src/index.ts +++ b/code/ui/components/src/index.ts @@ -66,6 +66,7 @@ export { default as ListItem } from './components/tooltip/ListItem'; // Toolbar and subcomponents export { Tabs, TabsState, TabBar, TabWrapper } from './components/tabs/tabs'; +export { EmptyTabContent } from './components/tabs/EmptyTabContent'; export { IconButtonSkeleton, TabButton } from './components/bar/button'; export { Separator, interleaveSeparators } from './components/bar/separator'; export { Bar, FlexBar } from './components/bar/bar'; diff --git a/code/ui/manager/src/components/mobile/navigation/MobileNavigation.tsx b/code/ui/manager/src/components/mobile/navigation/MobileNavigation.tsx index 47b7587019f8..9ecee759fb06 100644 --- a/code/ui/manager/src/components/mobile/navigation/MobileNavigation.tsx +++ b/code/ui/manager/src/components/mobile/navigation/MobileNavigation.tsx @@ -45,7 +45,7 @@ export const MobileNavigation: FC = ({ menu, panel, showP {isMobilePanelOpen ? ( {panel} ) : ( -