diff --git a/docs/src/pages/configurations/theming/index.md b/docs/src/pages/configurations/theming/index.md index 7d1fdf09b3b..70be2f54788 100644 --- a/docs/src/pages/configurations/theming/index.md +++ b/docs/src/pages/configurations/theming/index.md @@ -9,7 +9,7 @@ Storybook is theme-able! Just set a `theme` in the [options parameter](../option It's really easy to theme Storybook globally. -We've created two basic themes that look good of the box: "normal" (a light theme) and "dark" (a dark theme). +We've created two basic themes that look good of the box: "normal" (a light theme) and "dark" (a dark theme). Unless you've set your preferred color scheme as dark Storybook will use the light theme as default. As the simplest example, you can tell Storybook to use the "dark" theme by modifying `.storybook/config.js`: diff --git a/lib/core/src/client/preview/start.test.js b/lib/core/src/client/preview/start.test.js index 074ebcda5bc..47fda139b91 100644 --- a/lib/core/src/client/preview/start.test.js +++ b/lib/core/src/client/preview/start.test.js @@ -13,6 +13,7 @@ jest.mock('global', () => ({ addEventListener: jest.fn(), location: { search: '' }, history: { replaceState: jest.fn() }, + matchMedia: jest.fn().mockReturnValue({ matches: false }), }, document: { addEventListener: jest.fn(), diff --git a/lib/core/src/typings.d.ts b/lib/core/src/typings.d.ts new file mode 100644 index 00000000000..2f4eb9cf4fd --- /dev/null +++ b/lib/core/src/typings.d.ts @@ -0,0 +1 @@ +declare module 'global'; diff --git a/lib/core/tsconfig.json b/lib/core/tsconfig.json new file mode 100644 index 00000000000..a24ec639386 --- /dev/null +++ b/lib/core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["src/**.test.ts"] +} diff --git a/lib/theming/src/convert.ts b/lib/theming/src/convert.ts index 8496856213b..c35030a6ae1 100644 --- a/lib/theming/src/convert.ts +++ b/lib/theming/src/convert.ts @@ -4,8 +4,8 @@ import { background, typography, color } from './base'; import { Theme, Color, ThemeVars } from './types'; import { easing, animation } from './animation'; import { create as createSyntax, chromeLight, chromeDark } from './modules/syntax'; - -import lightThemeVars from './themes/light'; +import { getPreferredColorScheme } from './utils'; +import { themes } from './create'; const lightSyntaxColors = { green1: '#008000', @@ -72,7 +72,7 @@ const createColors = (vars: ThemeVars): Color => ({ inverseText: vars.textInverseColor || color.lightest, }); -export const convert = (inherit: ThemeVars = lightThemeVars): Theme => { +export const convert = (inherit: ThemeVars = themes[getPreferredColorScheme()]): Theme => { const { base, colorPrimary, diff --git a/lib/theming/src/create.ts b/lib/theming/src/create.ts index 2693e9f8fac..48ca51df8f6 100644 --- a/lib/theming/src/create.ts +++ b/lib/theming/src/create.ts @@ -3,6 +3,7 @@ import lightThemeVars from './themes/light'; import darkThemeVars from './themes/dark'; import { ThemeVars } from './types'; +import { getPreferredColorScheme } from './utils'; export const themes: { light: ThemeVars; dark: ThemeVars; normal: ThemeVars } = { light: lightThemeVars, @@ -14,12 +15,17 @@ interface Rest { [key: string]: any; } -export const create = (vars: ThemeVars = { base: 'light' }, rest?: Rest): ThemeVars => { +const preferredColorScheme = getPreferredColorScheme(); + +export const create = ( + vars: ThemeVars = { base: preferredColorScheme }, + rest?: Rest +): ThemeVars => { const inherit: ThemeVars = { - ...themes.light, + ...themes[preferredColorScheme], ...(themes[vars.base] || {}), ...vars, - ...{ base: themes[vars.base] ? vars.base : 'light' }, + ...{ base: themes[vars.base] ? vars.base : preferredColorScheme }, }; return { ...rest, diff --git a/lib/theming/src/tests/util.test.js b/lib/theming/src/tests/util.test.js index d2e6f57ffce..eb65701bf9e 100644 --- a/lib/theming/src/tests/util.test.js +++ b/lib/theming/src/tests/util.test.js @@ -1,4 +1,5 @@ -import { lightenColor as lighten, darkenColor as darken } from '../utils'; +import { window } from 'global'; +import { lightenColor as lighten, darkenColor as darken, getPreferredColorScheme } from '../utils'; describe('utils', () => { it('should apply polished when valid arguments are passed', () => { @@ -74,4 +75,34 @@ describe('utils', () => { expect(result).toEqual(color); }); + + describe('getPreferredColorScheme', () => { + it('should return "light" if "window" is unavailable', () => { + jest.mock('global', () => ({ window: undefined })); + + const colorScheme = getPreferredColorScheme(); + expect(colorScheme).toBe('light'); + }); + + it('should return "light" if the preferred color scheme is light or undefined', () => { + window.matchMedia = jest.fn().mockImplementation(() => ({ + matches: false, + })); + + const colorScheme = getPreferredColorScheme(); + expect(colorScheme).toBe('light'); + }); + + it('should return "dark" if the preferred color scheme is dark', () => { + // By setting matches to always be true any checks for prefer-color-scheme + // will match and since we only check if the preferred scheme is dark this + // is a simple way to test it + window.matchMedia = jest.fn().mockImplementation(() => ({ + matches: true, + })); + + const colorScheme = getPreferredColorScheme(); + expect(colorScheme).toBe('dark'); + }); + }); }); diff --git a/lib/theming/src/typings.d.ts b/lib/theming/src/typings.d.ts index 151bb56deda..ab568e12c7a 100644 --- a/lib/theming/src/typings.d.ts +++ b/lib/theming/src/typings.d.ts @@ -1,2 +1,3 @@ // todo the following packages need definition files or a TS migration declare module 'react-inspector'; +declare module 'global'; diff --git a/lib/theming/src/utils.ts b/lib/theming/src/utils.ts index 445ce2f683d..91732a17901 100644 --- a/lib/theming/src/utils.ts +++ b/lib/theming/src/utils.ts @@ -1,4 +1,5 @@ import { rgba, lighten, darken } from 'polished'; +import { window } from 'global'; import { logger } from '@storybook/client-logger'; @@ -57,3 +58,14 @@ const colorFactory = (type: string) => (color: string) => { export const lightenColor = colorFactory('lighten'); export const darkenColor = colorFactory('darken'); + +// The default color scheme is light so unless the preferred color +// scheme is set to dark we always want to use the light theme +export const getPreferredColorScheme = () => { + if (!window || !window.matchMedia) return 'light'; + + const isDarkThemePreferred = window.matchMedia('(prefers-color-scheme: dark)').matches; + if (isDarkThemePreferred) return 'dark'; + + return 'light'; +}; diff --git a/scripts/jest.init.js b/scripts/jest.init.js index 29ef6dfe513..9d4d516ecaf 100644 --- a/scripts/jest.init.js +++ b/scripts/jest.init.js @@ -51,3 +51,17 @@ const throwError = message => throwMessage('error: ', message); global.console.error = throwError; global.console.warn = throwWarning; + +// Mock for matchMedia since it's not yet implemented in JSDOM (https://jestjs.io/docs/en/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom) +global.window.matchMedia = jest.fn().mockImplementation(query => { + return { + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + }; +});