mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 07:37:19 +02:00
refactor(theme-classic): replace color mode toggle with button; remove switchConfig (#6771)
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
b1492135c2
commit
7ec44bb32c
15 changed files with 282 additions and 356 deletions
|
@ -509,16 +509,17 @@ describe('themeConfig', () => {
|
||||||
const withDefaultValues = (colorMode) =>
|
const withDefaultValues = (colorMode) =>
|
||||||
_.merge({}, DEFAULT_CONFIG.colorMode, colorMode);
|
_.merge({}, DEFAULT_CONFIG.colorMode, colorMode);
|
||||||
|
|
||||||
test('minimal config', () => {
|
test('switch config', () => {
|
||||||
const colorMode = {
|
const colorMode = {
|
||||||
switchConfig: {
|
switchConfig: {
|
||||||
darkIcon: '🌙',
|
darkIcon: '🌙',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(testValidateThemeConfig({colorMode})).toEqual({
|
expect(() =>
|
||||||
...DEFAULT_CONFIG,
|
testValidateThemeConfig({colorMode}),
|
||||||
colorMode: withDefaultValues(colorMode),
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
});
|
`"colorMode.switchConfig is deprecated. If you want to customize the icons for light and dark mode, swizzle IconLightMode, IconDarkMode, or ColorModeToggle instead."`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('max config', () => {
|
test('max config', () => {
|
||||||
|
@ -526,17 +527,6 @@ describe('themeConfig', () => {
|
||||||
defaultMode: 'dark',
|
defaultMode: 'dark',
|
||||||
disableSwitch: false,
|
disableSwitch: false,
|
||||||
respectPrefersColorScheme: true,
|
respectPrefersColorScheme: true,
|
||||||
switchConfig: {
|
|
||||||
darkIcon: '🌙',
|
|
||||||
darkIconStyle: {
|
|
||||||
marginTop: '1px',
|
|
||||||
marginLeft: '2px',
|
|
||||||
},
|
|
||||||
lightIcon: '☀️',
|
|
||||||
lightIconStyle: {
|
|
||||||
marginLeft: '1px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
expect(testValidateThemeConfig({colorMode})).toEqual({
|
expect(testValidateThemeConfig({colorMode})).toEqual({
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
|
@ -562,16 +552,6 @@ describe('themeConfig', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('empty switch config', () => {
|
|
||||||
const colorMode = {
|
|
||||||
switchConfig: {},
|
|
||||||
};
|
|
||||||
expect(testValidateThemeConfig({colorMode})).toEqual({
|
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
colorMode: withDefaultValues(colorMode),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,95 +7,117 @@
|
||||||
|
|
||||||
import type {SwizzleConfig} from '@docusaurus/types';
|
import type {SwizzleConfig} from '@docusaurus/types';
|
||||||
|
|
||||||
|
/* eslint sort-keys: "error" */
|
||||||
|
|
||||||
export default function getSwizzleConfig(): SwizzleConfig {
|
export default function getSwizzleConfig(): SwizzleConfig {
|
||||||
return {
|
return {
|
||||||
components: {
|
components: {
|
||||||
CodeBlock: {
|
CodeBlock: {
|
||||||
actions: {
|
actions: {
|
||||||
wrap: 'safe',
|
|
||||||
eject: 'safe',
|
eject: 'safe',
|
||||||
|
wrap: 'safe',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'The component used to render multi-line code blocks, generally used in Markdown files.',
|
'The component used to render multi-line code blocks, generally used in Markdown files.',
|
||||||
},
|
},
|
||||||
|
ColorModeToggle: {
|
||||||
|
actions: {
|
||||||
|
eject: 'safe',
|
||||||
|
wrap: 'safe',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'The color mode toggle to switch between light and dark mode.',
|
||||||
|
},
|
||||||
DocSidebar: {
|
DocSidebar: {
|
||||||
actions: {
|
actions: {
|
||||||
wrap: 'safe',
|
|
||||||
eject: 'unsafe', // too much technical code in sidebar, not very safe atm
|
eject: 'unsafe', // too much technical code in sidebar, not very safe atm
|
||||||
|
wrap: 'safe',
|
||||||
},
|
},
|
||||||
description: 'The sidebar component on docs pages',
|
description: 'The sidebar component on docs pages',
|
||||||
},
|
},
|
||||||
Footer: {
|
Footer: {
|
||||||
actions: {
|
actions: {
|
||||||
wrap: 'safe',
|
|
||||||
eject: 'unsafe', // TODO split footer into smaller parts
|
eject: 'unsafe', // TODO split footer into smaller parts
|
||||||
|
wrap: 'safe',
|
||||||
},
|
},
|
||||||
description: "The footer component of you site's layout",
|
description: "The footer component of you site's layout",
|
||||||
},
|
},
|
||||||
|
IconArrow: {
|
||||||
|
actions: {
|
||||||
|
eject: 'safe',
|
||||||
|
wrap: 'safe',
|
||||||
|
},
|
||||||
|
description: 'The arrow icon component',
|
||||||
|
},
|
||||||
|
IconDarkMode: {
|
||||||
|
actions: {
|
||||||
|
eject: 'safe',
|
||||||
|
wrap: 'safe',
|
||||||
|
},
|
||||||
|
description: 'The dark mode icon component.',
|
||||||
|
},
|
||||||
|
IconEdit: {
|
||||||
|
actions: {
|
||||||
|
eject: 'safe',
|
||||||
|
wrap: 'safe',
|
||||||
|
},
|
||||||
|
description: 'The edit icon component',
|
||||||
|
},
|
||||||
|
IconLightMode: {
|
||||||
|
actions: {
|
||||||
|
eject: 'safe',
|
||||||
|
wrap: 'safe',
|
||||||
|
},
|
||||||
|
description: 'The light mode icon component.',
|
||||||
|
},
|
||||||
|
IconMenu: {
|
||||||
|
actions: {
|
||||||
|
eject: 'safe',
|
||||||
|
wrap: 'safe',
|
||||||
|
},
|
||||||
|
description: 'The menu icon component',
|
||||||
|
},
|
||||||
|
MDXComponents: {
|
||||||
|
actions: {
|
||||||
|
eject: 'safe',
|
||||||
|
wrap: 'forbidden', /// TODO allow wrapping objects???
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'The MDX components to use for rendering MDX files. Meant to be ejected.',
|
||||||
|
},
|
||||||
|
// TODO should probably not even appear here
|
||||||
|
'NavbarItem/utils': {
|
||||||
|
actions: {
|
||||||
|
eject: 'forbidden',
|
||||||
|
wrap: 'forbidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
NotFound: {
|
NotFound: {
|
||||||
actions: {
|
actions: {
|
||||||
wrap: 'safe',
|
|
||||||
eject: 'safe',
|
eject: 'safe',
|
||||||
|
wrap: 'safe',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'The global 404 page of your site, meant to be ejected and customized',
|
'The global 404 page of your site, meant to be ejected and customized',
|
||||||
},
|
},
|
||||||
SearchBar: {
|
SearchBar: {
|
||||||
actions: {
|
actions: {
|
||||||
wrap: 'safe',
|
|
||||||
eject: 'safe',
|
eject: 'safe',
|
||||||
|
wrap: 'safe',
|
||||||
},
|
},
|
||||||
// TODO how to describe this one properly?
|
// TODO how to describe this one properly?
|
||||||
// By default it's an empty placeholder for the user to fill
|
// By default it's an empty placeholder for the user to fill
|
||||||
description:
|
description:
|
||||||
'The search bar component of your site, appearing in the navbar.',
|
'The search bar component of your site, appearing in the navbar.',
|
||||||
},
|
},
|
||||||
IconArrow: {
|
|
||||||
actions: {
|
|
||||||
wrap: 'safe',
|
|
||||||
eject: 'safe',
|
|
||||||
},
|
|
||||||
description: 'The arrow icon component',
|
|
||||||
},
|
|
||||||
IconEdit: {
|
|
||||||
actions: {
|
|
||||||
wrap: 'safe',
|
|
||||||
eject: 'safe',
|
|
||||||
},
|
|
||||||
description: 'The edit icon component',
|
|
||||||
},
|
|
||||||
IconMenu: {
|
|
||||||
actions: {
|
|
||||||
wrap: 'safe',
|
|
||||||
eject: 'safe',
|
|
||||||
},
|
|
||||||
description: 'The menu icon component',
|
|
||||||
},
|
|
||||||
|
|
||||||
'prism-include-languages': {
|
'prism-include-languages': {
|
||||||
actions: {
|
actions: {
|
||||||
wrap: 'forbidden', // not a component!
|
|
||||||
eject: 'safe',
|
eject: 'safe',
|
||||||
|
wrap: 'forbidden', // not a component!
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'The Prism languages to include for code block syntax highlighting. Meant to be ejected.',
|
'The Prism languages to include for code block syntax highlighting. Meant to be ejected.',
|
||||||
},
|
},
|
||||||
MDXComponents: {
|
|
||||||
actions: {
|
|
||||||
wrap: 'forbidden', /// TODO allow wrapping objects???
|
|
||||||
eject: 'safe',
|
|
||||||
},
|
|
||||||
description:
|
|
||||||
'The MDX components to use for rendering MDX files. Meant to be ejected.',
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO should probably not even appear here
|
|
||||||
'NavbarItem/utils': {
|
|
||||||
actions: {
|
|
||||||
wrap: 'forbidden',
|
|
||||||
eject: 'forbidden',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -632,7 +632,7 @@ declare module '@theme/TOCCollapsible' {
|
||||||
export default function TOCCollapsible(props: Props): JSX.Element;
|
export default function TOCCollapsible(props: Props): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/Toggle' {
|
declare module '@theme/ColorModeToggle' {
|
||||||
import type {SyntheticEvent} from 'react';
|
import type {SyntheticEvent} from 'react';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -663,6 +663,14 @@ declare module '@theme/IconArrow' {
|
||||||
export default function IconArrow(props: Props): JSX.Element;
|
export default function IconArrow(props: Props): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/IconDarkMode' {
|
||||||
|
import type {ComponentProps} from 'react';
|
||||||
|
|
||||||
|
export interface Props extends ComponentProps<'svg'> {}
|
||||||
|
|
||||||
|
export default function IconDarkMode(props: Props): JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/IconEdit' {
|
declare module '@theme/IconEdit' {
|
||||||
import type {ComponentProps} from 'react';
|
import type {ComponentProps} from 'react';
|
||||||
|
|
||||||
|
@ -671,6 +679,14 @@ declare module '@theme/IconEdit' {
|
||||||
export default function IconEdit(props: Props): JSX.Element;
|
export default function IconEdit(props: Props): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/IconLightMode' {
|
||||||
|
import type {ComponentProps} from 'react';
|
||||||
|
|
||||||
|
export interface Props extends ComponentProps<'svg'> {}
|
||||||
|
|
||||||
|
export default function IconLightMode(props: Props): JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/IconMenu' {
|
declare module '@theme/IconMenu' {
|
||||||
import type {ComponentProps} from 'react';
|
import type {ComponentProps} from 'react';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useState, useRef, useEffect} from 'react';
|
||||||
|
import type {Props} from '@theme/ColorModeToggle';
|
||||||
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
import IconLightMode from '@theme/IconLightMode';
|
||||||
|
import IconDarkMode from '@theme/IconDarkMode';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
function ColorModeToggle({
|
||||||
|
className,
|
||||||
|
checked: defaultChecked,
|
||||||
|
onChange,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const isBrowser = useIsBrowser();
|
||||||
|
const [checked, setChecked] = useState(defaultChecked);
|
||||||
|
const [focused, setFocused] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setChecked(defaultChecked);
|
||||||
|
}, [defaultChecked]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(styles.toggle, className, {
|
||||||
|
[styles.toggleChecked]: checked,
|
||||||
|
[styles.toggleFocused]: focused,
|
||||||
|
[styles.toggleDisabled]: !isBrowser,
|
||||||
|
})}>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
||||||
|
<div
|
||||||
|
className={styles.toggleButton}
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={() => inputRef.current?.click()}>
|
||||||
|
<IconLightMode
|
||||||
|
className={clsx(styles.toggleIcon, styles.lightToggleIcon)}
|
||||||
|
/>
|
||||||
|
<IconDarkMode
|
||||||
|
className={clsx(styles.toggleIcon, styles.darkToggleIcon)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
checked={checked}
|
||||||
|
type="checkbox"
|
||||||
|
className={styles.toggleScreenReader}
|
||||||
|
aria-label={translate(
|
||||||
|
{
|
||||||
|
message: 'Switch between dark and light mode (currently {mode})',
|
||||||
|
id: 'theme.colorToggle.ariaLabel',
|
||||||
|
description: 'The ARIA label for the navbar color mode toggle',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: checked
|
||||||
|
? translate({
|
||||||
|
message: 'dark mode',
|
||||||
|
id: 'theme.colorToggle.ariaLabel.mode.dark',
|
||||||
|
description: 'The name for the dark color mode',
|
||||||
|
})
|
||||||
|
: translate({
|
||||||
|
message: 'light mode',
|
||||||
|
id: 'theme.colorToggle.ariaLabel.mode.light',
|
||||||
|
description: 'The name for the light color mode',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onChange={onChange}
|
||||||
|
onClick={() => setChecked(!checked)}
|
||||||
|
onFocus={() => setFocused(true)}
|
||||||
|
onBlur={() => setFocused(false)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
inputRef.current?.click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(ColorModeToggle);
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleScreenReader {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleDisabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleButton {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleButton:hover {
|
||||||
|
background-color: #00000010;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .toggleButton:hover {
|
||||||
|
background-color: #ffffff20;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='light'] .darkToggleIcon,
|
||||||
|
[data-theme='dark'] .lightToggleIcon {
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import type {Props} from '@theme/IconDarkMode';
|
||||||
|
|
||||||
|
export default function IconDarkMode(props: Props): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" width={24} height={24} {...props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import type {Props} from '@theme/IconLightMode';
|
||||||
|
|
||||||
|
export default function IconLightMode(props: Props): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" width={24} height={24} {...props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import React, {useCallback, useState, useEffect} from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Translate from '@docusaurus/Translate';
|
import Translate from '@docusaurus/Translate';
|
||||||
import SearchBar from '@theme/SearchBar';
|
import SearchBar from '@theme/SearchBar';
|
||||||
import Toggle from '@theme/Toggle';
|
import ColorModeToggle from '@theme/ColorModeToggle';
|
||||||
import {
|
import {
|
||||||
useThemeConfig,
|
useThemeConfig,
|
||||||
useMobileSecondaryMenuRenderer,
|
useMobileSecondaryMenuRenderer,
|
||||||
|
@ -171,7 +171,7 @@ function NavbarMobileSidebar({
|
||||||
titleClassName="navbar__title"
|
titleClassName="navbar__title"
|
||||||
/>
|
/>
|
||||||
{!colorModeToggle.disabled && (
|
{!colorModeToggle.disabled && (
|
||||||
<Toggle
|
<ColorModeToggle
|
||||||
className={styles.navbarSidebarToggle}
|
className={styles.navbarSidebarToggle}
|
||||||
checked={colorModeToggle.isDarkTheme}
|
checked={colorModeToggle.isDarkTheme}
|
||||||
onChange={colorModeToggle.toggle}
|
onChange={colorModeToggle.toggle}
|
||||||
|
@ -271,7 +271,7 @@ export default function Navbar(): JSX.Element {
|
||||||
<NavbarItem {...item} key={i} />
|
<NavbarItem {...item} key={i} />
|
||||||
))}
|
))}
|
||||||
{!colorModeToggle.disabled && (
|
{!colorModeToggle.disabled && (
|
||||||
<Toggle
|
<ColorModeToggle
|
||||||
className={styles.toggle}
|
className={styles.toggle}
|
||||||
checked={colorModeToggle.isDarkTheme}
|
checked={colorModeToggle.isDarkTheme}
|
||||||
onChange={colorModeToggle.toggle}
|
onChange={colorModeToggle.toggle}
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, {useState, useRef, useEffect, memo} from 'react';
|
|
||||||
import type {Props} from '@theme/Toggle';
|
|
||||||
import {useThemeConfig, type ColorModeConfig} from '@docusaurus/theme-common';
|
|
||||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
|
||||||
import {translate} from '@docusaurus/Translate';
|
|
||||||
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import styles from './styles.module.css';
|
|
||||||
|
|
||||||
// Based on react-toggle (https://github.com/aaronshaf/react-toggle/).
|
|
||||||
const ToggleComponent = memo(
|
|
||||||
({
|
|
||||||
className,
|
|
||||||
switchConfig,
|
|
||||||
checked: defaultChecked,
|
|
||||||
disabled,
|
|
||||||
onChange,
|
|
||||||
}: Props & {
|
|
||||||
switchConfig: ColorModeConfig['switchConfig'];
|
|
||||||
disabled: boolean;
|
|
||||||
}): JSX.Element => {
|
|
||||||
const {darkIcon, darkIconStyle, lightIcon, lightIconStyle} = switchConfig;
|
|
||||||
const [checked, setChecked] = useState(defaultChecked);
|
|
||||||
const [focused, setFocused] = useState(false);
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setChecked(defaultChecked);
|
|
||||||
}, [defaultChecked]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(styles.toggle, className, {
|
|
||||||
[styles.toggleChecked]: checked,
|
|
||||||
[styles.toggleFocused]: focused,
|
|
||||||
[styles.toggleDisabled]: disabled,
|
|
||||||
})}>
|
|
||||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
|
||||||
<div
|
|
||||||
className={styles.toggleTrack}
|
|
||||||
role="button"
|
|
||||||
tabIndex={-1}
|
|
||||||
onClick={() => inputRef.current?.click()}>
|
|
||||||
<div className={styles.toggleTrackCheck}>
|
|
||||||
<span className={styles.toggleIcon} style={darkIconStyle}>
|
|
||||||
{darkIcon}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.toggleTrackX}>
|
|
||||||
<span className={styles.toggleIcon} style={lightIconStyle}>
|
|
||||||
{lightIcon}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.toggleTrackThumb} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
checked={checked}
|
|
||||||
type="checkbox"
|
|
||||||
className={styles.toggleScreenReader}
|
|
||||||
aria-label={translate(
|
|
||||||
{
|
|
||||||
message: 'Switch between dark and light mode (currently {mode})',
|
|
||||||
id: 'theme.colorToggle.ariaLabel',
|
|
||||||
description: 'The ARIA label for the navbar color mode toggle',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mode: checked
|
|
||||||
? translate({
|
|
||||||
message: 'dark mode',
|
|
||||||
id: 'theme.colorToggle.ariaLabel.mode.dark',
|
|
||||||
description: 'The name for the dark color mode',
|
|
||||||
})
|
|
||||||
: translate({
|
|
||||||
message: 'light mode',
|
|
||||||
id: 'theme.colorToggle.ariaLabel.mode.light',
|
|
||||||
description: 'The name for the light color mode',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
onChange={onChange}
|
|
||||||
onClick={() => setChecked(!checked)}
|
|
||||||
onFocus={() => setFocused(true)}
|
|
||||||
onBlur={() => setFocused(false)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
inputRef.current?.click();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function Toggle(props: Props): JSX.Element {
|
|
||||||
const {
|
|
||||||
colorMode: {switchConfig},
|
|
||||||
} = useThemeConfig();
|
|
||||||
const isBrowser = useIsBrowser();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToggleComponent
|
|
||||||
switchConfig={switchConfig}
|
|
||||||
disabled={!isBrowser}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.toggle {
|
|
||||||
touch-action: pan-x;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleScreenReader {
|
|
||||||
border: 0;
|
|
||||||
clip: rect(0 0 0 0);
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleDisabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleTrack {
|
|
||||||
width: 50px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 30px;
|
|
||||||
background-color: #4d4d4d;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleTrackCheck {
|
|
||||||
position: absolute;
|
|
||||||
width: 14px;
|
|
||||||
height: 10px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: auto 0;
|
|
||||||
left: 8px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleChecked .toggleTrackCheck,
|
|
||||||
[data-theme='dark'] .toggle .toggleTrackCheck {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleTrackX {
|
|
||||||
position: absolute;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: auto 0;
|
|
||||||
right: 10px;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleChecked .toggleTrackX,
|
|
||||||
[data-theme='dark'] .toggle .toggleTrackX {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleTrackThumb {
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
left: 1px;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border: 1px solid #4d4d4d;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #fafafa;
|
|
||||||
transition: all 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleFocused .toggleTrackThumb,
|
|
||||||
.toggle:hover .toggleTrackThumb {
|
|
||||||
box-shadow: 0 0 2px 3px var(--ifm-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* stylelint-disable-next-line no-descending-specificity */
|
|
||||||
.toggleChecked .toggleTrackThumb,
|
|
||||||
[data-theme='dark'] .toggle .toggleTrackThumb {
|
|
||||||
left: 27px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle:active:not(.toggleDisabled) .toggleTrackThumb {
|
|
||||||
box-shadow: 0 0 5px 5px var(--ifm-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleIcon {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
height: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
|
@ -21,12 +21,6 @@ const DEFAULT_COLOR_MODE_CONFIG = {
|
||||||
defaultMode: 'light',
|
defaultMode: 'light',
|
||||||
disableSwitch: false,
|
disableSwitch: false,
|
||||||
respectPrefersColorScheme: false,
|
respectPrefersColorScheme: false,
|
||||||
switchConfig: {
|
|
||||||
darkIcon: '🌜',
|
|
||||||
darkIconStyle: {},
|
|
||||||
lightIcon: '🌞',
|
|
||||||
lightIconStyle: {},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_CONFIG = {
|
export const DEFAULT_CONFIG = {
|
||||||
|
@ -220,20 +214,10 @@ const ColorModeSchema = Joi.object({
|
||||||
respectPrefersColorScheme: Joi.bool().default(
|
respectPrefersColorScheme: Joi.bool().default(
|
||||||
DEFAULT_COLOR_MODE_CONFIG.respectPrefersColorScheme,
|
DEFAULT_COLOR_MODE_CONFIG.respectPrefersColorScheme,
|
||||||
),
|
),
|
||||||
switchConfig: Joi.object({
|
switchConfig: Joi.any().forbidden().messages({
|
||||||
darkIcon: Joi.string().default(
|
'any.unknown':
|
||||||
DEFAULT_COLOR_MODE_CONFIG.switchConfig.darkIcon,
|
'colorMode.switchConfig is deprecated. If you want to customize the icons for light and dark mode, swizzle IconLightMode, IconDarkMode, or ColorModeToggle instead.',
|
||||||
),
|
}),
|
||||||
darkIconStyle: Joi.object().default(
|
|
||||||
DEFAULT_COLOR_MODE_CONFIG.switchConfig.darkIconStyle,
|
|
||||||
),
|
|
||||||
lightIcon: Joi.string().default(
|
|
||||||
DEFAULT_COLOR_MODE_CONFIG.switchConfig.lightIcon,
|
|
||||||
),
|
|
||||||
lightIconStyle: Joi.object().default(
|
|
||||||
DEFAULT_COLOR_MODE_CONFIG.switchConfig.lightIconStyle,
|
|
||||||
),
|
|
||||||
}).default(DEFAULT_COLOR_MODE_CONFIG.switchConfig),
|
|
||||||
}).default(DEFAULT_COLOR_MODE_CONFIG);
|
}).default(DEFAULT_COLOR_MODE_CONFIG);
|
||||||
|
|
||||||
// schema can probably be improved
|
// schema can probably be improved
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import type {PrismTheme} from 'prism-react-renderer';
|
import type {PrismTheme} from 'prism-react-renderer';
|
||||||
import type {CSSProperties} from 'react';
|
|
||||||
import type {DeepPartial} from 'utility-types';
|
import type {DeepPartial} from 'utility-types';
|
||||||
|
|
||||||
export type DocsVersionPersistence = 'localStorage' | 'none';
|
export type DocsVersionPersistence = 'localStorage' | 'none';
|
||||||
|
@ -43,12 +42,6 @@ export type ColorModeConfig = {
|
||||||
defaultMode: 'light' | 'dark';
|
defaultMode: 'light' | 'dark';
|
||||||
disableSwitch: boolean;
|
disableSwitch: boolean;
|
||||||
respectPrefersColorScheme: boolean;
|
respectPrefersColorScheme: boolean;
|
||||||
switchConfig: {
|
|
||||||
darkIcon: string;
|
|
||||||
darkIconStyle: CSSProperties;
|
|
||||||
lightIcon: string;
|
|
||||||
lightIconStyle: CSSProperties;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AnnouncementBarConfig = {
|
export type AnnouncementBarConfig = {
|
||||||
|
|
|
@ -284,18 +284,6 @@ module.exports = {
|
||||||
defaultMode: 'light',
|
defaultMode: 'light',
|
||||||
disableSwitch: false,
|
disableSwitch: false,
|
||||||
respectPrefersColorScheme: true,
|
respectPrefersColorScheme: true,
|
||||||
switchConfig: {
|
|
||||||
darkIcon: '🌙',
|
|
||||||
lightIcon: '\u2600',
|
|
||||||
// React inline style object
|
|
||||||
// see https://reactjs.org/docs/dom-elements.html#style
|
|
||||||
darkIconStyle: {
|
|
||||||
marginLeft: '2px',
|
|
||||||
},
|
|
||||||
lightIconStyle: {
|
|
||||||
marginLeft: '1px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
navbar: {
|
navbar: {
|
||||||
title: 'Site Title',
|
title: 'Site Title',
|
||||||
|
|
|
@ -28,11 +28,6 @@ Accepted fields:
|
||||||
| `defaultMode` | <code>'light' \| 'dark'</code> | `'light'` | The color mode when user first visits the site. |
|
| `defaultMode` | <code>'light' \| 'dark'</code> | `'light'` | The color mode when user first visits the site. |
|
||||||
| `disableSwitch` | `boolean` | `false` | Hides the switch in the navbar. Useful if you want to support a single color mode. |
|
| `disableSwitch` | `boolean` | `false` | Hides the switch in the navbar. Useful if you want to support a single color mode. |
|
||||||
| `respectPrefersColorScheme` | `boolean` | `false` | Whether to use the `prefers-color-scheme` media-query, using user system preferences, instead of the hardcoded `defaultMode`. |
|
| `respectPrefersColorScheme` | `boolean` | `false` | Whether to use the `prefers-color-scheme` media-query, using user system preferences, instead of the hardcoded `defaultMode`. |
|
||||||
| `switchConfig` | _See below_ | _See below_ | Dark/light switch icon options. |
|
|
||||||
| `switchConfig.darkIcon` | `string` | `'🌜'` | Icon for the switch while in dark mode. |
|
|
||||||
| `switchConfig.darkIconStyle` | JSX style object (see [documentation](https://reactjs.org/docs/dom-elements.html#style)) | `{}` | CSS to apply to dark icon. |
|
|
||||||
| `switchConfig.lightIcon` | `string` | `'🌞'` | Icon for the switch while in light mode. |
|
|
||||||
| `switchConfig.lightIconStyle` | JSX style object | `{}` | CSS to apply to light icon. |
|
|
||||||
|
|
||||||
</APITable>
|
</APITable>
|
||||||
|
|
||||||
|
@ -46,18 +41,6 @@ module.exports = {
|
||||||
defaultMode: 'light',
|
defaultMode: 'light',
|
||||||
disableSwitch: false,
|
disableSwitch: false,
|
||||||
respectPrefersColorScheme: false,
|
respectPrefersColorScheme: false,
|
||||||
switchConfig: {
|
|
||||||
darkIcon: '🌙',
|
|
||||||
darkIconStyle: {
|
|
||||||
marginLeft: '2px',
|
|
||||||
},
|
|
||||||
// Unicode icons such as '\u2600' will work
|
|
||||||
// Unicode with 5 chars require brackets: '\u{1F602}'
|
|
||||||
lightIcon: '\u{1F602}',
|
|
||||||
lightIconStyle: {
|
|
||||||
marginLeft: '1px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// highlight-end
|
// highlight-end
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import OriginalToggle from '@theme-original/Toggle';
|
import OriginalToggle from '@theme-original/ColorModeToggle';
|
||||||
import type {Props} from '@theme/Toggle';
|
import type {Props} from '@theme/ColorModeToggle';
|
||||||
import {
|
import {
|
||||||
lightStorage,
|
lightStorage,
|
||||||
darkStorage,
|
darkStorage,
|
||||||
|
@ -24,7 +24,7 @@ import {
|
||||||
// session storage, and we need to apply the same style when toggling modes even
|
// session storage, and we need to apply the same style when toggling modes even
|
||||||
// when we are not on the styling-layout page. The only way to do this so far is
|
// when we are not on the styling-layout page. The only way to do this so far is
|
||||||
// by hooking into the Toggle component.
|
// by hooking into the Toggle component.
|
||||||
export default function Toggle(props: Props): JSX.Element {
|
export default function ColorModeToggle(props: Props): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<OriginalToggle
|
<OriginalToggle
|
||||||
{...props}
|
{...props}
|
Loading…
Add table
Add a link
Reference in a new issue