diff --git a/examples/official-storybook/manager.js b/examples/official-storybook/manager.js index 7f92c9e1b8fe..b3c7f0839ac9 100644 --- a/examples/official-storybook/manager.js +++ b/examples/official-storybook/manager.js @@ -15,4 +15,5 @@ addons.setConfig({ hidden: true, }, }, + collapsedRoots: ['other'], }); diff --git a/lib/api/src/lib/stories.ts b/lib/api/src/lib/stories.ts index 6177e8d308f9..ddc2128eb041 100644 --- a/lib/api/src/lib/stories.ts +++ b/lib/api/src/lib/stories.ts @@ -19,6 +19,7 @@ export interface Root { isComponent: false; isRoot: true; isLeaf: false; + startCollapsed?: boolean; } export interface Group { @@ -143,7 +144,7 @@ export const transformStoriesRawToStoriesHash = ( const storiesHashOutOfOrder = values.reduce((acc, item) => { const { kind, parameters } = item; - const { showRoots } = provider.getConfig(); + const { showRoots, collapsedRoots = [] } = provider.getConfig(); const setShowRoots = typeof showRoots !== 'undefined'; if (usesOldHierarchySeparator && !setShowRoots) { @@ -176,6 +177,7 @@ export const transformStoriesRawToStoriesHash = ( isComponent: false, isLeaf: false, isRoot: true, + startCollapsed: collapsedRoots.includes(id), }); } else { list.push({ diff --git a/lib/ui/src/components/sidebar/RefIndicator.tsx b/lib/ui/src/components/sidebar/RefIndicator.tsx index 115d8475b940..2f7344da392b 100644 --- a/lib/ui/src/components/sidebar/RefIndicator.tsx +++ b/lib/ui/src/components/sidebar/RefIndicator.tsx @@ -200,7 +200,7 @@ export const RefIndicator = React.memo( } > - + diff --git a/lib/ui/src/components/sidebar/Refs.tsx b/lib/ui/src/components/sidebar/Refs.tsx index a8fbb9529b9a..3e92178489fd 100644 --- a/lib/ui/src/components/sidebar/Refs.tsx +++ b/lib/ui/src/components/sidebar/Refs.tsx @@ -142,7 +142,7 @@ export const Ref: FunctionComponent = React.memo((props) => aria-label={`${isExpanded ? 'Hide' : 'Show'} ${title} stories`} aria-expanded={isExpanded} > - + {title} diff --git a/lib/ui/src/components/sidebar/Tree.tsx b/lib/ui/src/components/sidebar/Tree.tsx index 2d3eddc2d1df..5156fdef0a09 100644 --- a/lib/ui/src/components/sidebar/Tree.tsx +++ b/lib/ui/src/components/sidebar/Tree.tsx @@ -4,8 +4,15 @@ import { Icons } from '@storybook/components'; import { transparentize } from 'polished'; import React, { MutableRefObject, useCallback, useMemo, useRef } from 'react'; -import { ComponentNode, DocumentNode, GroupNode, RootNode, StoryNode } from './TreeNode'; -import { useExpanded, ExpandAction } from './useExpanded'; +import { + ComponentNode, + DocumentNode, + GroupNode, + RootNode, + StoryNode, + CollapseIcon, +} from './TreeNode'; +import { useExpanded, ExpandAction, ExpandedState } from './useExpanded'; import { Highlight, Item } from './types'; import { createId, getAncestorIds, getDescendantIds, getLink } from './utils'; @@ -16,6 +23,7 @@ export const Action = styled.button(({ theme }) => ({ width: 20, height: 20, margin: 0, + marginLeft: 'auto', padding: 0, outline: 0, lineHeight: 'normal', @@ -42,6 +50,44 @@ export const Action = styled.button(({ theme }) => ({ }, })); +const CollapseButton = styled.button(({ theme }) => ({ + // Reset button + background: 'transparent', + border: 'none', + outline: 'none', + boxSizing: 'content-box', + cursor: 'pointer', + position: 'relative', + textAlign: 'left', + lineHeight: 'normal', + font: 'inherit', + color: 'inherit', + letterSpacing: 'inherit', + textTransform: 'inherit', + + display: 'flex', + flex: '0 1 auto', + padding: '3px 10px 1px 1px', + margin: 0, + marginLeft: -19, + overflow: 'hidden', + borderRadius: 26, + transition: 'color 150ms, box-shadow 150ms', + + 'span:first-of-type': { + marginTop: 4, + marginRight: 7, + }, + + '&:focus': { + boxShadow: `0 0 0 1px ${theme.color.secondary}`, + color: theme.color.secondary, + 'span:first-of-type': { + color: theme.color.secondary, + }, + }, +})); + interface NodeProps { item: Item; refId: string; @@ -105,20 +151,33 @@ const Node = React.memo( data-ref-id={refId} data-item-id={item.id} data-nodetype="root" + aria-expanded={isExpanded} > - {item.name} - { event.preventDefault(); - setFullyExpanded(); + setExpanded({ ids: [item.id], value: !isExpanded }); }} > - - + + {item.name} + + {isExpanded && ( + { + event.preventDefault(); + setFullyExpanded(); + }} + > + + + )} ); } @@ -197,16 +256,17 @@ export const Tree = React.memo<{ const containerRef = useRef(null); // Find top-level nodes and group them so we can hoist any orphans and expand any roots. - const [rootIds, orphanIds] = useMemo( + const [rootIds, orphanIds, initialExpanded] = useMemo( () => - Object.keys(data).reduce<[string[], string[]]>( + Object.keys(data).reduce<[string[], string[], ExpandedState]>( (acc, id) => { const item = data[id]; if (isRoot(item)) acc[0].push(id); else if (!item.parent) acc[1].push(id); + if (isRoot(item) && item.startCollapsed) acc[2][id] = false; return acc; }, - [[], []] + [[], [], {}] ), [data] ); @@ -277,6 +337,7 @@ export const Tree = React.memo<{ isBrowsing, // only enable keyboard shortcuts when tree is visible refId, data: collapsedData, + initialExpanded, rootIds, highlightedRef, setHighlightedItemId, diff --git a/lib/ui/src/components/sidebar/TreeNode.tsx b/lib/ui/src/components/sidebar/TreeNode.tsx index 44612d79f503..f745f38274b5 100644 --- a/lib/ui/src/components/sidebar/TreeNode.tsx +++ b/lib/ui/src/components/sidebar/TreeNode.tsx @@ -11,9 +11,10 @@ export const CollapseIcon = styled.span<{ isExpanded: boolean }>(({ theme, isExp marginTop: 6, marginLeft: 8, marginRight: 5, + color: transparentize(0.4, theme.color.mediumdark), borderTop: '3px solid transparent', borderBottom: '3px solid transparent', - borderLeft: `3px solid ${transparentize(0.4, theme.color.mediumdark)}`, + borderLeft: `3px solid`, transform: isExpanded ? 'rotateZ(90deg)' : 'none', transition: 'transform .1s ease-out', })); @@ -121,11 +122,13 @@ export const Path = styled.span(({ theme }) => ({ }, })); -export const RootNode = styled.span(({ theme }) => ({ +export const RootNode = styled.div(({ theme }) => ({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', - margin: '16px 20px 4px 20px', + padding: '0 20px', + marginTop: 16, + marginBottom: 4, fontSize: `${theme.typography.size.s1 - 1}px`, fontWeight: theme.typography.weight.black, lineHeight: '16px', diff --git a/lib/ui/src/components/sidebar/useExpanded.ts b/lib/ui/src/components/sidebar/useExpanded.ts index f4e717449732..b4d6e1ed88f5 100644 --- a/lib/ui/src/components/sidebar/useExpanded.ts +++ b/lib/ui/src/components/sidebar/useExpanded.ts @@ -20,6 +20,7 @@ export interface ExpandedProps { isBrowsing: boolean; refId: string; data: StoriesHash; + initialExpanded?: ExpandedState; rootIds: string[]; highlightedRef: MutableRefObject; setHighlightedItemId: (storyId: string) => void; @@ -30,11 +31,13 @@ export interface ExpandedProps { const initializeExpanded = ({ refId, data, + initialExpanded, highlightedRef, rootIds, }: { refId: string; data: StoriesHash; + initialExpanded?: ExpandedState; highlightedRef: MutableRefObject; rootIds: string[]; }) => { @@ -43,7 +46,7 @@ const initializeExpanded = ({ ? getAncestorIds(data, highlightedRef.current?.itemId) : []; return [...rootIds, ...highlightedAncestors].reduce( - (acc, id) => Object.assign(acc, { [id]: true }), + (acc, id) => Object.assign(acc, { [id]: id in initialExpanded ? initialExpanded[id] : true }), {} ); }; @@ -55,16 +58,17 @@ export const useExpanded = ({ isBrowsing, refId, data, + initialExpanded, rootIds, highlightedRef, setHighlightedItemId, selectedStoryId, onSelectStoryId, -}: ExpandedProps): [Record, Dispatch] => { +}: ExpandedProps): [ExpandedState, Dispatch] => { const api = useStorybookApi(); // Track the set of currently expanded nodes within this tree. - // Root nodes are expanded by default (and cannot be collapsed). + // Root nodes are expanded by default. const [expanded, setExpanded] = useReducer< React.Reducer, { @@ -72,11 +76,12 @@ export const useExpanded = ({ data: StoriesHash; highlightedRef: MutableRefObject; rootIds: string[]; + initialExpanded: ExpandedState; } >( (state, { ids, value }) => ids.reduce((acc, id) => Object.assign(acc, { [id]: value }), { ...state }), - { refId, data, highlightedRef, rootIds }, + { refId, data, highlightedRef, rootIds, initialExpanded }, initializeExpanded );