feat(v2): add ability to hide doc sidebar (#3615)

* feat(v2): add ability to hide doc sidebar

* Use relative imports
This commit is contained in:
Alexey Pyltsyn 2020-10-22 12:23:15 +03:00 committed by GitHub
parent 0ec5d533d6
commit 14cdd72ae4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 293 additions and 122 deletions

View file

@ -78,110 +78,108 @@ function DocItem(props: Props): JSX.Element {
{permalink && <meta property="og:url" content={siteUrl + permalink} />} {permalink && <meta property="og:url" content={siteUrl + permalink} />}
{permalink && <link rel="canonical" href={siteUrl + permalink} />} {permalink && <link rel="canonical" href={siteUrl + permalink} />}
</Head> </Head>
<div
className={clsx('container padding-vert--lg', styles.docItemWrapper)}> <div className="row">
<div className="row"> <div
<div className={clsx('col', {
className={clsx('col', { [styles.docItemCol]: !hideTableOfContents,
[styles.docItemCol]: !hideTableOfContents, })}>
})}> <DocVersionSuggestions />
<DocVersionSuggestions /> <div className={styles.docItemContainer}>
<div className={styles.docItemContainer}> <article>
<article> {showVersionBadge && (
{showVersionBadge && ( <div>
<div> <span className="badge badge--secondary">
<span className="badge badge--secondary"> Version: {version.label}
Version: {version.label} </span>
</span>
</div>
)}
{!hideTitle && (
<header>
<h1 className={styles.docTitle}>{title}</h1>
</header>
)}
<div className="markdown">
<DocContent />
</div>
</article>
{(editUrl || lastUpdatedAt || lastUpdatedBy) && (
<div className="margin-vert--xl">
<div className="row">
<div className="col">
{editUrl && (
<a
href={editUrl}
target="_blank"
rel="noreferrer noopener">
<svg
fill="currentColor"
height="1.2em"
width="1.2em"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 40 40"
style={{
marginRight: '0.3em',
verticalAlign: 'sub',
}}>
<g>
<path d="m34.5 11.7l-3 3.1-6.3-6.3 3.1-3q0.5-0.5 1.2-0.5t1.1 0.5l3.9 3.9q0.5 0.4 0.5 1.1t-0.5 1.2z m-29.5 17.1l18.4-18.5 6.3 6.3-18.4 18.4h-6.3v-6.2z" />
</g>
</svg>
Edit this page
</a>
)}
</div>
{(lastUpdatedAt || lastUpdatedBy) && (
<div className="col text--right">
<em>
<small>
Last updated{' '}
{lastUpdatedAt && (
<>
on{' '}
<time
dateTime={new Date(
lastUpdatedAt * 1000,
).toISOString()}
className={styles.docLastUpdatedAt}>
{new Date(
lastUpdatedAt * 1000,
).toLocaleDateString()}
</time>
{lastUpdatedBy && ' '}
</>
)}
{lastUpdatedBy && (
<>
by <strong>{lastUpdatedBy}</strong>
</>
)}
{process.env.NODE_ENV === 'development' && (
<div>
<small>
{' '}
(Simulated during dev for better perf)
</small>
</div>
)}
</small>
</em>
</div>
)}
</div>
</div> </div>
)} )}
<div className="margin-vert--lg"> {!hideTitle && (
<DocPaginator metadata={metadata} /> <header>
<h1 className={styles.docTitle}>{title}</h1>
</header>
)}
<div className="markdown">
<DocContent />
</div> </div>
</article>
{(editUrl || lastUpdatedAt || lastUpdatedBy) && (
<div className="margin-vert--xl">
<div className="row">
<div className="col">
{editUrl && (
<a
href={editUrl}
target="_blank"
rel="noreferrer noopener">
<svg
fill="currentColor"
height="1.2em"
width="1.2em"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 40 40"
style={{
marginRight: '0.3em',
verticalAlign: 'sub',
}}>
<g>
<path d="m34.5 11.7l-3 3.1-6.3-6.3 3.1-3q0.5-0.5 1.2-0.5t1.1 0.5l3.9 3.9q0.5 0.4 0.5 1.1t-0.5 1.2z m-29.5 17.1l18.4-18.5 6.3 6.3-18.4 18.4h-6.3v-6.2z" />
</g>
</svg>
Edit this page
</a>
)}
</div>
{(lastUpdatedAt || lastUpdatedBy) && (
<div className="col text--right">
<em>
<small>
Last updated{' '}
{lastUpdatedAt && (
<>
on{' '}
<time
dateTime={new Date(
lastUpdatedAt * 1000,
).toISOString()}
className={styles.docLastUpdatedAt}>
{new Date(
lastUpdatedAt * 1000,
).toLocaleDateString()}
</time>
{lastUpdatedBy && ' '}
</>
)}
{lastUpdatedBy && (
<>
by <strong>{lastUpdatedBy}</strong>
</>
)}
{process.env.NODE_ENV === 'development' && (
<div>
<small>
{' '}
(Simulated during dev for better perf)
</small>
</div>
)}
</small>
</em>
</div>
)}
</div>
</div>
)}
<div className="margin-vert--lg">
<DocPaginator metadata={metadata} />
</div> </div>
</div> </div>
{!hideTableOfContents && DocContent.rightToc && (
<div className="col col--3">
<TOC headings={DocContent.rightToc} />
</div>
)}
</div> </div>
{!hideTableOfContents && DocContent.rightToc && (
<div className="col col--3">
<TOC headings={DocContent.rightToc} />
</div>
)}
</div> </div>
</> </>
); );

View file

@ -21,14 +21,6 @@
} }
} }
@media (min-width: 997px) and (max-width: 1320px) {
.docItemWrapper {
max-width: calc(
var(--ifm-container-width) - 300px - var(--ifm-spacing-horizontal) * 2
);
}
}
@media only screen and (max-width: 996px) { @media only screen and (max-width: 996px) {
.docItemContainer { .docItemContainer {
padding: 0 0.3rem; padding: 0 0.3rem;

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import React, {ReactNode} from 'react'; import React, {ReactNode, useState, useCallback} from 'react';
import {MDXProvider} from '@mdx-js/react'; import {MDXProvider} from '@mdx-js/react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
@ -19,6 +19,7 @@ import type {DocumentRoute} from '@theme/DocItem';
import type {Props} from '@theme/DocPage'; import type {Props} from '@theme/DocPage';
import {matchPath} from '@docusaurus/router'; import {matchPath} from '@docusaurus/router';
import clsx from 'clsx';
import styles from './styles.module.css'; import styles from './styles.module.css';
import {docVersionSearchTag} from '../../utils/searchUtils'; import {docVersionSearchTag} from '../../utils/searchUtils';
@ -37,6 +38,17 @@ function DocPageContent({
const {pluginId, permalinkToSidebar, docsSidebars, version} = versionMetadata; const {pluginId, permalinkToSidebar, docsSidebars, version} = versionMetadata;
const sidebarName = permalinkToSidebar[currentDocRoute.path]; const sidebarName = permalinkToSidebar[currentDocRoute.path];
const sidebar = docsSidebars[sidebarName]; const sidebar = docsSidebars[sidebarName];
const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false);
const [hiddenSidebar, setHiddenSidebar] = useState(false);
const toggleSidebar = useCallback(() => {
if (hiddenSidebar) {
setHiddenSidebar(false);
}
setHiddenSidebarContainer(!hiddenSidebarContainer);
}, [hiddenSidebar]);
return ( return (
<Layout <Layout
key={isClient} key={isClient}
@ -46,7 +58,22 @@ function DocPageContent({
}}> }}>
<div className={styles.docPage}> <div className={styles.docPage}>
{sidebar && ( {sidebar && (
<div className={styles.docSidebarContainer} role="complementary"> <div
className={clsx(styles.docSidebarContainer, {
[styles.docSidebarContainerHidden]: hiddenSidebarContainer,
})}
onTransitionEnd={(e) => {
if (
!e.currentTarget.classList.contains(styles.docSidebarContainer)
) {
return;
}
if (hiddenSidebarContainer) {
setHiddenSidebar(true);
}
}}
role="complementary">
<DocSidebar <DocSidebar
key={ key={
// Reset sidebar state on sidebar changes // Reset sidebar state on sidebar changes
@ -58,11 +85,34 @@ function DocPageContent({
sidebarCollapsible={ sidebarCollapsible={
siteConfig.themeConfig?.sidebarCollapsible ?? true siteConfig.themeConfig?.sidebarCollapsible ?? true
} }
onCollapse={toggleSidebar}
isHidden={hiddenSidebar}
/> />
{hiddenSidebar && (
<div
className={styles.collapsedDocSidebar}
title="Expand sidebar"
aria-label="Expand sidebar"
tabIndex={0}
role="button"
onKeyDown={toggleSidebar}
onClick={toggleSidebar}
/>
)}
</div> </div>
)} )}
<main className={styles.docMainContainer}> <main className={styles.docMainContainer}>
<MDXProvider components={MDXComponents}>{children}</MDXProvider> <div
className={clsx(
'container padding-vert--lg',
styles.docItemWrapper,
{
[styles.docItemWrapperEnhanced]: hiddenSidebarContainer,
},
)}>
<MDXProvider components={MDXComponents}>{children}</MDXProvider>
</div>
</main> </main>
</div> </div>
</Layout> </Layout>

View file

@ -5,20 +5,64 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
.docPage { :root {
display: flex; --doc-sidebar-width: 300px;
} }
.docSidebarContainer { @media (min-width: 997px) {
border-right: 1px solid var(--ifm-toc-border-color); .docPage {
box-sizing: border-box; display: flex;
width: 300px; }
position: relative;
margin-top: calc(-1 * var(--ifm-navbar-height));
}
.docMainContainer { .docSidebarContainer {
flex-grow: 1; width: var(--doc-sidebar-width);
margin-top: calc(-1 * var(--ifm-navbar-height));
border-right: 1px solid var(--ifm-toc-border-color);
will-change: width;
transition: width var(--ifm-transition-fast) ease;
clip-path: inset(0);
}
.docSidebarContainerHidden {
width: 30px;
cursor: e-resize;
}
.collapsedDocSidebar {
position: sticky;
top: 0;
height: 100%;
max-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
transition: background-color var(--ifm-transition-fast) ease;
}
.collapsedDocSidebar:hover,
.collapsedDocSidebar:focus {
background-color: var(--ifm-color-emphasis-200);
}
html[data-theme='dark'] .collapsedDocSidebar:hover,
html[data-theme='dark'] .collapsedDocSidebar:focus {
background-color: var(--collapse-button-bg-color-dark);
}
.collapsedDocSidebar:before {
content: '';
background-image: url('~!file-loader!../icons/arrow.svg');
width: 20px;
height: 20px;
}
.docMainContainer {
flex-grow: 1;
}
.docItemWrapperEnhanced {
max-width: calc(var(--ifm-container-width) + var(--doc-sidebar-width));
}
} }
@media (max-width: 996px) { @media (max-width: 996px) {
@ -30,3 +74,18 @@
margin-top: 0; margin-top: 0;
} }
} }
@media (min-width: 997px) and (max-width: 1320px) {
.docItemWrapper {
max-width: calc(
var(--ifm-container-width) - var(--doc-sidebar-width) -
var(--ifm-spacing-horizontal) * 2
);
}
.docItemWrapperEnhanced {
max-width: calc(
var(--ifm-container-width) - var(--ifm-spacing-horizontal) * 2
);
}
}

View file

@ -189,10 +189,13 @@ function DocSidebar({
path, path,
sidebar, sidebar,
sidebarCollapsible = true, sidebarCollapsible = true,
onCollapse,
isHidden,
}: Props): JSX.Element | null { }: Props): JSX.Element | null {
const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false); const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false);
const { const {
navbar: {title, hideOnScroll}, navbar: {title, hideOnScroll},
hideableSidebar,
} = useThemeConfig(); } = useThemeConfig();
const {isClient} = useDocusaurusContext(); const {isClient} = useDocusaurusContext();
const {logoLink, logoLinkProps, logoImageUrl, logoAlt} = useLogo(); const {logoLink, logoLinkProps, logoImageUrl, logoAlt} = useLogo();
@ -212,6 +215,7 @@ function DocSidebar({
<div <div
className={clsx(styles.sidebar, { className={clsx(styles.sidebar, {
[styles.sidebarWithHideableNavbar]: hideOnScroll, [styles.sidebarWithHideableNavbar]: hideOnScroll,
[styles.sidebarHidden]: isHidden,
})}> })}>
{hideOnScroll && ( {hideOnScroll && (
<Link <Link
@ -283,6 +287,19 @@ function DocSidebar({
))} ))}
</ul> </ul>
</div> </div>
{hideableSidebar && (
<button
type="button"
title="Collapse sidebar"
aria-label="Collapse sidebar"
className={clsx(
'button button--secondary button--outline',
styles.collapseSidebarButton,
)}
onClick={onCollapse}
/>
)}
</div> </div>
); );
} }

View file

@ -5,21 +5,32 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
:root {
--collapse-button-bg-color-dark: #2e333a;
}
@media (min-width: 997px) { @media (min-width: 997px) {
.sidebar { .sidebar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
overflow-y: auto;
position: sticky; position: sticky;
top: 0; top: 0;
padding-top: var(--ifm-navbar-height); padding-top: var(--ifm-navbar-height);
width: var(--doc-sidebar-width);
transition: opacity 50ms ease;
} }
.sidebarWithHideableNavbar { .sidebarWithHideableNavbar {
padding-top: 0; padding-top: 0;
} }
.sidebarHidden {
opacity: 0;
height: 0;
overflow: hidden;
}
.sidebarLogo { .sidebarLogo {
display: flex !important; display: flex !important;
align-items: center; align-items: center;
@ -69,9 +80,32 @@
.menuWithAnnouncementBar { .menuWithAnnouncementBar {
margin-bottom: var(--docusaurus-announcement-bar-height); margin-bottom: var(--docusaurus-announcement-bar-height);
} }
.collapseSidebarButton {
display: block !important;
background: url('~!file-loader!../icons/arrow.svg') no-repeat center center;
background-color: var(--ifm-button-background-color);
height: 40px;
position: sticky;
bottom: 0;
border-radius: 0;
transform: rotate(180deg);
}
html[data-theme='dark'] .collapseSidebarButton {
background-color: var(--collapse-button-bg-color-dark);
border: none;
border-left: 1px solid var(--ifm-toc-border-color);
}
html[data-theme='dark'] .collapseSidebarButton:hover,
html[data-theme='dark'] .collapseSidebarButton:focus {
background-color: var(--ifm-color-emphasis-200);
}
} }
.sidebarLogo { .sidebarLogo,
.collapseSidebarButton {
display: none; display: none;
} }

View file

@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'><g fill='#7a7a7a'><path d='M9.992 10.023c0 .2-.062.399-.172.547l-4.996 7.492a.982.982 0 01-.828.454H1c-.55 0-1-.453-1-1 0-.2.059-.403.168-.551l4.629-6.942L.168 3.078A.939.939 0 010 2.528c0-.548.45-.997 1-.997h2.996c.352 0 .649.18.828.45L9.82 9.472c.11.148.172.347.172.55zm0 0'/><path d='M19.98 10.023c0 .2-.058.399-.168.547l-4.996 7.492a.987.987 0 01-.828.454h-3c-.547 0-.996-.453-.996-1 0-.2.059-.403.168-.551l4.625-6.942-4.625-6.945a.939.939 0 01-.168-.55 1 1 0 01.996-.997h3c.348 0 .649.18.828.45l4.996 7.492c.11.148.168.347.168.55zm0 0'/></g></svg>

After

Width:  |  Height:  |  Size: 615 B

View file

@ -79,6 +79,8 @@ declare module '@theme/DocSidebar' {
readonly path: string; readonly path: string;
readonly sidebar: readonly PropSidebarItem[]; readonly sidebar: readonly PropSidebarItem[];
readonly sidebarCollapsible?: boolean; readonly sidebarCollapsible?: boolean;
readonly onCollapse: () => void;
readonly isHidden: boolean;
}; };
const DocSidebar: (props: Props) => JSX.Element; const DocSidebar: (props: Props) => JSX.Element;

View file

@ -23,6 +23,7 @@ export type ThemeConfig = {
announcementBar: any; announcementBar: any;
prism: any; prism: any;
footer: any; footer: any;
hideableSidebar: any;
}; };
export default function useThemeConfig(): ThemeConfig { export default function useThemeConfig(): ThemeConfig {

View file

@ -40,6 +40,7 @@ const DEFAULT_CONFIG = {
hideOnScroll: false, hideOnScroll: false,
items: [], items: [],
}, },
hideableSidebar: false,
}; };
exports.DEFAULT_CONFIG = DEFAULT_CONFIG; exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
@ -283,6 +284,7 @@ const ThemeConfigSchema = Joi.object({
}) })
.default(DEFAULT_CONFIG.prism) .default(DEFAULT_CONFIG.prism)
.unknown(), .unknown(),
hideableSidebar: Joi.bool().default(DEFAULT_CONFIG.hideableSidebar),
}); });
exports.ThemeConfigSchema = ThemeConfigSchema; exports.ThemeConfigSchema = ThemeConfigSchema;

View file

@ -82,6 +82,20 @@ module.exports = {
}; };
``` ```
### Hideable sidebar
Using the enabled `themeConfig.hideableSidebar` option, you can make the entire sidebar hided, allowing you to better focus your users on the content. This is especially useful when content consumption on medium screens (e.g. on tablets).
```js {4} title="docusaurus.config.js"
module.exports = {
// ...
themeConfig: {
hideableSidebar: true,
// ...
},
};
```
### Sidebar object ### Sidebar object
A sidebar object is defined like this: A sidebar object is defined like this:

View file

@ -229,6 +229,7 @@ module.exports = {
], ],
], ],
themeConfig: { themeConfig: {
hideableSidebar: true,
colorMode: { colorMode: {
defaultMode: 'light', defaultMode: 'light',
disableSwitch: false, disableSwitch: false,