Skip to content

Commit

Permalink
feat(ThemeProvider): Added ability for ThemeProvider to be nestable, …
Browse files Browse the repository at this point in the history
…and renamed initialTheme prop to defaultTheme

BREAKING CHANGE: Reanmed ThemeProvider initialTheme prop to defaultTheme
  • Loading branch information
HHogg committed Dec 22, 2023
1 parent 7a83489 commit 2b7a8fd
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 20 deletions.
62 changes: 48 additions & 14 deletions workspaces/package/src/ThemeSwitcher/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { PropsWithChildren, useEffect, useRef } from 'react';
import {
PropsWithChildren,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import useLocalStorage from '../hooks/useLocalStorage';
import { TypeTheme } from '../types';
import { themes, themesOpposite } from '../variables';
import { useSystemTheme } from './useSystemTheme';
import { ThemeContext } from './useThemeContext';
import { ThemeContext, useThemeContext } from './useThemeContext';

type ThemeProviderProps = {
disableSystemTheme?: boolean;
initialTheme?: TypeTheme;
defaultTheme?: TypeTheme;
theme?: TypeTheme;
};

const updateThemeClassName = (theme: TypeTheme) => {
Expand All @@ -21,40 +28,67 @@ const updateThemeClassName = (theme: TypeTheme) => {
export function ThemeProvider({
children,
disableSystemTheme = false,
initialTheme = 'day',
defaultTheme = 'day',
theme: propsTheme,
}: PropsWithChildren<ThemeProviderProps>) {
const [theme, setTheme] = useLocalStorage<TypeTheme>(
const { themeRoot } = useThemeContext();
const [localStateTheme, setLocalStateTheme] = useState<TypeTheme>(
propsTheme || defaultTheme
);

const [localStorageTheme, setLocalStorageTheme] = useLocalStorage<TypeTheme>(
'preshape.theme',
initialTheme
defaultTheme
);

const refPreviousSystemTheme = useRef<TypeTheme | null>(null);
const systemTheme = useSystemTheme();

// When a ThemeProvider has a parent ThemeProvider, it should not
// update the theme based on the system theme, nor update the body
// class.
const isRootTheme = themeRoot === null;
const theme = isRootTheme ? localStorageTheme : localStateTheme;

const handleSetTheme = useCallback(
(theme: TypeTheme) => {
if (isRootTheme) {
setLocalStorageTheme(theme);
} else {
setLocalStateTheme(theme);
}
},
[isRootTheme, setLocalStateTheme, setLocalStorageTheme]
);

useEffect(() => {
if (!isRootTheme || disableSystemTheme) {
return;
}

if (
!refPreviousSystemTheme.current ||
refPreviousSystemTheme.current !== systemTheme
) {
refPreviousSystemTheme.current = systemTheme;

if (!disableSystemTheme) {
setTheme(systemTheme);
}
setLocalStorageTheme(systemTheme);
}
}, [setTheme, systemTheme, disableSystemTheme]);
}, [setLocalStorageTheme, isRootTheme, systemTheme, disableSystemTheme]);

useEffect(() => {
updateThemeClassName(theme);
}, [theme]);
if (isRootTheme) {
updateThemeClassName(localStorageTheme);
}
}, [isRootTheme, localStorageTheme]);

return (
<ThemeContext.Provider
value={{
colors: themes[theme],
onChange: setTheme,
onChange: handleSetTheme,
theme,
themeOpposite: themesOpposite[theme],
themeRoot: isRootTheme ? theme : themeRoot,
}}
>
{children}
Expand Down
2 changes: 2 additions & 0 deletions workspaces/package/src/ThemeSwitcher/useThemeContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ type ThemeContextProps = {
onChange: (theme: TypeTheme) => void;
theme: TypeTheme;
themeOpposite: TypeTheme;
themeRoot: TypeTheme | null;
};

export const ThemeContext = createContext<ThemeContextProps>({
colors: themeDay,
onChange: () => {},
theme: 'day',
themeOpposite: 'night',
themeRoot: null,
});

export const useThemeContext = () => {
Expand Down
42 changes: 36 additions & 6 deletions workspaces/site/src/pages/Themes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
TypeTheme,
themes,
useThemeContext,
ThemeProvider,

Check failure on line 8 in workspaces/site/src/pages/Themes.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

'ThemeProvider' is defined but never used

Check failure on line 8 in workspaces/site/src/pages/Themes.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

'ThemeProvider' is defined but never used
} from 'preshape';
import { ColorProps } from '../components/Color/Color';
import { ColorList } from '../components/Color/ColorList';
import { Page } from '../components/Page/Page';
import { PageSection } from '../components/Page/PageSection';
import { PageSubtitle } from '../components/Page/PageSubtitle';
import LogTheme from './LogTheme';

Check failure on line 15 in workspaces/site/src/pages/Themes.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

'LogTheme' is defined but never used

Check failure on line 15 in workspaces/site/src/pages/Themes.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

'LogTheme' is defined but never used

Check failure on line 15 in workspaces/site/src/pages/Themes.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

Unable to resolve path to module './LogTheme'

const paletteTheme = (theme: TypeTheme): ColorProps[] => [
{
Expand Down Expand Up @@ -152,27 +154,27 @@ export const ThemesPage = () => {
<CodeBlock language="tsx">{`
import { ThemeProvider } from 'preshape';
<ThemeProvider initialTheme="${theme}">
<ThemeProvider defaultTheme="day">
...
</ThemeProvider>
`}</CodeBlock>
</CodeWindow>

<Text>
Wrapping the entire application with the theme provider will allow
components to react to the theme. The theme provider accepts an{' '}
components to react to the theme. The theme provider accepts a{' '}
<Text tag="span" weight="x2">
initialTheme
defaultTheme
</Text>{' '}
prop, which can be either{' '}
<Text tag="span" weight="x2">
"Day"
"day"
</Text>{' '}
or{' '}
<Text tag="span" weight="x2">
"Night"
"night"
</Text>
. The provider also stored the theme in local storage, so that the
. The provider also stores the theme in local storage, so that the
theme can be persisted between page loads.
</Text>

Expand Down Expand Up @@ -203,6 +205,34 @@ console.log(colors); // { colorAccentShade1: ${colors.colorAccentShade1}, ... }
</Text>{' '}
component which can be used to toggle the theme.
</Text>

<CodeWindow>
<CodeBlock language="tsx">{`
<ThemeProvider theme="day">
<LogTheme /> // "day"
<ThemeProvider theme="night">
<LogTheme /> // "night"
</ThemeProvider>
</ThemeProvider>
`}</CodeBlock>
</CodeWindow>

<Text>
<Text tag="span" weight="x2">
ThemeProvider
</Text>{' '}
can be nested, and the closest{' '}
<Text tag="span" weight="x2">
ThemeProvider
</Text>{' '}
will be used by descendants, however the top most{' '}
<Text tag="span" weight="x2">
ThemeProvider
</Text>{' '}
theme will be used to control the overall application theme and listen
for system theme changes.
</Text>
</PageSection>

<PageSection>
Expand Down

0 comments on commit 2b7a8fd

Please sign in to comment.