Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(theme): allow reverting to auto theme mode #8474

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
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
Expand Up @@ -1348,16 +1348,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 @@ -1382,6 +1383,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
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
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
@@ -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>
);
}
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