diff --git a/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts b/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts index 004594b5e74b..f9a97d31588d 100644 --- a/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts +++ b/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts @@ -509,16 +509,17 @@ describe('themeConfig', () => { const withDefaultValues = (colorMode) => _.merge({}, DEFAULT_CONFIG.colorMode, colorMode); - test('minimal config', () => { + test('switch config', () => { const colorMode = { switchConfig: { darkIcon: '🌙', }, }; - expect(testValidateThemeConfig({colorMode})).toEqual({ - ...DEFAULT_CONFIG, - colorMode: withDefaultValues(colorMode), - }); + expect(() => + testValidateThemeConfig({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', () => { @@ -526,17 +527,6 @@ describe('themeConfig', () => { defaultMode: 'dark', disableSwitch: false, respectPrefersColorScheme: true, - switchConfig: { - darkIcon: '🌙', - darkIconStyle: { - marginTop: '1px', - marginLeft: '2px', - }, - lightIcon: '☀️', - lightIconStyle: { - marginLeft: '1px', - }, - }, }; expect(testValidateThemeConfig({colorMode})).toEqual({ ...DEFAULT_CONFIG, @@ -562,16 +552,6 @@ describe('themeConfig', () => { }, }); }); - - test('empty switch config', () => { - const colorMode = { - switchConfig: {}, - }; - expect(testValidateThemeConfig({colorMode})).toEqual({ - ...DEFAULT_CONFIG, - colorMode: withDefaultValues(colorMode), - }); - }); }); }); diff --git a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts index 465d078b7f22..f185a7ba2e4f 100644 --- a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts +++ b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts @@ -7,94 +7,116 @@ import type {SwizzleConfig} from '@docusaurus/types'; +/* eslint sort-keys: "error" */ + export default function getSwizzleConfig(): SwizzleConfig { return { components: { CodeBlock: { actions: { - wrap: 'safe', eject: 'safe', + wrap: 'safe', }, description: 'The component used to render multi-line code blocks, generally used in Markdown files.', }, - DocSidebar: { + ColorModeToggle: { actions: { + eject: 'safe', wrap: 'safe', + }, + description: + 'The color mode toggle to switch between light and dark mode.', + }, + DocSidebar: { + actions: { eject: 'unsafe', // too much technical code in sidebar, not very safe atm + wrap: 'safe', }, description: 'The sidebar component on docs pages', }, Footer: { actions: { - wrap: 'safe', eject: 'unsafe', // TODO split footer into smaller parts + wrap: 'safe', }, description: "The footer component of you site's layout", }, - NotFound: { + IconArrow: { actions: { - wrap: 'safe', eject: 'safe', - }, - description: - 'The global 404 page of your site, meant to be ejected and customized', - }, - SearchBar: { - actions: { wrap: 'safe', - eject: 'safe', }, - // TODO how to describe this one properly? - // By default it's an empty placeholder for the user to fill - description: - 'The search bar component of your site, appearing in the navbar.', + description: 'The arrow icon component', }, - IconArrow: { + IconDarkMode: { actions: { - wrap: 'safe', eject: 'safe', + wrap: 'safe', }, - description: 'The arrow icon component', + description: 'The dark mode icon component.', }, IconEdit: { actions: { - wrap: 'safe', eject: 'safe', + wrap: 'safe', }, description: 'The edit icon component', }, - IconMenu: { + IconLightMode: { actions: { - wrap: 'safe', eject: 'safe', + wrap: 'safe', }, - description: 'The menu icon component', + description: 'The light mode icon component.', }, - - 'prism-include-languages': { + IconMenu: { actions: { - wrap: 'forbidden', // not a component! eject: 'safe', + wrap: 'safe', }, - description: - 'The Prism languages to include for code block syntax highlighting. Meant to be ejected.', + description: 'The menu icon component', }, MDXComponents: { actions: { - wrap: 'forbidden', /// TODO allow wrapping objects??? 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: { - wrap: 'forbidden', eject: 'forbidden', + wrap: 'forbidden', + }, + }, + NotFound: { + actions: { + eject: 'safe', + wrap: 'safe', + }, + description: + 'The global 404 page of your site, meant to be ejected and customized', + }, + SearchBar: { + actions: { + eject: 'safe', + wrap: 'safe', }, + // TODO how to describe this one properly? + // By default it's an empty placeholder for the user to fill + description: + 'The search bar component of your site, appearing in the navbar.', + }, + 'prism-include-languages': { + actions: { + eject: 'safe', + wrap: 'forbidden', // not a component! + }, + description: + 'The Prism languages to include for code block syntax highlighting. Meant to be ejected.', }, }, }; diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 518512b11f6f..d1f5be5333d5 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -632,7 +632,7 @@ declare module '@theme/TOCCollapsible' { export default function TOCCollapsible(props: Props): JSX.Element; } -declare module '@theme/Toggle' { +declare module '@theme/ColorModeToggle' { import type {SyntheticEvent} from 'react'; export interface Props { @@ -663,6 +663,14 @@ declare module '@theme/IconArrow' { 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' { import type {ComponentProps} from 'react'; @@ -671,6 +679,14 @@ declare module '@theme/IconEdit' { 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' { import type {ComponentProps} from 'react'; diff --git a/packages/docusaurus-theme-classic/src/theme/ColorModeToggle/index.tsx b/packages/docusaurus-theme-classic/src/theme/ColorModeToggle/index.tsx new file mode 100644 index 000000000000..46be740f6fe4 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/ColorModeToggle/index.tsx @@ -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(null); + + useEffect(() => { + setChecked(defaultChecked); + }, [defaultChecked]); + + return ( +
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} +
inputRef.current?.click()}> + + +
+ + setChecked(!checked)} + onFocus={() => setFocused(true)} + onBlur={() => setFocused(false)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + inputRef.current?.click(); + } + }} + /> +
+ ); +} + +export default React.memo(ColorModeToggle); diff --git a/packages/docusaurus-theme-classic/src/theme/ColorModeToggle/styles.module.css b/packages/docusaurus-theme-classic/src/theme/ColorModeToggle/styles.module.css new file mode 100644 index 000000000000..a5a7bfb73799 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/ColorModeToggle/styles.module.css @@ -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; +} diff --git a/packages/docusaurus-theme-classic/src/theme/IconDarkMode/index.tsx b/packages/docusaurus-theme-classic/src/theme/IconDarkMode/index.tsx new file mode 100644 index 000000000000..d319096eeab3 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/IconDarkMode/index.tsx @@ -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 ( + + + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/IconLightMode/index.tsx b/packages/docusaurus-theme-classic/src/theme/IconLightMode/index.tsx new file mode 100644 index 000000000000..9c080c966791 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/IconLightMode/index.tsx @@ -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 ( + + + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Navbar/index.tsx b/packages/docusaurus-theme-classic/src/theme/Navbar/index.tsx index 789b79a96fda..84c92596337a 100644 --- a/packages/docusaurus-theme-classic/src/theme/Navbar/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Navbar/index.tsx @@ -9,7 +9,7 @@ import React, {useCallback, useState, useEffect} from 'react'; import clsx from 'clsx'; import Translate from '@docusaurus/Translate'; import SearchBar from '@theme/SearchBar'; -import Toggle from '@theme/Toggle'; +import ColorModeToggle from '@theme/ColorModeToggle'; import { useThemeConfig, useMobileSecondaryMenuRenderer, @@ -171,7 +171,7 @@ function NavbarMobileSidebar({ titleClassName="navbar__title" /> {!colorModeToggle.disabled && ( - ))} {!colorModeToggle.disabled && ( - { - const {darkIcon, darkIconStyle, lightIcon, lightIconStyle} = switchConfig; - const [checked, setChecked] = useState(defaultChecked); - const [focused, setFocused] = useState(false); - const inputRef = useRef(null); - - useEffect(() => { - setChecked(defaultChecked); - }, [defaultChecked]); - - return ( -
- {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} -
inputRef.current?.click()}> -
- - {darkIcon} - -
-
- - {lightIcon} - -
-
-
- - setChecked(!checked)} - onFocus={() => setFocused(true)} - onBlur={() => setFocused(false)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - inputRef.current?.click(); - } - }} - /> -
- ); - }, -); - -export default function Toggle(props: Props): JSX.Element { - const { - colorMode: {switchConfig}, - } = useThemeConfig(); - const isBrowser = useIsBrowser(); - - return ( - - ); -} diff --git a/packages/docusaurus-theme-classic/src/theme/Toggle/styles.module.css b/packages/docusaurus-theme-classic/src/theme/Toggle/styles.module.css deleted file mode 100644 index c293660b85c5..000000000000 --- a/packages/docusaurus-theme-classic/src/theme/Toggle/styles.module.css +++ /dev/null @@ -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; -} diff --git a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts index 4f7a9e3ded5d..f0e7d7bf4a3d 100644 --- a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts +++ b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts @@ -21,12 +21,6 @@ const DEFAULT_COLOR_MODE_CONFIG = { defaultMode: 'light', disableSwitch: false, respectPrefersColorScheme: false, - switchConfig: { - darkIcon: '🌜', - darkIconStyle: {}, - lightIcon: '🌞', - lightIconStyle: {}, - }, }; export const DEFAULT_CONFIG = { @@ -220,20 +214,10 @@ const ColorModeSchema = Joi.object({ respectPrefersColorScheme: Joi.bool().default( DEFAULT_COLOR_MODE_CONFIG.respectPrefersColorScheme, ), - switchConfig: Joi.object({ - darkIcon: Joi.string().default( - DEFAULT_COLOR_MODE_CONFIG.switchConfig.darkIcon, - ), - 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), + switchConfig: Joi.any().forbidden().messages({ + 'any.unknown': + 'colorMode.switchConfig is deprecated. If you want to customize the icons for light and dark mode, swizzle IconLightMode, IconDarkMode, or ColorModeToggle instead.', + }), }).default(DEFAULT_COLOR_MODE_CONFIG); // schema can probably be improved diff --git a/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts b/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts index 26975d2563ce..762ed3a14943 100644 --- a/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts +++ b/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts @@ -7,7 +7,6 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import type {PrismTheme} from 'prism-react-renderer'; -import type {CSSProperties} from 'react'; import type {DeepPartial} from 'utility-types'; export type DocsVersionPersistence = 'localStorage' | 'none'; @@ -43,12 +42,6 @@ export type ColorModeConfig = { defaultMode: 'light' | 'dark'; disableSwitch: boolean; respectPrefersColorScheme: boolean; - switchConfig: { - darkIcon: string; - darkIconStyle: CSSProperties; - lightIcon: string; - lightIconStyle: CSSProperties; - }; }; export type AnnouncementBarConfig = { diff --git a/website/docs/api/docusaurus.config.js.md b/website/docs/api/docusaurus.config.js.md index ab50dc53956c..4f064ef619b1 100644 --- a/website/docs/api/docusaurus.config.js.md +++ b/website/docs/api/docusaurus.config.js.md @@ -284,18 +284,6 @@ module.exports = { defaultMode: 'light', disableSwitch: false, 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: { title: 'Site Title', diff --git a/website/docs/api/themes/theme-configuration.md b/website/docs/api/themes/theme-configuration.md index e79cf89614cf..108aa6aa78d3 100644 --- a/website/docs/api/themes/theme-configuration.md +++ b/website/docs/api/themes/theme-configuration.md @@ -28,11 +28,6 @@ Accepted fields: | `defaultMode` | 'light' \| 'dark' | `'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. | | `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. | @@ -46,18 +41,6 @@ module.exports = { defaultMode: 'light', disableSwitch: 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 }, diff --git a/website/src/theme/Toggle.tsx b/website/src/theme/ColorModeToggle.tsx similarity index 87% rename from website/src/theme/Toggle.tsx rename to website/src/theme/ColorModeToggle.tsx index 2a0b13d598d7..d0e08f7871e9 100644 --- a/website/src/theme/Toggle.tsx +++ b/website/src/theme/ColorModeToggle.tsx @@ -6,8 +6,8 @@ */ import React from 'react'; -import OriginalToggle from '@theme-original/Toggle'; -import type {Props} from '@theme/Toggle'; +import OriginalToggle from '@theme-original/ColorModeToggle'; +import type {Props} from '@theme/ColorModeToggle'; import { lightStorage, darkStorage, @@ -24,7 +24,7 @@ import { // 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 // by hooking into the Toggle component. -export default function Toggle(props: Props): JSX.Element { +export default function ColorModeToggle(props: Props): JSX.Element { return (