From 38e437c301daa9e57ea9ca7491d928a922e0c670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DUNGLER?= Date: Wed, 20 Jan 2021 22:13:05 +0100 Subject: [PATCH 1/9] feat(Tree): add capacity to collapse roots --- examples/official-storybook/manager.js | 1 + lib/api/src/lib/stories.ts | 4 +- lib/ui/src/components/sidebar/Tree.tsx | 218 +++++++++++++++++++-- lib/ui/src/components/sidebar/TreeNode.tsx | 3 +- 4 files changed, 209 insertions(+), 17 deletions(-) diff --git a/examples/official-storybook/manager.js b/examples/official-storybook/manager.js index 7f92c9e1b8fe..0240bdbb487b 100644 --- a/examples/official-storybook/manager.js +++ b/examples/official-storybook/manager.js @@ -15,4 +15,5 @@ addons.setConfig({ hidden: true, }, }, + initialRootsCollapsed: ['other'], }); diff --git a/lib/api/src/lib/stories.ts b/lib/api/src/lib/stories.ts index 7bf8b04bbbd3..3033f07b004c 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; + defaultCollapsed?: boolean; } export interface Group { @@ -141,7 +142,7 @@ export const transformStoriesRawToStoriesHash = ( const storiesHashOutOfOrder = values.reduce((acc, item) => { const { kind, parameters } = item; - const { showRoots } = provider.getConfig(); + const { showRoots, initialRootsCollapsed = [] } = provider.getConfig(); const setShowRoots = typeof showRoots !== 'undefined'; if (usesOldHierarchySeparator && !setShowRoots) { @@ -174,6 +175,7 @@ export const transformStoriesRawToStoriesHash = ( isComponent: false, isLeaf: false, isRoot: true, + defaultCollapsed: initialRootsCollapsed.includes(id), }); } else { list.push({ diff --git a/lib/ui/src/components/sidebar/Tree.tsx b/lib/ui/src/components/sidebar/Tree.tsx index 2d3eddc2d1df..844a7501a2b1 100644 --- a/lib/ui/src/components/sidebar/Tree.tsx +++ b/lib/ui/src/components/sidebar/Tree.tsx @@ -2,9 +2,16 @@ import { Group, Story, StoriesHash, isRoot, isStory } from '@storybook/api'; import { styled } from '@storybook/theming'; import { Icons } from '@storybook/components'; import { transparentize } from 'polished'; -import React, { MutableRefObject, useCallback, useMemo, useRef } from 'react'; +import React, { MutableRefObject, useCallback, useMemo, useRef, useEffect } from 'react'; -import { ComponentNode, DocumentNode, GroupNode, RootNode, StoryNode } from './TreeNode'; +import { + ComponentNode, + DocumentNode, + GroupNode, + RootNode, + StoryNode, + CollapseIcon, +} from './TreeNode'; import { useExpanded, ExpandAction } 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,47 @@ 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: 1, + padding: 3, + paddingLeft: 1, + paddingRight: 12, + margin: 0, + marginLeft: -20, + overflow: 'hidden', + + 'span:first-of-type': { + marginTop: 5, + }, + + '&:focus': { + borderColor: theme.color.secondary, + 'span:first-of-type': { + borderLeftColor: theme.color.secondary, + }, + }, +})); + +type DisplayableNode = { + id: string; + children?: string[]; +}; + interface NodeProps { item: Item; refId: string; @@ -53,6 +102,8 @@ interface NodeProps { setExpanded: (action: ExpandAction) => void; setFullyExpanded?: () => void; onSelectStoryId: (itemId: string) => void; + sectionCollapsed?: boolean; + toggleSectionCollapse?: (id: string) => void; } const Node = React.memo( @@ -67,6 +118,8 @@ const Node = React.memo( isExpanded, setExpanded, onSelectStoryId, + sectionCollapsed, + toggleSectionCollapse, }) => { if (!isDisplayed) return null; @@ -106,7 +159,16 @@ const Node = React.memo( data-item-id={item.id} data-nodetype="root" > - {item.name} + { + event.preventDefault(); + toggleSectionCollapse?.(id); + }} + > + + {item.name} + ( ); const Root = React.memo( - ({ setExpanded, isFullyExpanded, expandableDescendants, ...props }) => { - const setFullyExpanded = useCallback( - () => setExpanded({ ids: expandableDescendants, value: !isFullyExpanded }), - [setExpanded, isFullyExpanded, expandableDescendants] - ); + ({ + item, + setExpanded, + isFullyExpanded, + expandableDescendants, + sectionCollapsed, + toggleSectionCollapse, + ...props + }) => { + const setFullyExpanded = useCallback(() => { + setExpanded({ ids: expandableDescendants, value: !isFullyExpanded }); + if (sectionCollapsed) { + toggleSectionCollapse?.(item.id); + } + }, [setExpanded, isFullyExpanded, expandableDescendants]); return ( ((props) => ({ marginBottom: 20, })); +const Section = styled.div<{ expanded: boolean }>(({ expanded }) => ({ + height: expanded ? 'auto' : 0, + overflow: expanded ? 'visible' : 'hidden', +})); + export const Tree = React.memo<{ isBrowsing: boolean; isMain: boolean; @@ -197,16 +277,20 @@ 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, defaultCollapsedRootIds] = useMemo( () => - Object.keys(data).reduce<[string[], string[]]>( + Object.keys(data).reduce<[string[], string[], string[]]>( (acc, id) => { const item = data[id]; - if (isRoot(item)) acc[0].push(id); - else if (!item.parent) acc[1].push(id); + if (isRoot(item)) { + acc[0].push(id); + if (item.defaultCollapsed) { + acc[2].push(id); + } + } else if (!item.parent) acc[1].push(id); return acc; }, - [[], []] + [[], [], []] ), [data] ); @@ -284,8 +368,114 @@ export const Tree = React.memo<{ onSelectStoryId, }); + /** + * Build the array for + * Single Story + * Root + * Story1 + * Story2 + */ + const itemsDisplayed = useMemo(() => { + return collapsedItems.reduce((acc, itemId) => { + const item = collapsedData[itemId]; + const latestRoot = acc.pop(); + if (latestRoot && isRoot(collapsedData[latestRoot.id]) && !isRoot(item)) { + acc.push({ + ...latestRoot, + children: [...latestRoot.children, itemId], + }); + } else { + // an orphan or a root, we add the latest one back + if (latestRoot) { + acc.push(latestRoot); + } + acc.push({ id: itemId, children: [] }); + } + return acc; + }, []); + }, [collapsedItems, collapsedData]); + + const [sectionCollapsed, setSectionCollapsed] = React.useState( + defaultCollapsedRootIds + ); + useEffect(() => setSectionCollapsed(defaultCollapsedRootIds), [defaultCollapsedRootIds]); + const handleSectionToggle = useCallback((id: string) => { + setSectionCollapsed((prev) => + prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] + ); + }, []); + return ( 0}> + {itemsDisplayed.map((itemDisplay) => { + const item = collapsedData[itemDisplay.id]; + + const id = createId(itemDisplay.id, refId); + + if (isRoot(item)) { + const descendants = expandableDescendants[item.id]; + const isFullyExpanded = descendants.every((d: string) => expanded[d]); + const collapsed = sectionCollapsed.includes(id); + return ( + <> + +
+ {itemDisplay.children?.map((childId) => { + const child = collapsedData[childId] as Group | Story; + + const isDisplayed = + !child.parent || ancestry[childId].every((a: string) => expanded[a]); + return ( + childId === oid || childId.startsWith(`${oid}-`) + )} + isDisplayed={isDisplayed} + isSelected={selectedStoryId === childId} + isExpanded={!!expanded[childId]} + setExpanded={setExpanded} + onSelectStoryId={onSelectStoryId} + /> + ); + })} +
+ + ); + } + + const isDisplayed = !item.parent || ancestry[item.id].every((a: string) => expanded[a]); + return ( + item.id === oid || item.id.startsWith(`${oid}-`))} + isDisplayed={isDisplayed} + isSelected={selectedStoryId === item.id} + isExpanded={!!expanded[item.id]} + setExpanded={setExpanded} + onSelectStoryId={onSelectStoryId} + /> + ); + })} + {/*
Old one
{collapsedItems.map((itemId) => { const item = collapsedData[itemId]; const id = createId(itemId, refId); @@ -324,7 +514,7 @@ export const Tree = React.memo<{ onSelectStoryId={onSelectStoryId} /> ); - })} + })} */}
); } diff --git a/lib/ui/src/components/sidebar/TreeNode.tsx b/lib/ui/src/components/sidebar/TreeNode.tsx index 44612d79f503..8982184958a7 100644 --- a/lib/ui/src/components/sidebar/TreeNode.tsx +++ b/lib/ui/src/components/sidebar/TreeNode.tsx @@ -121,10 +121,9 @@ 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', fontSize: `${theme.typography.size.s1 - 1}px`, fontWeight: theme.typography.weight.black, From 6a406bd85d40274454ad39bf854b8c4e73b52419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DUNGLER?= Date: Wed, 20 Jan 2021 22:18:03 +0100 Subject: [PATCH 2/9] cleanup --- lib/ui/src/components/sidebar/Tree.tsx | 40 -------------------------- 1 file changed, 40 deletions(-) diff --git a/lib/ui/src/components/sidebar/Tree.tsx b/lib/ui/src/components/sidebar/Tree.tsx index 844a7501a2b1..78d4f343c28b 100644 --- a/lib/ui/src/components/sidebar/Tree.tsx +++ b/lib/ui/src/components/sidebar/Tree.tsx @@ -475,46 +475,6 @@ export const Tree = React.memo<{ /> ); })} - {/*
Old one
- {collapsedItems.map((itemId) => { - const item = collapsedData[itemId]; - const id = createId(itemId, refId); - - if (isRoot(item)) { - const descendants = expandableDescendants[item.id]; - const isFullyExpanded = descendants.every((d: string) => expanded[d]); - return ( - - ); - } - - const isDisplayed = !item.parent || ancestry[itemId].every((a: string) => expanded[a]); - return ( - itemId === oid || itemId.startsWith(`${oid}-`))} - isDisplayed={isDisplayed} - isSelected={selectedStoryId === itemId} - isExpanded={!!expanded[itemId]} - setExpanded={setExpanded} - onSelectStoryId={onSelectStoryId} - /> - ); - })} */} ); } From 078a94df7dc123356c4c54a75f8c88da4b6a85ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DUNGLER?= Date: Fri, 29 Jan 2021 22:55:39 +0100 Subject: [PATCH 3/9] fix keyboard navigation and ux --- .../components/sidebar/HighlightStyles.tsx | 2 +- lib/ui/src/components/sidebar/Tree.tsx | 37 ++++++------------- lib/ui/src/components/sidebar/TreeNode.tsx | 3 +- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/lib/ui/src/components/sidebar/HighlightStyles.tsx b/lib/ui/src/components/sidebar/HighlightStyles.tsx index c1f8269e37b2..3ad0f7078c56 100644 --- a/lib/ui/src/components/sidebar/HighlightStyles.tsx +++ b/lib/ui/src/components/sidebar/HighlightStyles.tsx @@ -9,7 +9,7 @@ export const HighlightStyles: FunctionComponent = ({ refId, itemId }) const background = transparentize(0.85, color.secondary); return { [`[data-ref-id="${refId}"][data-item-id="${itemId}"]:not([data-selected="true"])`]: { - [`&[data-nodetype="component"], &[data-nodetype="group"]`]: { + [`&[data-nodetype="root"], &[data-nodetype="component"], &[data-nodetype="group"]`]: { background, '&:hover, &:focus': { background }, }, diff --git a/lib/ui/src/components/sidebar/Tree.tsx b/lib/ui/src/components/sidebar/Tree.tsx index 78d4f343c28b..fa9b7f951a39 100644 --- a/lib/ui/src/components/sidebar/Tree.tsx +++ b/lib/ui/src/components/sidebar/Tree.tsx @@ -102,8 +102,6 @@ interface NodeProps { setExpanded: (action: ExpandAction) => void; setFullyExpanded?: () => void; onSelectStoryId: (itemId: string) => void; - sectionCollapsed?: boolean; - toggleSectionCollapse?: (id: string) => void; } const Node = React.memo( @@ -118,8 +116,6 @@ const Node = React.memo( isExpanded, setExpanded, onSelectStoryId, - sectionCollapsed, - toggleSectionCollapse, }) => { if (!isDisplayed) return null; @@ -158,15 +154,17 @@ const Node = React.memo( data-ref-id={refId} data-item-id={item.id} data-nodetype="root" + data-highlightable + aria-expanded={isExpanded} > { event.preventDefault(); - toggleSectionCollapse?.(id); + setExpanded({ ids: [item.id], value: !isExpanded }); }} > - + {item.name} ( ); const Root = React.memo( - ({ - item, - setExpanded, - isFullyExpanded, - expandableDescendants, - sectionCollapsed, - toggleSectionCollapse, - ...props - }) => { + ({ item, setExpanded, isFullyExpanded, expandableDescendants, ...props }) => { const setFullyExpanded = useCallback(() => { - setExpanded({ ids: expandableDescendants, value: !isFullyExpanded }); - if (sectionCollapsed) { - toggleSectionCollapse?.(item.id); - } - }, [setExpanded, isFullyExpanded, expandableDescendants]); + setExpanded({ + ids: [!isFullyExpanded && item.id, ...expandableDescendants].filter(Boolean), + value: !isFullyExpanded, + }); + }, [setExpanded, isFullyExpanded, expandableDescendants, item.id]); return ( expanded[d]); - const collapsed = sectionCollapsed.includes(id); return ( <> -
+
{itemDisplay.children?.map((childId) => { const child = collapsedData[childId] as Group | Story; diff --git a/lib/ui/src/components/sidebar/TreeNode.tsx b/lib/ui/src/components/sidebar/TreeNode.tsx index 8982184958a7..a5ff9eae9b57 100644 --- a/lib/ui/src/components/sidebar/TreeNode.tsx +++ b/lib/ui/src/components/sidebar/TreeNode.tsx @@ -124,7 +124,8 @@ export const Path = styled.span(({ theme }) => ({ export const RootNode = styled.div(({ theme }) => ({ display: 'flex', alignItems: 'center', - margin: '16px 20px 4px 20px', + padding: '8px 20px 4px 20px', + marginTop: 8, fontSize: `${theme.typography.size.s1 - 1}px`, fontWeight: theme.typography.weight.black, lineHeight: '16px', From 0c5d30580e3ed0205b775e79e18b7278e1e362eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DUNGLER?= Date: Fri, 29 Jan 2021 23:16:18 +0100 Subject: [PATCH 4/9] make default working back --- lib/ui/src/components/sidebar/Tree.tsx | 11 +---------- lib/ui/src/components/sidebar/useExpanded.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/ui/src/components/sidebar/Tree.tsx b/lib/ui/src/components/sidebar/Tree.tsx index fa9b7f951a39..e4069bf00c2b 100644 --- a/lib/ui/src/components/sidebar/Tree.tsx +++ b/lib/ui/src/components/sidebar/Tree.tsx @@ -354,6 +354,7 @@ export const Tree = React.memo<{ setHighlightedItemId, selectedStoryId, onSelectStoryId, + defaultCollapsedRootIds, }); /** @@ -383,16 +384,6 @@ export const Tree = React.memo<{ }, []); }, [collapsedItems, collapsedData]); - const [sectionCollapsed, setSectionCollapsed] = React.useState( - defaultCollapsedRootIds - ); - useEffect(() => setSectionCollapsed(defaultCollapsedRootIds), [defaultCollapsedRootIds]); - const handleSectionToggle = useCallback((id: string) => { - setSectionCollapsed((prev) => - prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] - ); - }, []); - return ( 0}> {itemsDisplayed.map((itemDisplay) => { diff --git a/lib/ui/src/components/sidebar/useExpanded.ts b/lib/ui/src/components/sidebar/useExpanded.ts index f4e717449732..a9c5df697998 100644 --- a/lib/ui/src/components/sidebar/useExpanded.ts +++ b/lib/ui/src/components/sidebar/useExpanded.ts @@ -25,6 +25,7 @@ export interface ExpandedProps { setHighlightedItemId: (storyId: string) => void; selectedStoryId: string | null; onSelectStoryId: (storyId: string) => void; + defaultCollapsedRootIds?: string[]; } const initializeExpanded = ({ @@ -32,18 +33,20 @@ const initializeExpanded = ({ data, highlightedRef, rootIds, + defaultCollapsedRootIds, }: { refId: string; data: StoriesHash; highlightedRef: MutableRefObject; rootIds: string[]; + defaultCollapsedRootIds: string[]; }) => { const highlightedAncestors = highlightedRef.current?.refId === refId ? getAncestorIds(data, highlightedRef.current?.itemId) : []; return [...rootIds, ...highlightedAncestors].reduce( - (acc, id) => Object.assign(acc, { [id]: true }), + (acc, id) => Object.assign(acc, { [id]: !defaultCollapsedRootIds.includes(id) }), {} ); }; @@ -60,11 +63,12 @@ export const useExpanded = ({ setHighlightedItemId, selectedStoryId, onSelectStoryId, + defaultCollapsedRootIds, }: ExpandedProps): [Record, 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[]; + defaultCollapsedRootIds: string[]; } >( (state, { ids, value }) => ids.reduce((acc, id) => Object.assign(acc, { [id]: value }), { ...state }), - { refId, data, highlightedRef, rootIds }, + { refId, data, highlightedRef, rootIds, defaultCollapsedRootIds }, initializeExpanded ); From e5746e0979bf55dd2b29ffe97b46715905abf408 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 25 Feb 2021 14:39:53 +0100 Subject: [PATCH 5/9] Fix styling --- .../components/sidebar/HighlightStyles.tsx | 2 +- lib/ui/src/components/sidebar/Tree.tsx | 66 +++++++++---------- lib/ui/src/components/sidebar/TreeNode.tsx | 5 +- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/lib/ui/src/components/sidebar/HighlightStyles.tsx b/lib/ui/src/components/sidebar/HighlightStyles.tsx index 3ad0f7078c56..c1f8269e37b2 100644 --- a/lib/ui/src/components/sidebar/HighlightStyles.tsx +++ b/lib/ui/src/components/sidebar/HighlightStyles.tsx @@ -9,7 +9,7 @@ export const HighlightStyles: FunctionComponent = ({ refId, itemId }) const background = transparentize(0.85, color.secondary); return { [`[data-ref-id="${refId}"][data-item-id="${itemId}"]:not([data-selected="true"])`]: { - [`&[data-nodetype="root"], &[data-nodetype="component"], &[data-nodetype="group"]`]: { + [`&[data-nodetype="component"], &[data-nodetype="group"]`]: { background, '&:hover, &:focus': { background }, }, diff --git a/lib/ui/src/components/sidebar/Tree.tsx b/lib/ui/src/components/sidebar/Tree.tsx index e4069bf00c2b..85c14d1343b3 100644 --- a/lib/ui/src/components/sidebar/Tree.tsx +++ b/lib/ui/src/components/sidebar/Tree.tsx @@ -2,7 +2,7 @@ import { Group, Story, StoriesHash, isRoot, isStory } from '@storybook/api'; import { styled } from '@storybook/theming'; import { Icons } from '@storybook/components'; import { transparentize } from 'polished'; -import React, { MutableRefObject, useCallback, useMemo, useRef, useEffect } from 'react'; +import React, { Fragment, MutableRefObject, useCallback, useMemo, useRef } from 'react'; import { ComponentNode, @@ -50,7 +50,7 @@ export const Action = styled.button(({ theme }) => ({ }, })); -const CollapseButton = styled.button(({ theme }) => ({ +const CollapseButton = styled.button<{ isExpanded: boolean }>(({ theme, isExpanded }) => ({ // Reset button background: 'transparent', border: 'none', @@ -67,22 +67,24 @@ const CollapseButton = styled.button(({ theme }) => ({ display: 'flex', flex: 1, - padding: 3, - paddingLeft: 1, - paddingRight: 12, + padding: '4px 12px 2px 2px', margin: 0, marginLeft: -20, overflow: 'hidden', 'span:first-of-type': { - marginTop: 5, + marginTop: 4, + marginRight: 7, + opacity: isExpanded ? 0 : 1, + transition: 'opacity 150ms', + }, + + '&:hover span:first-of-type': { + opacity: 1, }, '&:focus': { borderColor: theme.color.secondary, - 'span:first-of-type': { - borderLeftColor: theme.color.secondary, - }, }, })); @@ -154,11 +156,11 @@ const Node = React.memo( data-ref-id={refId} data-item-id={item.id} data-nodetype="root" - data-highlightable aria-expanded={isExpanded} > { event.preventDefault(); setExpanded({ ids: [item.id], value: !isExpanded }); @@ -167,18 +169,20 @@ const Node = React.memo( {item.name} - { - event.preventDefault(); - setFullyExpanded(); - }} - > - - + {isExpanded && ( + { + event.preventDefault(); + setFullyExpanded(); + }} + > + + + )} ); } @@ -213,17 +217,14 @@ const Node = React.memo( ); const Root = React.memo( - ({ item, setExpanded, isFullyExpanded, expandableDescendants, ...props }) => { - const setFullyExpanded = useCallback(() => { - setExpanded({ - ids: [!isFullyExpanded && item.id, ...expandableDescendants].filter(Boolean), - value: !isFullyExpanded, - }); - }, [setExpanded, isFullyExpanded, expandableDescendants, item.id]); + ({ setExpanded, isFullyExpanded, expandableDescendants, ...props }) => { + const setFullyExpanded = useCallback( + () => setExpanded({ ids: expandableDescendants, value: !isFullyExpanded }), + [setExpanded, isFullyExpanded, expandableDescendants] + ); return ( expanded[d]); return ( - <> + - + ); } diff --git a/lib/ui/src/components/sidebar/TreeNode.tsx b/lib/ui/src/components/sidebar/TreeNode.tsx index a5ff9eae9b57..8548bc4e5637 100644 --- a/lib/ui/src/components/sidebar/TreeNode.tsx +++ b/lib/ui/src/components/sidebar/TreeNode.tsx @@ -124,8 +124,9 @@ export const Path = styled.span(({ theme }) => ({ export const RootNode = styled.div(({ theme }) => ({ display: 'flex', alignItems: 'center', - padding: '8px 20px 4px 20px', - marginTop: 8, + padding: '0 20px', + marginTop: 14, + marginBottom: 4, fontSize: `${theme.typography.size.s1 - 1}px`, fontWeight: theme.typography.weight.black, lineHeight: '16px', From fd5ef22adb9edf9a9c1d3727d2aaf356fac07091 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 25 Feb 2021 15:46:30 +0100 Subject: [PATCH 6/9] Simplify and clean up --- examples/official-storybook/manager.js | 2 +- lib/api/src/lib/stories.ts | 6 +- lib/ui/src/components/sidebar/Tree.tsx | 118 +++++-------------- lib/ui/src/components/sidebar/useExpanded.ts | 16 +-- 4 files changed, 41 insertions(+), 101 deletions(-) diff --git a/examples/official-storybook/manager.js b/examples/official-storybook/manager.js index 0240bdbb487b..b3c7f0839ac9 100644 --- a/examples/official-storybook/manager.js +++ b/examples/official-storybook/manager.js @@ -15,5 +15,5 @@ addons.setConfig({ hidden: true, }, }, - initialRootsCollapsed: ['other'], + collapsedRoots: ['other'], }); diff --git a/lib/api/src/lib/stories.ts b/lib/api/src/lib/stories.ts index 64e1bbe72ce5..ddc2128eb041 100644 --- a/lib/api/src/lib/stories.ts +++ b/lib/api/src/lib/stories.ts @@ -19,7 +19,7 @@ export interface Root { isComponent: false; isRoot: true; isLeaf: false; - defaultCollapsed?: boolean; + startCollapsed?: boolean; } export interface Group { @@ -144,7 +144,7 @@ export const transformStoriesRawToStoriesHash = ( const storiesHashOutOfOrder = values.reduce((acc, item) => { const { kind, parameters } = item; - const { showRoots, initialRootsCollapsed = [] } = provider.getConfig(); + const { showRoots, collapsedRoots = [] } = provider.getConfig(); const setShowRoots = typeof showRoots !== 'undefined'; if (usesOldHierarchySeparator && !setShowRoots) { @@ -177,7 +177,7 @@ export const transformStoriesRawToStoriesHash = ( isComponent: false, isLeaf: false, isRoot: true, - defaultCollapsed: initialRootsCollapsed.includes(id), + startCollapsed: collapsedRoots.includes(id), }); } else { list.push({ diff --git a/lib/ui/src/components/sidebar/Tree.tsx b/lib/ui/src/components/sidebar/Tree.tsx index 85c14d1343b3..6d33a9e9de02 100644 --- a/lib/ui/src/components/sidebar/Tree.tsx +++ b/lib/ui/src/components/sidebar/Tree.tsx @@ -2,7 +2,7 @@ import { Group, Story, StoriesHash, isRoot, isStory } from '@storybook/api'; import { styled } from '@storybook/theming'; import { Icons } from '@storybook/components'; import { transparentize } from 'polished'; -import React, { Fragment, MutableRefObject, useCallback, useMemo, useRef } from 'react'; +import React, { MutableRefObject, useCallback, useMemo, useRef } from 'react'; import { ComponentNode, @@ -12,7 +12,7 @@ import { StoryNode, CollapseIcon, } from './TreeNode'; -import { useExpanded, ExpandAction } from './useExpanded'; +import { useExpanded, ExpandAction, ExpandedState } from './useExpanded'; import { Highlight, Item } from './types'; import { createId, getAncestorIds, getDescendantIds, getLink } from './utils'; @@ -238,11 +238,6 @@ const Container = styled.div<{ hasOrphans: boolean }>((props) => ({ marginBottom: 20, })); -const Section = styled.div<{ expanded: boolean }>(({ expanded }) => ({ - height: expanded ? 'auto' : 0, - overflow: expanded ? 'visible' : 'hidden', -})); - export const Tree = React.memo<{ isBrowsing: boolean; isMain: boolean; @@ -266,20 +261,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, defaultCollapsedRootIds] = useMemo( + const [rootIds, orphanIds, initialExpanded] = useMemo( () => - Object.keys(data).reduce<[string[], string[], string[]]>( + Object.keys(data).reduce<[string[], string[], ExpandedState]>( (acc, id) => { const item = data[id]; - if (isRoot(item)) { - acc[0].push(id); - if (item.defaultCollapsed) { - acc[2].push(id); - } - } else if (!item.parent) acc[1].push(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] ); @@ -350,102 +342,50 @@ export const Tree = React.memo<{ isBrowsing, // only enable keyboard shortcuts when tree is visible refId, data: collapsedData, + initialExpanded, rootIds, highlightedRef, setHighlightedItemId, selectedStoryId, onSelectStoryId, - defaultCollapsedRootIds, }); - /** - * Build the array for - * Single Story - * Root - * Story1 - * Story2 - */ - const itemsDisplayed = useMemo(() => { - return collapsedItems.reduce((acc, itemId) => { - const item = collapsedData[itemId]; - const latestRoot = acc.pop(); - if (latestRoot && isRoot(collapsedData[latestRoot.id]) && !isRoot(item)) { - acc.push({ - ...latestRoot, - children: [...latestRoot.children, itemId], - }); - } else { - // an orphan or a root, we add the latest one back - if (latestRoot) { - acc.push(latestRoot); - } - acc.push({ id: itemId, children: [] }); - } - return acc; - }, []); - }, [collapsedItems, collapsedData]); - return ( 0}> - {itemsDisplayed.map((itemDisplay) => { - const item = collapsedData[itemDisplay.id]; - - const id = createId(itemDisplay.id, refId); + {collapsedItems.map((itemId) => { + const item = collapsedData[itemId]; + const id = createId(itemId, refId); if (isRoot(item)) { const descendants = expandableDescendants[item.id]; const isFullyExpanded = descendants.every((d: string) => expanded[d]); return ( - - -
- {itemDisplay.children?.map((childId) => { - const child = collapsedData[childId] as Group | Story; - - const isDisplayed = - !child.parent || ancestry[childId].every((a: string) => expanded[a]); - return ( - childId === oid || childId.startsWith(`${oid}-`) - )} - isDisplayed={isDisplayed} - isSelected={selectedStoryId === childId} - isExpanded={!!expanded[childId]} - setExpanded={setExpanded} - onSelectStoryId={onSelectStoryId} - /> - ); - })} -
-
+ ); } - const isDisplayed = !item.parent || ancestry[item.id].every((a: string) => expanded[a]); + const isDisplayed = !item.parent || ancestry[itemId].every((a: string) => expanded[a]); return ( item.id === oid || item.id.startsWith(`${oid}-`))} + isOrphan={orphanIds.some((oid) => itemId === oid || itemId.startsWith(`${oid}-`))} isDisplayed={isDisplayed} - isSelected={selectedStoryId === item.id} - isExpanded={!!expanded[item.id]} + isSelected={selectedStoryId === itemId} + isExpanded={!!expanded[itemId]} setExpanded={setExpanded} onSelectStoryId={onSelectStoryId} /> diff --git a/lib/ui/src/components/sidebar/useExpanded.ts b/lib/ui/src/components/sidebar/useExpanded.ts index a9c5df697998..b4d6e1ed88f5 100644 --- a/lib/ui/src/components/sidebar/useExpanded.ts +++ b/lib/ui/src/components/sidebar/useExpanded.ts @@ -20,33 +20,33 @@ export interface ExpandedProps { isBrowsing: boolean; refId: string; data: StoriesHash; + initialExpanded?: ExpandedState; rootIds: string[]; highlightedRef: MutableRefObject; setHighlightedItemId: (storyId: string) => void; selectedStoryId: string | null; onSelectStoryId: (storyId: string) => void; - defaultCollapsedRootIds?: string[]; } const initializeExpanded = ({ refId, data, + initialExpanded, highlightedRef, rootIds, - defaultCollapsedRootIds, }: { refId: string; data: StoriesHash; + initialExpanded?: ExpandedState; highlightedRef: MutableRefObject; rootIds: string[]; - defaultCollapsedRootIds: string[]; }) => { const highlightedAncestors = highlightedRef.current?.refId === refId ? getAncestorIds(data, highlightedRef.current?.itemId) : []; return [...rootIds, ...highlightedAncestors].reduce( - (acc, id) => Object.assign(acc, { [id]: !defaultCollapsedRootIds.includes(id) }), + (acc, id) => Object.assign(acc, { [id]: id in initialExpanded ? initialExpanded[id] : true }), {} ); }; @@ -58,13 +58,13 @@ export const useExpanded = ({ isBrowsing, refId, data, + initialExpanded, rootIds, highlightedRef, setHighlightedItemId, selectedStoryId, onSelectStoryId, - defaultCollapsedRootIds, -}: ExpandedProps): [Record, Dispatch] => { +}: ExpandedProps): [ExpandedState, Dispatch] => { const api = useStorybookApi(); // Track the set of currently expanded nodes within this tree. @@ -76,12 +76,12 @@ export const useExpanded = ({ data: StoriesHash; highlightedRef: MutableRefObject; rootIds: string[]; - defaultCollapsedRootIds: string[]; + initialExpanded: ExpandedState; } >( (state, { ids, value }) => ids.reduce((acc, id) => Object.assign(acc, { [id]: value }), { ...state }), - { refId, data, highlightedRef, rootIds, defaultCollapsedRootIds }, + { refId, data, highlightedRef, rootIds, initialExpanded }, initializeExpanded ); From b9611f52dfd2fee1017997be98245de16181f44d Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 25 Feb 2021 15:51:10 +0100 Subject: [PATCH 7/9] Fix alignment --- lib/ui/src/components/sidebar/Tree.tsx | 7 +------ lib/ui/src/components/sidebar/TreeNode.tsx | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/ui/src/components/sidebar/Tree.tsx b/lib/ui/src/components/sidebar/Tree.tsx index 6d33a9e9de02..174a93e5b911 100644 --- a/lib/ui/src/components/sidebar/Tree.tsx +++ b/lib/ui/src/components/sidebar/Tree.tsx @@ -67,7 +67,7 @@ const CollapseButton = styled.button<{ isExpanded: boolean }>(({ theme, isExpand display: 'flex', flex: 1, - padding: '4px 12px 2px 2px', + padding: '2px 12px 2px 2px', margin: 0, marginLeft: -20, overflow: 'hidden', @@ -88,11 +88,6 @@ const CollapseButton = styled.button<{ isExpanded: boolean }>(({ theme, isExpand }, })); -type DisplayableNode = { - id: string; - children?: string[]; -}; - interface NodeProps { item: Item; refId: string; diff --git a/lib/ui/src/components/sidebar/TreeNode.tsx b/lib/ui/src/components/sidebar/TreeNode.tsx index 8548bc4e5637..71f67ebe2c8f 100644 --- a/lib/ui/src/components/sidebar/TreeNode.tsx +++ b/lib/ui/src/components/sidebar/TreeNode.tsx @@ -125,7 +125,7 @@ export const RootNode = styled.div(({ theme }) => ({ display: 'flex', alignItems: 'center', padding: '0 20px', - marginTop: 14, + marginTop: 16, marginBottom: 4, fontSize: `${theme.typography.size.s1 - 1}px`, fontWeight: theme.typography.weight.black, From 35a69009651b7cd9c9ccbe8e34ee292d3d4c9034 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 25 Feb 2021 17:01:11 +0100 Subject: [PATCH 8/9] Fix keyboard navigation --- lib/ui/src/components/sidebar/RefIndicator.tsx | 2 +- lib/ui/src/components/sidebar/Refs.tsx | 2 +- lib/ui/src/components/sidebar/Tree.tsx | 17 ++++++++++++----- lib/ui/src/components/sidebar/TreeNode.tsx | 4 +++- 4 files changed, 17 insertions(+), 8 deletions(-) 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 174a93e5b911..a71fccfa7c84 100644 --- a/lib/ui/src/components/sidebar/Tree.tsx +++ b/lib/ui/src/components/sidebar/Tree.tsx @@ -66,11 +66,13 @@ const CollapseButton = styled.button<{ isExpanded: boolean }>(({ theme, isExpand textTransform: 'inherit', display: 'flex', - flex: 1, - padding: '2px 12px 2px 2px', + flex: '0 1 auto', + padding: '3px 10px 1px 1px', margin: 0, - marginLeft: -20, + marginLeft: -19, overflow: 'hidden', + borderRadius: 26, + transition: 'color 150ms, box-shadow 150ms', 'span:first-of-type': { marginTop: 4, @@ -79,12 +81,16 @@ const CollapseButton = styled.button<{ isExpanded: boolean }>(({ theme, isExpand transition: 'opacity 150ms', }, - '&:hover span:first-of-type': { + '&:hover span:first-of-type, &:focus span:first-of-type': { opacity: 1, }, '&:focus': { - borderColor: theme.color.secondary, + boxShadow: `0 0 0 1px ${theme.color.secondary}`, + color: theme.color.secondary, + 'span:first-of-type': { + color: theme.color.secondary, + }, }, })); @@ -155,6 +161,7 @@ const Node = React.memo( > { event.preventDefault(); diff --git a/lib/ui/src/components/sidebar/TreeNode.tsx b/lib/ui/src/components/sidebar/TreeNode.tsx index 71f67ebe2c8f..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', })); @@ -124,6 +125,7 @@ export const Path = styled.span(({ theme }) => ({ export const RootNode = styled.div(({ theme }) => ({ display: 'flex', alignItems: 'center', + justifyContent: 'space-between', padding: '0 20px', marginTop: 16, marginBottom: 4, From 15577cccbf5fe753603f2cfa1325f63e29ce665c Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 25 Feb 2021 22:53:03 +0100 Subject: [PATCH 9/9] Always show arrow on roots --- lib/ui/src/components/sidebar/Tree.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/ui/src/components/sidebar/Tree.tsx b/lib/ui/src/components/sidebar/Tree.tsx index a71fccfa7c84..5156fdef0a09 100644 --- a/lib/ui/src/components/sidebar/Tree.tsx +++ b/lib/ui/src/components/sidebar/Tree.tsx @@ -50,7 +50,7 @@ export const Action = styled.button(({ theme }) => ({ }, })); -const CollapseButton = styled.button<{ isExpanded: boolean }>(({ theme, isExpanded }) => ({ +const CollapseButton = styled.button(({ theme }) => ({ // Reset button background: 'transparent', border: 'none', @@ -77,12 +77,6 @@ const CollapseButton = styled.button<{ isExpanded: boolean }>(({ theme, isExpand 'span:first-of-type': { marginTop: 4, marginRight: 7, - opacity: isExpanded ? 0 : 1, - transition: 'opacity 150ms', - }, - - '&:hover span:first-of-type, &:focus span:first-of-type': { - opacity: 1, }, '&:focus': { @@ -162,7 +156,6 @@ const Node = React.memo( { event.preventDefault(); setExpanded({ ids: [item.id], value: !isExpanded });