Skip to content

Commit

Permalink
feat(theme): allow reverting to auto theme mode
Browse files Browse the repository at this point in the history
  • Loading branch information
nasso committed Dec 22, 2022
1 parent b064311 commit 00f67ba
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,6 @@ describe('themeConfig', () => {
const colorMode: ThemeConfig['colorMode'] = {
defaultMode: 'dark',
disableSwitch: false,
respectPrefersColorScheme: true,
};
expect(testValidateThemeConfig({colorMode})).toEqual({
...DEFAULT_CONFIG,
Expand Down
36 changes: 15 additions & 21 deletions packages/docusaurus-theme-classic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,14 @@ const ContextReplacementPlugin = requireFromDocusaurusCore(
// Need to be inlined to prevent dark mode FOUC
// Make sure the key is the same as the one in `/theme/hooks/useTheme.js`
const ThemeStorageKey = 'theme';
const noFlashColorMode = ({
defaultMode,
respectPrefersColorScheme,
}: ThemeConfig['colorMode']) =>
const noFlashColorMode = ({defaultMode}: ThemeConfig['colorMode']) =>
/* language=js */
`(function() {
var defaultMode = '${defaultMode}';
var respectPrefersColorScheme = ${respectPrefersColorScheme};
function setDataThemeAttribute(theme) {
function setDataThemeAttributes(theme, choice) {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.setAttribute('data-theme-choice', choice);
}
function getStoredTheme() {
Expand All @@ -48,22 +45,19 @@ const noFlashColorMode = ({
}
var storedTheme = getStoredTheme();
if (storedTheme !== null) {
setDataThemeAttribute(storedTheme);
var mode = storedTheme === null ? defaultMode : storedTheme;
if (
mode === 'auto' &&
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
setDataThemeAttributes('dark', mode);
} else if (
mode === 'auto' &&
window.matchMedia('(prefers-color-scheme: light)').matches
) {
setDataThemeAttributes('light', mode);
} else {
if (
respectPrefersColorScheme &&
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
setDataThemeAttribute('dark');
} else if (
respectPrefersColorScheme &&
window.matchMedia('(prefers-color-scheme: light)').matches
) {
setDataThemeAttribute('light');
} else {
setDataThemeAttribute(defaultMode === 'dark' ? 'dark' : 'light');
}
setDataThemeAttributes(mode === 'dark' ? 'dark' : 'light', mode);
}
})();`;

Expand Down
10 changes: 5 additions & 5 deletions packages/docusaurus-theme-classic/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const DocsSchema = Joi.object({
const DEFAULT_COLOR_MODE_CONFIG: ThemeConfig['colorMode'] = {
defaultMode: 'light',
disableSwitch: false,
respectPrefersColorScheme: false,
};

export const DEFAULT_CONFIG: ThemeConfig = {
Expand Down Expand Up @@ -266,12 +265,13 @@ const NavbarItemSchema = Joi.object({

const ColorModeSchema = Joi.object({
defaultMode: Joi.string()
.equal('dark', 'light')
.equal('auto', 'dark', 'light')
.default(DEFAULT_COLOR_MODE_CONFIG.defaultMode),
disableSwitch: Joi.bool().default(DEFAULT_COLOR_MODE_CONFIG.disableSwitch),
respectPrefersColorScheme: Joi.bool().default(
DEFAULT_COLOR_MODE_CONFIG.respectPrefersColorScheme,
),
respectPrefersColorScheme: Joi.any().forbidden().messages({
'any.unknown':
'colorMode.respectPrefersColorScheme is deprecated. Please use colorMode.defaultMode=auto instead.',
}),
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.',
Expand Down
13 changes: 11 additions & 2 deletions packages/docusaurus-theme-classic/src/theme-classic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1370,16 +1370,17 @@ declare module '@theme/TOCCollapsible/CollapseButton' {
}

declare module '@theme/ColorModeToggle' {
import type {ColorMode} from '@docusaurus/theme-common';
import type {ColorMode, ColorModeChoice} from '@docusaurus/theme-common';

export interface Props {
readonly className?: string;
readonly value: ColorMode;
readonly choice: ColorModeChoice;
/**
* The parameter represents the "to-be" value. For example, if currently in
* dark mode, clicking the button should call `onChange("light")`
*/
readonly onChange: (colorMode: ColorMode) => void;
readonly onChange: (colorModeChoice: ColorModeChoice) => void;
}

export default function ColorModeToggle(props: Props): JSX.Element;
Expand All @@ -1404,6 +1405,14 @@ declare module '@theme/Icon/Arrow' {
export default function IconArrow(props: Props): JSX.Element;
}

declare module '@theme/Icon/AutoThemeMode' {
import type {ComponentProps} from 'react';

export interface Props extends ComponentProps<'svg'> {}

export default function IconAutoThemeMode(props: Props): JSX.Element;
}

declare module '@theme/Icon/DarkMode' {
import type {ComponentProps} from 'react';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ import useIsBrowser from '@docusaurus/useIsBrowser';
import {translate} from '@docusaurus/Translate';
import IconLightMode from '@theme/Icon/LightMode';
import IconDarkMode from '@theme/Icon/DarkMode';
import IconAutoThemeMode from '@theme/Icon/AutoThemeMode';
import type {Props} from '@theme/ColorModeToggle';
import type {ColorModeChoice} from '@docusaurus/theme-common';

import styles from './styles.module.css';

function ColorModeToggle({className, value, onChange}: Props): JSX.Element {
function ColorModeToggle({
className,
value,
choice,
onChange,
}: Props): JSX.Element {
const isBrowser = useIsBrowser();

const title = translate(
Expand All @@ -25,21 +32,49 @@ function ColorModeToggle({className, value, onChange}: Props): JSX.Element {
description: 'The ARIA label for the navbar color mode toggle',
},
{
mode:
value === 'dark'
? 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',
}),
mode: {
dark: () =>
translate({
message: 'dark mode',
id: 'theme.colorToggle.ariaLabel.mode.dark',
description: 'The name for the dark color mode',
}),
light: () =>
translate({
message: 'light mode',
id: 'theme.colorToggle.ariaLabel.mode.light',
description: 'The name for the light color mode',
}),
auto: () =>
translate({
message: 'auto mode',
id: 'theme.colorToggle.ariaLabel.mode.auto',
description: 'The name for the auto color mode',
}),
}[choice ?? 'auto'](),
},
);

// cycle through dark/light/auto, as follows:
//
// (prefers-color-scheme: dark)
// ? [auto, light, dark]
// : [auto, dark, light]
const nextTheme = (): ColorModeChoice => {
// auto -> opposite
if (choice === 'auto') {
return value === 'dark' ? 'light' : 'dark';
}

// same as `prefers-color-scheme` -> auto
if (window.matchMedia(`(prefers-color-scheme: ${choice})`).matches) {
return 'auto';
}

// dark/light -> opposite
return choice === 'dark' ? 'light' : 'dark';
};

return (
<div className={clsx(styles.toggle, className)}>
<button
Expand All @@ -49,7 +84,7 @@ function ColorModeToggle({className, value, onChange}: Props): JSX.Element {
!isBrowser && styles.toggleButtonDisabled,
)}
type="button"
onClick={() => onChange(value === 'dark' ? 'light' : 'dark')}
onClick={() => onChange(nextTheme())}
disabled={!isBrowser}
title={title}
aria-label={title}
Expand All @@ -60,6 +95,9 @@ function ColorModeToggle({className, value, onChange}: Props): JSX.Element {
<IconDarkMode
className={clsx(styles.toggleIcon, styles.darkToggleIcon)}
/>
<IconAutoThemeMode
className={clsx(styles.toggleIcon, styles.autoToggleIcon)}
/>
</button>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@
background: var(--ifm-color-emphasis-200);
}

[data-theme='light'] .darkToggleIcon,
[data-theme='dark'] .lightToggleIcon {
[data-theme-choice='auto'] .lightToggleIcon,
[data-theme-choice='auto'] .darkToggleIcon,
[data-theme-choice='dark'] .autoToggleIcon,
[data-theme-choice='dark'] .lightToggleIcon,
[data-theme-choice='light'] .autoToggleIcon,
[data-theme-choice='light'] .darkToggleIcon {
display: none;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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/Icon/AutoThemeMode';

export default function IconAutoThemeMode(props: Props): JSX.Element {
return (
<svg viewBox="0 0 24 24" width={24} height={24} {...props}>
<path
fill="currentColor"
d="m12 21c4.971 0 9-4.029 9-9s-4.029-9-9-9-9 4.029-9 9 4.029 9 9 9zm4.95-13.95c1.313 1.313 2.05 3.093 2.05 4.95s-0.738 3.637-2.05 4.95c-1.313 1.313-3.093 2.05-4.95 2.05v-14c1.857 0 3.637 0.737 4.95 2.05z"
/>
</svg>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function NavbarColorModeToggle({
className,
}: Props): JSX.Element | null {
const disabled = useThemeConfig().colorMode.disableSwitch;
const {colorMode, setColorMode} = useColorMode();
const {colorMode, colorModeChoice, setColorMode} = useColorMode();

if (disabled) {
return null;
Expand All @@ -24,6 +24,7 @@ export default function NavbarColorModeToggle({
<ColorModeToggle
className={className}
value={colorMode}
choice={colorModeChoice}
onChange={setColorMode}
/>
);
Expand Down
Loading

0 comments on commit 00f67ba

Please sign in to comment.