mirror of
https://github.com/facebook/docusaurus.git
synced 2025-12-26 01:33:02 +00:00
feat(v2): persistent & responsive dark mode toggle (#1521)
This commit is contained in:
parent
f5a8caf34d
commit
9bb6ba113d
|
|
@ -9,7 +9,8 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"docsearch.js": "^2.5.2"
|
||||
"docsearch.js": "^2.5.2",
|
||||
"react-toggle": "^4.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@docusaurus/core": "^2.0.0",
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import React, {useCallback, useState, useEffect} from 'react';
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import Link from '@docusaurus/Link';
|
||||
import Head from '@docusaurus/Head';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import withBaseUrl from '@docusaurus/withBaseUrl';
|
||||
|
||||
|
|
@ -15,6 +17,8 @@ import SearchBar from '@theme/SearchBar';
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import './styles.css';
|
||||
|
||||
function NavLink(props) {
|
||||
return (
|
||||
<Link
|
||||
|
|
@ -38,6 +42,11 @@ function NavLink(props) {
|
|||
function Navbar() {
|
||||
const context = useDocusaurusContext();
|
||||
const [sidebarShown, setSidebarShown] = useState(false);
|
||||
const currentTheme =
|
||||
typeof document !== 'undefined'
|
||||
? document.querySelector('html').getAttribute('data-theme')
|
||||
: '';
|
||||
const [theme, setTheme] = useState(currentTheme);
|
||||
const {siteConfig = {}} = context;
|
||||
const {baseUrl, themeConfig = {}} = siteConfig;
|
||||
const {algolia, navbar = {}} = themeConfig;
|
||||
|
|
@ -50,107 +59,145 @@ function Navbar() {
|
|||
setSidebarShown(false);
|
||||
}, [setSidebarShown]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const localStorageTheme = localStorage.getItem('theme');
|
||||
setTheme(localStorageTheme);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onToggleChange = e => {
|
||||
const nextTheme = e.target.checked ? 'dark' : '';
|
||||
setTheme(nextTheme);
|
||||
try {
|
||||
localStorage.setItem('theme', nextTheme);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={classnames('navbar', 'navbar--light', 'navbar--fixed-top', {
|
||||
'navbar--sidebar-show': sidebarShown,
|
||||
})}>
|
||||
<div className="navbar__inner">
|
||||
<div className="navbar__items">
|
||||
<div
|
||||
aria-label="Navigation bar toggle"
|
||||
className="navbar__toggle"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={showSidebar}
|
||||
onKeyDown={showSidebar}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="30"
|
||||
height="30"
|
||||
viewBox="0 0 30 30"
|
||||
role="img"
|
||||
focusable="false">
|
||||
<title>Menu</title>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
d="M4 7h22M4 15h22M4 23h22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<Link className="navbar__brand" to={baseUrl}>
|
||||
{logo != null && (
|
||||
<img
|
||||
className="navbar__logo"
|
||||
src={withBaseUrl(logo.src)}
|
||||
alt={logo.alt}
|
||||
/>
|
||||
)}
|
||||
{title != null && <strong>{title}</strong>}
|
||||
</Link>
|
||||
{links
|
||||
.filter(linkItem => linkItem.position !== 'right')
|
||||
.map((linkItem, i) => (
|
||||
<NavLink {...linkItem} key={i} />
|
||||
))}
|
||||
</div>
|
||||
<div className="navbar__items navbar__items--right">
|
||||
{links
|
||||
.filter(linkItem => linkItem.position === 'right')
|
||||
.map((linkItem, i) => (
|
||||
<NavLink {...linkItem} key={i} />
|
||||
))}
|
||||
{algolia && (
|
||||
<div className="navbar__search" key="search-box">
|
||||
<SearchBar />
|
||||
<React.Fragment>
|
||||
<Head>
|
||||
{/* TODO: Do not assume that it is in english language */}
|
||||
<html lang="en" data-theme={theme} />
|
||||
</Head>
|
||||
<nav
|
||||
className={classnames('navbar', 'navbar--light', 'navbar--fixed-top', {
|
||||
'navbar--sidebar-show': sidebarShown,
|
||||
})}>
|
||||
<div className="navbar__inner">
|
||||
<div className="navbar__items">
|
||||
<div
|
||||
aria-label="Navigation bar toggle"
|
||||
className="navbar__toggle"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={showSidebar}
|
||||
onKeyDown={showSidebar}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="30"
|
||||
height="30"
|
||||
viewBox="0 0 30 30"
|
||||
role="img"
|
||||
focusable="false">
|
||||
<title>Menu</title>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
d="M4 7h22M4 15h22M4 23h22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="presentation"
|
||||
className="navbar__sidebar__backdrop"
|
||||
onClick={() => {
|
||||
setSidebarShown(false);
|
||||
}}
|
||||
/>
|
||||
<div className="navbar__sidebar">
|
||||
<div className="navbar__sidebar__brand">
|
||||
<a
|
||||
className="navbar__brand"
|
||||
href="#!"
|
||||
role="button"
|
||||
onClick={hideSidebar}>
|
||||
{logo != null && (
|
||||
<img
|
||||
className="navbar__logo"
|
||||
src={withBaseUrl(logo.src)}
|
||||
alt={logo.alt}
|
||||
/>
|
||||
)}
|
||||
{title != null && <strong>{title}</strong>}
|
||||
</a>
|
||||
</div>
|
||||
<div className="navbar__sidebar__items">
|
||||
<div className="menu">
|
||||
<ul className="menu__list">
|
||||
{links.map((linkItem, i) => (
|
||||
<li className="menu__list-item" key={i}>
|
||||
<NavLink
|
||||
className="menu__link"
|
||||
{...linkItem}
|
||||
onClick={hideSidebar}
|
||||
/>
|
||||
</li>
|
||||
<Link className="navbar__brand" to={baseUrl}>
|
||||
{logo != null && (
|
||||
<img
|
||||
className="navbar__logo"
|
||||
src={withBaseUrl(logo.src)}
|
||||
alt={logo.alt}
|
||||
/>
|
||||
)}
|
||||
{title != null && <strong>{title}</strong>}
|
||||
</Link>
|
||||
{links
|
||||
.filter(linkItem => linkItem.position !== 'right')
|
||||
.map((linkItem, i) => (
|
||||
<NavLink {...linkItem} key={i} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="navbar__items navbar__items--right">
|
||||
{links
|
||||
.filter(linkItem => linkItem.position === 'right')
|
||||
.map((linkItem, i) => (
|
||||
<NavLink {...linkItem} key={i} />
|
||||
))}
|
||||
<Toggle
|
||||
className="large__viewport"
|
||||
aria-label="Dark mode toggle"
|
||||
checked={theme === 'dark'}
|
||||
onChange={onToggleChange}
|
||||
/>
|
||||
{algolia && (
|
||||
<div className="navbar__search" key="search-box">
|
||||
<SearchBar />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div
|
||||
role="presentation"
|
||||
className="navbar__sidebar__backdrop"
|
||||
onClick={() => {
|
||||
setSidebarShown(false);
|
||||
}}
|
||||
/>
|
||||
<div className="navbar__sidebar">
|
||||
<div className="navbar__sidebar__brand">
|
||||
<a
|
||||
className="navbar__brand"
|
||||
href="#!"
|
||||
role="button"
|
||||
onClick={hideSidebar}>
|
||||
{logo != null && (
|
||||
<img
|
||||
className="navbar__logo"
|
||||
src={withBaseUrl(logo.src)}
|
||||
alt={logo.alt}
|
||||
/>
|
||||
)}
|
||||
{title != null && <strong>{title}</strong>}
|
||||
</a>
|
||||
{sidebarShown && (
|
||||
<Toggle
|
||||
aria-label="Dark mode toggle in sidebar"
|
||||
checked={theme === 'dark'}
|
||||
onChange={onToggleChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="navbar__sidebar__items">
|
||||
<div className="menu">
|
||||
<ul className="menu__list">
|
||||
{links.map((linkItem, i) => (
|
||||
<li className="menu__list-item" key={i}>
|
||||
<NavLink
|
||||
className="menu__link"
|
||||
{...linkItem}
|
||||
onClick={hideSidebar}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.react-toggle {
|
||||
touch-action: pan-x;
|
||||
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 996px) {
|
||||
.large__viewport {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.react-toggle-screenreader-only {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.react-toggle--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
-webkit-transition: opacity 0.25s;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.react-toggle-track {
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border-radius: 30px;
|
||||
background-color: #4d4d4d;
|
||||
-webkit-transition: all 0.2s ease;
|
||||
-moz-transition: all 0.2s ease;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.react-toggle-track-check {
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
line-height: 0;
|
||||
left: 8px;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.25s ease;
|
||||
-moz-transition: opacity 0.25s ease;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.react-toggle--checked .react-toggle-track-check {
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity 0.25s ease;
|
||||
-moz-transition: opacity 0.25s ease;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.react-toggle-track-x {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
line-height: 0;
|
||||
right: 10px;
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity 0.25s ease;
|
||||
-moz-transition: opacity 0.25s ease;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.react-toggle--checked .react-toggle-track-x {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.react-toggle-thumb {
|
||||
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #4d4d4d;
|
||||
border-radius: 50%;
|
||||
background-color: #fafafa;
|
||||
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
-webkit-transition: all 0.25s ease;
|
||||
-moz-transition: all 0.25s ease;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.react-toggle--checked .react-toggle-thumb {
|
||||
left: 27px;
|
||||
border-color: #19ab27;
|
||||
}
|
||||
|
||||
.react-toggle--focus .react-toggle-thumb {
|
||||
-webkit-box-shadow: 0px 0px 3px 2px #0099e0;
|
||||
-moz-box-shadow: 0px 0px 3px 2px #0099e0;
|
||||
box-shadow: 0px 0px 2px 3px #0099e0;
|
||||
}
|
||||
|
||||
.react-toggle:active:not(.react-toggle--disabled) .react-toggle-thumb {
|
||||
-webkit-box-shadow: 0px 0px 5px 5px #0099e0;
|
||||
-moz-box-shadow: 0px 0px 5px 5px #0099e0;
|
||||
box-shadow: 0px 0px 5px 5px #0099e0;
|
||||
}
|
||||
|
|
@ -3763,7 +3763,7 @@ class-utils@^0.3.5:
|
|||
isobject "^3.0.0"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
classnames@^2.2.6:
|
||||
classnames@^2.2.5, classnames@^2.2.6:
|
||||
version "2.2.6"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
||||
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
||||
|
|
@ -11557,6 +11557,13 @@ react-test-renderer@^16.0.0-0:
|
|||
react-is "^16.8.6"
|
||||
scheduler "^0.13.6"
|
||||
|
||||
react-toggle@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-toggle/-/react-toggle-4.0.2.tgz#77f487860efb87fafd197672a2db8c885be1440f"
|
||||
integrity sha512-EPTWnN7gQHgEAUEmjheanZXNzY5TPnQeyyHfEs3YshaiWZf5WNjfYDrglO5F1Hl/dNveX18i4l0grTEsYH2Ccw==
|
||||
dependencies:
|
||||
classnames "^2.2.5"
|
||||
|
||||
react@^16.5.0, react@^16.8.4:
|
||||
version "16.8.6"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
|
||||
|
|
|
|||
Loading…
Reference in New Issue