Skip to content

Commit c1e25cf

Browse files
Barthélémy Ledouxsapegin
Barthélémy Ledoux
authored andcommitted
New: Collapsible sections (styleguidist#1487)
Closes styleguidist#1436 New config option `tocMode` to allow collapsible table of contents sections. Possible values are: - `collapse`: All sections are collapsed by default - `expand`: Sections cannot be collapsed in the Table Of Contents
1 parent 1deb547 commit c1e25cf

23 files changed

+443
-113
lines changed

docs/Configuration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ Type: `String` or `Array`, optional
5858

5959
Your application static assets folder will be accessible as `/` in the style guide dev server.
6060

61+
#### `tocMode`
62+
63+
Type: `String` default: `expand`
64+
65+
Defines if the table of contents sections will behave like an accordion:
66+
67+
- `collapse`: All sections are collapsed by default
68+
- `expand`: Sections cannot be collapsed in the Table Of Contents
69+
70+
Collapse the sections created in the sidebar to reduce the height of the sidebar. This can be useful in large codebases with lots of components to avoid having to scroll too far.
71+
6172
#### `compilerConfig`
6273

6374
Type: `Object`, default:

examples/sections/docs/Components.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ Ut veniam sit pariatur deserunt non officia. Esse commodo proident quis culpa es
44

55
List of components:
66

7-
- [Buttons](#/Components?id=buttons)
8-
- [Fields](#/Components?id=fields)
9-
- [Others](#/Components?id=others)
7+
- [Buttons](#/Components?id=section-buttons)
8+
- [Fields](#/Components?id=section-fields)
9+
- [Others](#/Components?id=section-others)

examples/sections/styleguide.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const path = require('path');
33
module.exports = {
44
title: 'React Style Guide Example',
55
pagePerSection: true,
6+
// tocMode: 'collapse',
67
sections: [
78
{
89
name: 'Documentation',

src/client/rsg-components/ComponentsList/ComponentsList.spec.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Context from '../Context';
77
const context = {
88
config: {
99
pagePerSection: true,
10+
tocMode: 'collapse',
1011
},
1112
};
1213

@@ -159,3 +160,72 @@ it('should ignore items without visibleName', () => {
159160
'http://localhost/#button',
160161
]);
161162
});
163+
164+
it('should show content of items that are open and not what is closed', () => {
165+
const components = [
166+
{
167+
visibleName: 'Button',
168+
name: 'Button',
169+
slug: 'button',
170+
content: <div data-testid="content">Content for Button</div>,
171+
},
172+
{
173+
visibleName: 'Input',
174+
name: 'Input',
175+
slug: 'input',
176+
content: <div data-testid="content">Content for Input</div>,
177+
},
178+
];
179+
180+
const { getAllByTestId, getByText } = render(
181+
<Provider>
182+
<ComponentsList
183+
items={components}
184+
useRouterLinks
185+
hashPath={['Components']}
186+
useHashId={false}
187+
/>
188+
</Provider>
189+
);
190+
191+
getByText('Button').click();
192+
193+
expect(
194+
Array.from(getAllByTestId('content')).map(node => (node as HTMLDivElement).innerHTML)
195+
).toEqual(['Content for Button']);
196+
});
197+
198+
it('should show content of initialOpen items even if they are not active', () => {
199+
const components = [
200+
{
201+
visibleName: 'Button',
202+
name: 'Button',
203+
slug: 'button',
204+
content: <div data-testid="content">Content for Button</div>,
205+
},
206+
{
207+
visibleName: 'Input',
208+
name: 'Input',
209+
slug: 'input',
210+
content: <div data-testid="content">Content for Input</div>,
211+
initialOpen: true,
212+
},
213+
];
214+
215+
const { getAllByTestId, getByText } = render(
216+
<Provider>
217+
<ComponentsList
218+
items={components}
219+
useRouterLinks
220+
hashPath={['Components']}
221+
useHashId={false}
222+
/>
223+
</Provider>
224+
);
225+
226+
getByText('Button').click();
227+
228+
expect(
229+
Array.from(getAllByTestId('content')).map(node => (node as HTMLDivElement).innerHTML)
230+
).toEqual(['Content for Button', 'Content for Input']);
231+
});

src/client/rsg-components/ComponentsList/ComponentsList.tsx

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import ComponentsListRenderer from 'rsg-components/ComponentsList/ComponentsList
44
import getUrl from '../../utils/getUrl';
55

66
interface ComponentsListProps {
7-
items: Rsg.Component[];
7+
items: Rsg.TOCItem[];
88
hashPath?: string[];
99
useRouterLinks?: boolean;
1010
useHashId?: boolean;
@@ -16,20 +16,26 @@ const ComponentsList: React.FunctionComponent<ComponentsListProps> = ({
1616
useHashId,
1717
hashPath,
1818
}) => {
19-
const mappedItems = items.map(item => ({
20-
...item,
21-
shouldOpenInNewTab: !!item.href,
22-
href: item.href
23-
? item.href
24-
: getUrl({
25-
name: item.name,
26-
slug: item.slug,
27-
anchor: !useRouterLinks,
28-
hashPath: useRouterLinks ? hashPath : false,
29-
id: useRouterLinks ? useHashId : false,
30-
}),
31-
}));
32-
return <ComponentsListRenderer items={mappedItems} />;
19+
const mappedItems = items
20+
.map(item => {
21+
const href = item.href
22+
? item.href
23+
: getUrl({
24+
name: item.name,
25+
slug: item.slug,
26+
anchor: !useRouterLinks,
27+
hashPath: useRouterLinks ? hashPath : false,
28+
id: useRouterLinks ? useHashId : false,
29+
});
30+
31+
return {
32+
...item,
33+
href,
34+
};
35+
})
36+
.filter(item => item.visibleName);
37+
38+
return mappedItems.length > 0 ? <ComponentsListRenderer items={mappedItems} /> : null;
3339
};
3440

3541
ComponentsList.propTypes = {

src/client/rsg-components/ComponentsList/ComponentsListRenderer.tsx

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import cx from 'clsx';
44
import Link from 'rsg-components/Link';
55
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
66
import { useStyleGuideContext } from 'rsg-components/Context';
7-
import { getHash } from '../../utils/handleHash';
87

98
const styles = ({ color, fontFamily, fontSize, space, mq }: Rsg.Theme) => ({
109
list: {
@@ -39,50 +38,55 @@ const styles = ({ color, fontFamily, fontSize, space, mq }: Rsg.Theme) => ({
3938
});
4039

4140
interface ComponentsListRendererProps extends JssInjectedProps {
42-
items: Rsg.Component[];
41+
items: Rsg.TOCItem[];
4342
}
4443

4544
export const ComponentsListRenderer: React.FunctionComponent<ComponentsListRendererProps> = ({
4645
classes,
4746
items,
47+
}) => {
48+
return (
49+
<ul className={classes.list}>
50+
{items.map(item => (
51+
<ComponentsListSectionRenderer key={item.slug} classes={classes} {...item} />
52+
))}
53+
</ul>
54+
);
55+
};
56+
57+
const ComponentsListSectionRenderer: React.FunctionComponent<Rsg.TOCItem & JssInjectedProps> = ({
58+
classes,
59+
heading,
60+
visibleName,
61+
href,
62+
content,
63+
shouldOpenInNewTab,
64+
selected,
65+
initialOpen,
4866
}) => {
4967
const {
50-
config: { pagePerSection },
68+
config: { tocMode },
5169
} = useStyleGuideContext();
5270

53-
const visibleItems = items.filter(item => item.visibleName);
54-
55-
if (!visibleItems.length) {
56-
return null;
57-
}
58-
59-
// Match selected component in both basic routing and pagePerSection routing.
60-
const { hash, pathname } = window.location;
61-
const windowHash = pathname + (pagePerSection ? hash : getHash(hash));
71+
const [open, setOpen] = tocMode !== 'collapse' ? [true, () => {}] : React.useState(!!initialOpen);
6272
return (
63-
<ul className={classes.list}>
64-
{visibleItems.map(({ heading, visibleName, href, content, shouldOpenInNewTab }) => {
65-
const isItemSelected = windowHash === href;
66-
return (
67-
<li
68-
className={cx(classes.item, {
69-
[classes.isChild]: (!content || !content.props.items.length) && !shouldOpenInNewTab,
70-
[classes.isSelected]: isItemSelected,
71-
})}
72-
key={href}
73-
>
74-
<Link
75-
className={cx(heading && classes.heading)}
76-
href={href}
77-
target={shouldOpenInNewTab ? '_blank' : undefined}
78-
>
79-
{visibleName}
80-
</Link>
81-
{content}
82-
</li>
83-
);
73+
<li
74+
className={cx(classes.item, {
75+
[classes.isChild]: !content && !shouldOpenInNewTab,
76+
[classes.isSelected]: selected,
8477
})}
85-
</ul>
78+
key={href}
79+
>
80+
<Link
81+
className={cx(heading && classes.heading)}
82+
href={href}
83+
onClick={() => setOpen(!open)}
84+
target={shouldOpenInNewTab ? '_blank' : undefined}
85+
>
86+
{visibleName}
87+
</Link>
88+
{open ? content : null}
89+
</li>
8690
);
8791
};
8892

src/client/rsg-components/Context/Context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22

33
const StyleGuideContext = React.createContext<StyleGuideContextContents>({
44
codeRevision: 0,
5-
config: {} as Rsg.ProcessedStyleguidistConfig,
5+
config: {} as Rsg.SanitizedStyleguidistConfig,
66
slots: {},
77
displayMode: 'collapse',
88
});
@@ -16,7 +16,7 @@ export interface SlotObject {
1616

1717
export interface StyleGuideContextContents {
1818
codeRevision: number;
19-
config: Rsg.ProcessedStyleguidistConfig;
19+
config: Rsg.SanitizedStyleguidistConfig;
2020
slots: Record<string, (SlotObject | React.FunctionComponent<any>)[]>;
2121
displayMode: string;
2222
}

src/client/rsg-components/Link/LinkRenderer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface LinkProps extends JssInjectedProps {
2323
className?: string;
2424
href?: string;
2525
target?: string;
26+
onClick?: () => void;
2627
}
2728

2829
export const LinkRenderer: React.FunctionComponent<LinkProps> = ({

src/client/rsg-components/StyleGuide/StyleGuide.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,11 @@ export default class StyleGuide extends Component<StyleGuideProps, StyleGuideSta
112112
homepageUrl={HOMEPAGE}
113113
toc={
114114
allSections ? (
115-
<TableOfContents sections={allSections} useRouterLinks={pagePerSection} />
115+
<TableOfContents
116+
sections={allSections}
117+
useRouterLinks={pagePerSection}
118+
tocMode={config.tocMode}
119+
/>
116120
) : null
117121
}
118122
hasSidebar={hasSidebar(displayMode, config.showSidebar)}

0 commit comments

Comments
 (0)