Skip to content

Commit

Permalink
Merge pull request #98 from hipstersmoothie/current
Browse files Browse the repository at this point in the history
respect current param if it's set
  • Loading branch information
hipstersmoothie authored Apr 15, 2020
2 parents 6800aba + 234a372 commit 1077dbe
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 40 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ addParameters({
});
```

### Default Theme

Order of precedence for the initial color scheme:

1. If the user has previously set a color theme it's used
2. The value you have configured for `current` parameter in your storybook
3. The OS color scheme preference

Once the initial color scheme has been set, subsequent reloads will use this value.
To clear the cached color scheme you have to `localStorage.clear()` in the chrome console.

```js
import { addParameters } from '@storybook/react';

addParameters({
darkMode: {
// Set the initial theme
current: 'light'
}
});
```

## Story integration

If your components use a custom Theme provider, you can integrate it by using the provided hook.
Expand Down
126 changes: 86 additions & 40 deletions src/Tool.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
import * as React from 'react';
import { themes, ThemeVars } from '@storybook/theming';
import { IconButton } from '@storybook/components';
import {
STORY_CHANGED,
STORIES_CONFIGURED,
DOCS_RENDERED
} from '@storybook/core-events';
import { API, useParameter } from '@storybook/api';
import equal from 'fast-deep-equal';
import { DARK_MODE_EVENT_NAME } from './constants';

import Sun from './icons/Sun';
import Moon from './icons/Moon';

interface DarkModeProps {
api: API;
}
const modes = ['light', 'dark'] as const;
type Mode = typeof modes[number];

interface DarkModeStore {
current: 'dark' | 'light';
/** The current mode the storybook is set to */
current: Mode;
/** The dark theme for storybook */
dark: ThemeVars;
/** The light theme for storybook */
light: ThemeVars;
}

const STORAGE_KEY = 'sb-addon-themes-3';
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');

const defaultStore: DarkModeStore = {
current: prefersDark.matches ? 'dark' : 'light',
const defaultParams: Partial<DarkModeStore> = {
dark: themes.dark,
light: themes.light
};

/** Persist the dark mode settings in localStorage */
const update = (newStore: DarkModeStore) => {
window.localStorage.setItem('sb-addon-themes-3', JSON.stringify(newStore));
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(newStore));
};

/** Update changed dark mode settings and persist to localStorage */
const store = (themes: Partial<DarkModeStore> = {}): DarkModeStore => {
const storedItem = window.localStorage.getItem('sb-addon-themes-3');
const storedItem = window.localStorage.getItem(STORAGE_KEY);

if (typeof storedItem === 'string') {
const stored: DarkModeStore = JSON.parse(storedItem);

Expand All @@ -50,67 +60,103 @@ const store = (themes: Partial<DarkModeStore> = {}): DarkModeStore => {
return stored;
}

return { ...defaultStore, ...themes };
return { ...defaultParams, ...themes } as DarkModeStore;
};

export const DarkMode: React.FunctionComponent<DarkModeProps> = props => {
interface DarkModeProps {
/** The storybook API */
api: API;
}

/** A toolbar icon to toggle between dark and light themes in storybook */
export const DarkMode = ({ api }: DarkModeProps) => {
const [isDark, setDark] = React.useState(prefersDark.matches);
const params = useParameter('darkMode', {
const { current: defaultMode, ...params } = useParameter<
Partial<DarkModeStore>
>('darkMode', {
dark: themes.dark,
light: themes.light
});
// const lastMode = React.useRef(defaultMode);

// Save custom themes on init
store(params);
function setMode(mode?: 'dark' | 'light') {
const currentStore = store(params);
const current =
mode || (currentStore.current === 'dark' ? 'light' : 'dark');

update({
...currentStore,
current
});
props.api.setOptions({ theme: currentStore[current] });
setDark(!isDark);
props.api.getChannel().emit(DARK_MODE_EVENT_NAME, !isDark);
}
const initialMode = React.useRef(store(params).current);

/** Set the theme in storybook, update the local state, and emit an event */
const setMode = React.useCallback(
(mode: Mode) => {
const currentStore = store();
console.log('set', mode);
api.setOptions({ theme: currentStore[mode] });
setDark(mode === 'dark');
api.getChannel().emit(DARK_MODE_EVENT_NAME, mode === 'dark');
},
[api]
);

/** Update the theme settings in localStorage, react, and storybook */
const updateMode = React.useCallback(
(mode?: Mode) => {
const currentStore = store();
const current =
mode || (currentStore.current === 'dark' ? 'light' : 'dark');

update({ ...currentStore, current });
setMode(current);
},
[setMode]
);

/** Update the theme based on the color preference */
function prefersDarkUpdate(event: MediaQueryListEvent) {
setMode(event.matches ? 'dark' : 'light');
updateMode(event.matches ? 'dark' : 'light');
}

/** Render the current theme */
function renderTheme() {
const currentStore = store(params);
const { current } = currentStore;

props.api.setOptions({ theme: currentStore[current] });
setDark(current === 'dark');
props.api.getChannel().emit(DARK_MODE_EVENT_NAME, current === 'dark');
const { current } = store();
setMode(current);
}

React.useEffect(() => {
const channel = props.api.getChannel();
channel.on('storyChanged', renderTheme);
channel.on('storiesConfigured', renderTheme);
channel.on('docsRendered', renderTheme);
const channel = api.getChannel();

channel.on(STORY_CHANGED, renderTheme);
channel.on(STORIES_CONFIGURED, renderTheme);
channel.on(DOCS_RENDERED, renderTheme);
prefersDark.addListener(prefersDarkUpdate);

return () => {
channel.removeListener('storyChanged', renderTheme);
channel.removeListener('storiesConfigured', renderTheme);
channel.removeListener('docsRendered', renderTheme);
channel.removeListener(STORY_CHANGED, renderTheme);
channel.removeListener(STORIES_CONFIGURED, renderTheme);
channel.removeListener(DOCS_RENDERED, renderTheme);
prefersDark.removeListener(prefersDarkUpdate);
};
});

// Storybook's first render doesn't have the global user params loaded so we
// need the effect to run whenever defaultMode is updated
React.useEffect(() => {
// If a users has set the mode this is respected
if (initialMode.current) {
return;
}

if (defaultMode) {
updateMode(defaultMode);
} else if (prefersDark.matches) {
updateMode('dark');
}
}, [defaultMode, updateMode]);

return (
<IconButton
key="dark-mode"
active={isDark}
title={
isDark ? 'Change theme to light mode' : 'Change theme to dark mode'
}
onClick={() => setMode()}
onClick={() => updateMode()}
>
{isDark ? <Sun /> : <Moon />}
</IconButton>
Expand Down

0 comments on commit 1077dbe

Please sign in to comment.