Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Revert "Addon-docs: Add opt-in table of contents"" #23334

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions code/addons/docs/template/stories/toc/basic.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { global as globalThis } from '@storybook/global';

export default {
component: globalThis.Components.Button,
tags: ['autodocs'],
parameters: {
chromatic: { disable: true },
docs: { toc: {} },
},
};

export const One = {
args: { label: 'One' },
};

export const Two = {
args: { label: 'Two' },
};

export const Three = {
args: { label: 'Two' },
};
14 changes: 14 additions & 0 deletions code/addons/docs/template/stories/toc/custom-selector.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { global as globalThis } from '@storybook/global';
import { One, Two, Three } from './basic.stories';

export default {
component: globalThis.Components.Button,
tags: ['autodocs'],
parameters: {
chromatic: { disable: true },
// Select all the headings in the document
docs: { toc: { headingSelector: 'h1, h2, h3' } },
},
};

export { One, Two, Three };
14 changes: 14 additions & 0 deletions code/addons/docs/template/stories/toc/custom-title.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { global as globalThis } from '@storybook/global';
import { One, Two, Three } from './basic.stories';

export default {
component: globalThis.Components.Button,
tags: ['autodocs'],
parameters: {
chromatic: { disable: true },
// Custom title label
docs: { toc: { title: 'Contents' } },
},
};

export { One, Two, Three };
14 changes: 14 additions & 0 deletions code/addons/docs/template/stories/toc/ignore-selector.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { global as globalThis } from '@storybook/global';
import { One, Two, Three } from './basic.stories';

export default {
component: globalThis.Components.Button,
tags: ['autodocs'],
parameters: {
chromatic: { disable: true },
// Skip the first story in the TOC
docs: { toc: { ignoreSelector: '#one' } },
},
};

export { One, Two, Three };
1 change: 0 additions & 1 deletion code/builders/builder-manager/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@ const starter: StarterFunction = async function* starterGeneratorFn({
}
});
router.use(`/index.html`, ({ path }, res) => {
console.log({ path });
res.status(200).send(html);
});

Expand Down
1 change: 1 addition & 0 deletions code/ui/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export const parameters = {
},
docs: {
theme: themes.light,
toc: {},
},
controls: {
presetColors: [
Expand Down
1 change: 1 addition & 0 deletions code/ui/blocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"polished": "^4.2.2",
"react-colorful": "^5.1.2",
"telejson": "^7.0.3",
"tocbot": "^4.20.1",
"ts-dedent": "^2.0.0",
"util-deprecate": "^1.0.2"
},
Expand Down
17 changes: 16 additions & 1 deletion code/ui/blocks/src/blocks/DocsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { DocsContextProps } from './DocsContext';
import { DocsContext } from './DocsContext';
import { SourceContainer } from './SourceContainer';
import { scrollToElement } from './utils';
import { TableOfContents } from '../components/TableOfContents';

const { document, window: globalWindow } = global;

Expand All @@ -22,6 +23,16 @@ export const DocsContainer: FC<PropsWithChildren<DocsContainerProps>> = ({
theme,
children,
}) => {
let toc;

try {
const meta = context.resolveOf('meta', ['meta']);
toc = meta.preparedMeta.parameters?.docs?.toc;
} catch (err) {
// No meta, falling back to project annotations
toc = context?.projectAnnotations?.parameters?.docs?.toc;
}

useEffect(() => {
let url;
try {
Expand All @@ -44,7 +55,11 @@ export const DocsContainer: FC<PropsWithChildren<DocsContainerProps>> = ({
<DocsContext.Provider value={context}>
<SourceContainer channel={context.channel}>
<ThemeProvider theme={ensureTheme(theme)}>
<DocsPageWrapper>{children}</DocsPageWrapper>
<DocsPageWrapper
toc={toc ? <TableOfContents className="sbdocs sbdocs-toc--custom" {...toc} /> : null}
>
{children}
</DocsPageWrapper>
</ThemeProvider>
</SourceContainer>
</DocsContext.Provider>
Expand Down
11 changes: 7 additions & 4 deletions code/ui/blocks/src/components/DocsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { transparentize } from 'polished';
import { withReset } from '@storybook/components';
import type { CSSObject } from '@storybook/theming';
import { styled } from '@storybook/theming';
import { transparentize } from 'polished';
import type { FC } from 'react';
import React from 'react';

/**
* This selector styles all raw elements inside the DocsPage like this example with a `<div/>`:
Expand Down Expand Up @@ -429,16 +429,19 @@ export const DocsWrapper = styled.div(({ theme }) => ({
padding: '4rem 20px',
minHeight: '100vh',
boxSizing: 'border-box',
gap: '3rem',

[`@media (min-width: ${breakpoint}px)`]: {},
}));

interface DocsPageWrapperProps {
children?: React.ReactNode;
toc?: React.ReactNode;
}

export const DocsPageWrapper: FC<DocsPageWrapperProps> = ({ children }) => (
export const DocsPageWrapper: FC<DocsPageWrapperProps> = ({ children, toc }) => (
<DocsWrapper className="sbdocs sbdocs-wrapper">
<DocsContent className="sbdocs sbdocs-content">{children}</DocsContent>
{toc}
</DocsWrapper>
);
181 changes: 181 additions & 0 deletions code/ui/blocks/src/components/TableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { useEffect } from 'react';
import type { FC, ReactElement } from 'react';
import { styled } from '@storybook/theming';
import tocbot from 'tocbot';

export interface TocParameters {
/** CSS selector for the container to search for headings. */
contentsSelector?: string;

/**
* When true, hide the TOC. We still show the empty container
* (as opposed to showing nothing at all) because it affects the
* page layout and we want to preserve the layout across pages.
*/
disable?: boolean;

/** CSS selector to match headings to list in the TOC. */
headingSelector?: string;

/** Headings that match the ignoreSelector will be skipped. */
ignoreSelector?: string;

/** Custom title ReactElement or string to display above the TOC. */
title?: ReactElement | string | null;

/**
* TocBot options, not guaranteed to be available in future versions.
* @see tocbot docs {@link https://tscanlin.github.io/tocbot/#usage}
*/
unsafeTocbotOptions?: tocbot.IStaticOptions;
}

const Wrapper = styled.div(({ theme }) => ({
width: '10rem',

'@media (max-width: 768px)': {
display: 'none',
},
}));

const Content = styled.div(({ theme }) => ({
position: 'fixed',
top: 0,
width: '10rem',
paddingTop: '4rem',

fontFamily: theme.typography.fonts.base,
fontSize: theme.typography.size.s2,

WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
WebkitTapHighlightColor: 'rgba(0, 0, 0, 0)',
WebkitOverflowScrolling: 'touch',

'& *': {
boxSizing: 'border-box',
},

'& > .toc-wrapper > .toc-list': {
paddingLeft: 0,
borderLeft: `solid 2px ${theme.color.mediumlight}`,

'.toc-list': {
paddingLeft: 0,
borderLeft: `solid 2px ${theme.color.mediumlight}`,

'.toc-list': {
paddingLeft: 0,
borderLeft: `solid 2px ${theme.color.mediumlight}`,
},
},
},
'& .toc-list-item': {
position: 'relative',
listStyleType: 'none',
marginLeft: 20,
paddingTop: 3,
paddingBottom: 3,
},
'& .toc-list-item::before': {
content: '""',
position: 'absolute',
height: '100%',
top: 0,
left: 0,
transform: `translateX(calc(-2px - 20px))`,
borderLeft: `solid 2px ${theme.color.mediumdark}`,
opacity: 0,
transition: 'opacity 0.2s',
},
'& .toc-list-item.is-active-li::before': {
opacity: 1,
},
'& .toc-list-item > a': {
color: theme.color.defaultText,
textDecoration: 'none',
},
'& .toc-list-item.is-active-li > a': {
fontWeight: 600,
color: theme.color.secondary,
textDecoration: 'none',
},
}));

const Heading = styled.p(({ theme }) => ({
fontWeight: 600,
fontSize: '0.875em',
color: theme.textColor,
textTransform: 'uppercase',
marginBottom: 10,
}));

type TableOfContentsProps = React.PropsWithChildren<
TocParameters & {
className?: string;
}
>;

const OptionalTitle: FC<{ title: TableOfContentsProps['title'] }> = ({ title }) => {
if (title === null) {
return null;
}
if (typeof title === 'string') {
return <Heading>{title}</Heading>;
}
return title;
};

export const TableOfContents = ({
title,
disable,
headingSelector,
contentsSelector,
ignoreSelector,
unsafeTocbotOptions,
}: TableOfContentsProps) => {
useEffect(() => {
const configuration = {
tocSelector: '.toc-wrapper',
contentSelector: contentsSelector ?? '.sbdocs-content',
headingSelector: headingSelector ?? 'h3',
ignoreSelector: ignoreSelector ?? '.skip-toc',
headingsOffset: 40,
scrollSmoothOffset: -40,
/**
* Ignore headings that did not
* come from the main markdown code.
*/
// ignoreSelector: ':not(.sbdocs), .hide-from-toc',
orderedList: false,
/**
* Prevent default linking behavior,
* leaving only the smooth scrolling.
*/
onClick: () => false,
...unsafeTocbotOptions,
};

/**
* Wait for the DOM to be ready.
*/
const timeout = setTimeout(() => tocbot.init(configuration), 100);
return () => {
clearTimeout(timeout);
tocbot.destroy();
};
}, [disable]);

return (
<>
<Wrapper>
{!disable ? (
<Content>
<OptionalTitle title={title || null} />
<div className="toc-wrapper" />
</Content>
) : null}
</Wrapper>
</>
);
};
8 changes: 8 additions & 0 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5802,6 +5802,7 @@ __metadata:
polished: ^4.2.2
react-colorful: ^5.1.2
telejson: ^7.0.3
tocbot: ^4.20.1
ts-dedent: ^2.0.0
util-deprecate: ^1.0.2
peerDependencies:
Expand Down Expand Up @@ -29218,6 +29219,13 @@ __metadata:
languageName: node
linkType: hard

"tocbot@npm:^4.20.1":
version: 4.21.0
resolution: "tocbot@npm:4.21.0"
checksum: 877d99df40c07ec5e5c2259b820be9c8af9a9f52d582a61b7bed3d43daff820f23031bc613a5cc3bb14ecc34b79c1a45349dcbae8f3a79de7ecc127f366ed3c6
languageName: node
linkType: hard

"toggle-selection@npm:^1.0.6":
version: 1.0.6
resolution: "toggle-selection@npm:1.0.6"
Expand Down