UI: Preferred color scheme awareness (#8271)

UI: Preferred color scheme awareness
This commit is contained in:
Michael Shilman 2019-10-02 23:49:15 -07:00 committed by GitHub
commit 0eea02a46c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 82 additions and 8 deletions

View File

@ -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`:

View File

@ -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(),

1
lib/core/src/typings.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'global';

8
lib/core/tsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["src/**.test.ts"]
}

View File

@ -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,

View File

@ -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,

View File

@ -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');
});
});
});

View File

@ -1,2 +1,3 @@
// todo the following packages need definition files or a TS migration
declare module 'react-inspector';
declare module 'global';

View File

@ -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';
};

View File

@ -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(),
};
});