diff --git a/docs/configure/features-and-behavior.md b/docs/configure/features-and-behavior.md index 3dc9c0fc33b6..b6b0596f7c5a 100644 --- a/docs/configure/features-and-behavior.md +++ b/docs/configure/features-and-behavior.md @@ -35,3 +35,4 @@ The following options are configurable under the `sidebar` namespace: | ----------------------|:-------------:|:-------------------------------------------------------------:|:----------------------------------------------:| | **showRoots** | Boolean |Display the top-level nodes as a "root" in the sidebar |`false` | | **collapsedRoots** | Array |Set of root node IDs to visually collapse by default |`['misc', 'other']` | +| **renderLabel** | Function |Create a custom label for tree nodes; must return a ReactNode |`(item) => {item.name}`| diff --git a/examples/official-storybook/manager.js b/examples/official-storybook/manager.js index cd74ecf5fd6f..b46efbc338c5 100644 --- a/examples/official-storybook/manager.js +++ b/examples/official-storybook/manager.js @@ -1,10 +1,21 @@ +import React from 'react'; import { addons } from '@storybook/addons'; -import { themes } from '@storybook/theming'; +import { themes, styled } from '@storybook/theming'; +import { Icons } from '@storybook/components'; import addHeadWarning from './head-warning'; addHeadWarning('manager-head-not-loaded', 'Manager head not loaded'); +const PrefixIcon = styled(Icons)(({ theme }) => ({ + marginRight: 8, + fontSize: 'inherit', + height: '1em', + width: '1em', + display: 'inline', + alignSelf: 'center', +})); + addons.setConfig({ theme: themes.light, // { base: 'dark', brandTitle: 'Storybook!' }, previewTabs: { @@ -17,5 +28,34 @@ addons.setConfig({ }, sidebar: { collapsedRoots: ['other'], + renderLabel: ({ id, name }) => { + const map = { + addons: ( + <> + + {name} + + ), + 'addons-a11y': ( + <> + + {name} + + ), + 'addons-a11y-basebutton': ( + <> + + {name} + + ), + 'addons-a11y-basebutton--default': ( + <> + + {name} + + ), + }; + return map[id]; + }, }, }); diff --git a/lib/api/src/lib/stories.ts b/lib/api/src/lib/stories.ts index c6e0991da219..3983d06525f8 100644 --- a/lib/api/src/lib/stories.ts +++ b/lib/api/src/lib/stories.ts @@ -1,3 +1,4 @@ +import React from 'react'; import deprecate from 'util-deprecate'; import dedent from 'ts-dedent'; import { sanitize } from '@storybook/csf'; @@ -19,6 +20,7 @@ export interface Root { isComponent: false; isRoot: true; isLeaf: false; + label?: React.ReactNode; startCollapsed?: boolean; } @@ -32,6 +34,7 @@ export interface Group { isComponent: boolean; isRoot: false; isLeaf: false; + label?: React.ReactNode; // MDX docs-only stories are "Group" type parameters?: { docsOnly?: boolean; @@ -50,6 +53,7 @@ export interface Story { isComponent: boolean; isRoot: false; isLeaf: true; + label?: React.ReactNode; parameters?: { fileName: string; options: { @@ -153,7 +157,7 @@ export const transformStoriesRawToStoriesHash = ( const storiesHashOutOfOrder = values.reduce((acc, item) => { const { kind, parameters } = item; const { sidebar = {}, showRoots: deprecatedShowRoots } = provider.getConfig(); - const { showRoots = deprecatedShowRoots, collapsedRoots = [] } = sidebar; + const { showRoots = deprecatedShowRoots, collapsedRoots = [], renderLabel } = sidebar; if (typeof deprecatedShowRoots !== 'undefined') { warnLegacyShowRoots(); @@ -182,7 +186,7 @@ export const transformStoriesRawToStoriesHash = ( } if (root.length && index === 0) { - list.push({ + const rootElement: Root = { id, name, depth: index, @@ -191,9 +195,10 @@ export const transformStoriesRawToStoriesHash = ( isLeaf: false, isRoot: true, startCollapsed: collapsedRoots.includes(id), - }); + }; + list.push({ ...rootElement, label: renderLabel?.(rootElement) }); } else { - list.push({ + const groupElement: Group = { id, name, parent, @@ -206,6 +211,10 @@ export const transformStoriesRawToStoriesHash = ( docsOnly: parameters?.docsOnly, viewMode: parameters?.viewMode, }, + }; + list.push({ + ...groupElement, + label: renderLabel?.(groupElement), }); } @@ -232,7 +241,7 @@ export const transformStoriesRawToStoriesHash = ( isComponent: false, isRoot: false, }; - acc[item.id] = story; + acc[item.id] = { ...story, label: renderLabel?.(story) }; return acc; }, {} as StoriesHash); diff --git a/lib/api/src/modules/provider.ts b/lib/api/src/modules/provider.ts index b0766d71e0e0..298546b1dcdd 100644 --- a/lib/api/src/modules/provider.ts +++ b/lib/api/src/modules/provider.ts @@ -2,13 +2,14 @@ import { ReactNode } from 'react'; import { Channel } from '@storybook/channels'; import { ThemeVars } from '@storybook/theming'; -import { API, State, ModuleFn } from '../index'; +import { API, State, ModuleFn, Root, Group, Story } from '../index'; import { StoryMapper, Refs } from './refs'; import { UIOptions } from './layout'; interface SidebarOptions { showRoots?: boolean; collapsedRoots?: string[]; + renderLabel?: (item: Root | Group | Story) => ReactNode; } type IframeRenderer = ( diff --git a/lib/ui/src/components/sidebar/Tree.stories.tsx b/lib/ui/src/components/sidebar/Tree.stories.tsx index 8cb2c0ef791d..98a38247522e 100644 --- a/lib/ui/src/components/sidebar/Tree.stories.tsx +++ b/lib/ui/src/components/sidebar/Tree.stories.tsx @@ -44,6 +44,7 @@ const singleStoryComponent = { isComponent: true, isLeaf: false, isRoot: false, + label: 🔥 Single, }, 'single--single': { id: 'single--single', @@ -58,6 +59,7 @@ const singleStoryComponent = { isLeaf: true, isComponent: false, isRoot: false, + label: 🔥 Single, }, }; diff --git a/lib/ui/src/components/sidebar/Tree.tsx b/lib/ui/src/components/sidebar/Tree.tsx index 5156fdef0a09..616724ccbec4 100644 --- a/lib/ui/src/components/sidebar/Tree.tsx +++ b/lib/ui/src/components/sidebar/Tree.tsx @@ -137,7 +137,7 @@ const Node = React.memo( onSelectStoryId(item.id); }} > - {item.name} + {item.label || item.name} ); } @@ -162,7 +162,7 @@ const Node = React.memo( }} > - {item.name} + {item.label || item.name} {isExpanded && ( ( if (item.isComponent && !isExpanded) onSelectStoryId(item.id); }} > - {item.name} + {item.label || item.name} ); }