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

fix(v2): refactor color mode system #3012

Merged
merged 11 commits into from
Jun 30, 2020
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ packages/docusaurus-plugin-content-pages/lib/
packages/docusaurus-plugin-debug/lib/
packages/docusaurus-plugin-sitemap/lib/
packages/docusaurus-plugin-ideal-image/lib/
packages/docusaurus-theme-classic/lib/
__fixtures__
62 changes: 41 additions & 21 deletions packages/docusaurus-theme-classic/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,46 @@ const ContextReplacementPlugin = requireFromDocusaurusCore(
// Need to be inlined to prevent dark mode FOUC
// Make sure that the 'storageKey' is the same as the one in `/theme/hooks/useTheme.js`
const storageKey = 'theme';
const noFlash = (defaultDarkMode) => `(function() {
var defaultDarkMode = ${defaultDarkMode};
const noFlashColorMode = ({defaultMode, respectPrefersColorScheme}) => {
return `(function() {
var defaultMode = '${defaultMode}';
var respectPrefersColorScheme = ${respectPrefersColorScheme};

function setDataThemeAttribute(theme) {
document.documentElement.setAttribute('data-theme', theme);
}

function getPreferredTheme() {
function getStoredTheme() {
var theme = null;
try {
theme = localStorage.getItem('${storageKey}');
} catch (err) {}

return theme;
}

var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');

var preferredTheme = getPreferredTheme();
if (preferredTheme !== null) {
setDataThemeAttribute(preferredTheme);
} else if (darkQuery.matches || defaultDarkMode) {
setDataThemeAttribute('dark');
var storedTheme = getStoredTheme();
if (storedTheme !== null) {
slorber marked this conversation as resolved.
Show resolved Hide resolved
setDataThemeAttribute(storedTheme);
}
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');
}
}
})();`;
};

module.exports = function (context, options) {
const {
siteConfig: {themeConfig},
} = context;
const {
disableDarkMode = false,
defaultDarkMode = false,
prism: {additionalLanguages = []} = {},
} = themeConfig || {};
const {colorMode, prism: {additionalLanguages = []} = {}} = themeConfig || {};
const {customCss} = options || {};

return {
Expand Down Expand Up @@ -97,17 +102,14 @@ module.exports = function (context, options) {
},

injectHtmlTags() {
if (disableDarkMode) {
return {};
}
return {
preBodyTags: [
{
tagName: 'script',
attributes: {
type: 'text/javascript',
},
innerHTML: noFlash(defaultDarkMode),
innerHTML: noFlashColorMode(colorMode),
},
],
};
Expand All @@ -130,8 +132,26 @@ const NavbarLinkSchema = Joi.object({
.xor('href', 'to')
.id('navbarLinkSchema');

const ColorModeSchema = Joi.object({
defaultMode: Joi.string().equal('dark', 'light').default('light'),
disableSwitch: Joi.bool().default(false),
respectPrefersColorScheme: Joi.bool().default(false),
}).default({
defaultMode: 'light',
disableSwitch: false,
respectPrefersColorScheme: false,
});

const ThemeConfigSchema = Joi.object({
disableDarkMode: Joi.bool().default(false),
disableDarkMode: Joi.any().forbidden(false).messages({
'any.unknown':
'disableDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.disableSwitch = true',
}),
defaultDarkMode: Joi.any().forbidden(false).messages({
'any.unknown':
'defaultDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.defaultMode = "dark"',
}),
colorMode: ColorModeSchema,
image: Joi.string(),
announcementBar: Joi.object({
id: Joi.string(),
Expand Down
6 changes: 3 additions & 3 deletions packages/docusaurus-theme-classic/src/theme/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ function Navbar(): JSX.Element {
siteConfig: {
themeConfig: {
navbar: {title = '', links = [], hideOnScroll = false} = {},
disableDarkMode = false,
colorMode: {disableSwitch: disableColorModeSwitch = false} = {},
},
},
isClient,
Expand Down Expand Up @@ -283,7 +283,7 @@ function Navbar(): JSX.Element {
{rightLinks.map((linkItem, i) => (
<NavItem {...linkItem} key={i} />
))}
{!disableDarkMode && (
{!disableColorModeSwitch && (
<Toggle
className={styles.displayOnlyInLargeViewport}
aria-label="Dark mode toggle"
Expand Down Expand Up @@ -321,7 +321,7 @@ function Navbar(): JSX.Element {
<strong className="navbar__title">{title}</strong>
)}
</Link>
{!disableDarkMode && sidebarShown && (
{!disableColorModeSwitch && sidebarShown && (
<Toggle
aria-label="Dark mode toggle in sidebar"
checked={isDarkTheme}
Expand Down
56 changes: 32 additions & 24 deletions packages/docusaurus-theme-classic/src/theme/hooks/useTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,64 +10,72 @@ import {useState, useCallback, useEffect} from 'react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';

const themes = {
light: '',
light: 'light',
dark: 'dark',
};

// Ensure to always return a valid theme even if input is invalid
const coerceToTheme = (theme) => {
return theme === themes.dark ? themes.dark : themes.light;
};

const getInitialTheme = () => {
if (typeof document === 'undefined') {
slorber marked this conversation as resolved.
Show resolved Hide resolved
return themes.light; // SSR: we don't care
}
return coerceToTheme(document.documentElement.getAttribute('data-theme'));
};

const storeTheme = (newTheme) => {
try {
localStorage.setItem('theme', coerceToTheme(newTheme));
} catch (err) {
console.error(err);
}
};

const useTheme = (): {
isDarkTheme: boolean;
setLightTheme: () => void;
setDarkTheme: () => void;
} => {
const {
siteConfig: {themeConfig: {disableDarkMode = false} = {}} = {},
siteConfig: {
themeConfig: {colorMode: {disableSwitch = false} = {}} = {},
} = {},
} = useDocusaurusContext();
const [theme, setTheme] = useState(
typeof document !== 'undefined'
? document.documentElement.getAttribute('data-theme')
: themes.light,
);
const setThemeSyncWithLocalStorage = useCallback(
(newTheme) => {
try {
localStorage.setItem('theme', newTheme);
} catch (err) {
console.error(err);
}
},
[setTheme],
);
const [theme, setTheme] = useState(getInitialTheme);

const setLightTheme = useCallback(() => {
setTheme(themes.light);
setThemeSyncWithLocalStorage(themes.light);
storeTheme(themes.light);
}, []);
const setDarkTheme = useCallback(() => {
setTheme(themes.dark);
setThemeSyncWithLocalStorage(themes.dark);
storeTheme(themes.dark);
}, []);

useEffect(() => {
// @ts-expect-error: safe to set null as attribute
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.setAttribute('data-theme', coerceToTheme(theme));
}, [theme]);

useEffect(() => {
if (disableDarkMode) {
if (disableSwitch) {
return;
}

try {
const localStorageTheme = localStorage.getItem('theme');
if (localStorageTheme !== null) {
setTheme(localStorageTheme);
setTheme(coerceToTheme(localStorageTheme));
}
} catch (err) {
console.error(err);
}
}, [setTheme]);

useEffect(() => {
if (disableDarkMode) {
if (disableSwitch) {
return;
}

Expand Down
11 changes: 8 additions & 3 deletions packages/docusaurus/src/server/plugins/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ export default function initPlugins({
pluginModule.default?.validateOptions ?? pluginModule.validateOptions;

if (validateOptions) {
const options = validateOptions({
const normalizedOptions = validateOptions({
validate,
options: pluginOptions,
});
pluginOptions = options;
pluginOptions = normalizedOptions;
}

// support both commonjs and ES modules
Expand All @@ -96,10 +96,15 @@ export default function initPlugins({
pluginModule.validateThemeConfig;

if (validateThemeConfig) {
validateThemeConfig({
const normalizedThemeConfig = validateThemeConfig({
validate: validateAndStrip,
themeConfig: context.siteConfig.themeConfig,
});

context.siteConfig.themeConfig = {
...context.siteConfig.themeConfig,
...normalizedThemeConfig,
};
}
return plugin(context, pluginOptions);
})
Expand Down
12 changes: 7 additions & 5 deletions website/docs/lifecycle-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,19 @@ export function validateOptions({options, validate}) {

## `validateThemeConfig({themeConfig,validate})`

Validate `themeConfig` for the plugins and theme. This method is called before the plugin is initialized.
Return validated and normalized configuration for the theme.

### `themeConfig`

`validateThemeConfig` is called with `themeConfig` provided in `docusaurus.config.js` for validation.
`validateThemeConfig` is called with `themeConfig` provided in `docusaurus.config.js` for validation and normalization.

### `validate`

`validateThemeConfig` is called with `validate` function which takes a **[Joi](https://www.npmjs.com/package/@hapi/joi)** schema and `themeConfig` as argument, returns validated and normalized options. `validate` will automatically handle error and validation config.

:::tip

[Joi](https://www.npmjs.com/package/@hapi/joi) is recommended for validation and normalization of options.
[Joi](https://www.npmjs.com/package/@hapi/joi) is recommended for validation and normalization of theme config.

:::

Expand All @@ -90,7 +90,8 @@ module.exports = function (context, options) {
};

module.exports.validateThemeConfig = ({themeConfig, validate}) => {
validate(myValidationSchema, options);
const validatedThemeConfig = validate(myValidationSchema, options);
return validatedThemeConfig;
};
```

Expand All @@ -105,7 +106,8 @@ export default function (context, options) {
}

export function validateThemeConfig({themeConfig, validate}) {
validate(myValidationSchema, options);
const validatedThemeConfig = validate(myValidationSchema, options);
return validatedThemeConfig;
}
```

Expand Down
37 changes: 24 additions & 13 deletions website/docs/theme-classic.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,43 @@ This section is a work in progress.

## Common

### Dark mode
### Color mode - dark mode

To remove the ability to switch on dark mode, there is an option `themeConfig.disableDarkMode`, which is implicitly set to `false`.
The classic theme provides by default light and dark mode support, with a navbar switch for the user.

```js {4} title="docusaurus.config.js"
It is possible to customize the color mode support with the following configuration:

```js {6-15} title="docusaurus.config.js"
module.exports = {
// ...
themeConfig: {
disableDarkMode: false,
// ...
},
};
```
colorMode: {
// "light" | "dark"
defaultMode: 'light',

With the enabled `defaultDarkMode` option you could set dark mode by default. However, in this case, the user's preference will not be taken into account until they manually sets the desired mode via toggle in the navbar.
// Hides the switch in the navbar
// Useful if you want to support a single color mode
disableSwitch: false,

```js {4} title="docusaurus.config.js"
module.exports = {
// ...
themeConfig: {
defaultDarkMode: true,
// Should we use the prefers-color-scheme media-query,
// using user system preferences, instead of the hardcoded defaultMode
respectPrefersColorScheme: false,
},
// ...
},
// ...
};
```

:::caution

With `respectPrefersColorScheme: true`, the `defaultMode` is overriden by user system preferences.
slorber marked this conversation as resolved.
Show resolved Hide resolved

If you only want to support one color mode, you likely want to ignore user system preferences.

:::

### Meta image

You can configure a default image that will be used for your meta tag, in particular `og:image` and `twitter:image`.
Expand Down
5 changes: 5 additions & 0 deletions website/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ module.exports = {
],
],
themeConfig: {
colorMode: {
defaultMode: 'light',
disableSwitch: false,
respectPrefersColorScheme: true,
},
announcementBar: {
id: 'supportus',
content:
Expand Down