-
Notifications
You must be signed in to change notification settings - Fork 26
/
Copy paththeme-provider.tsx
109 lines (89 loc) · 3.19 KB
/
theme-provider.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import { useLayoutEffect, useState, useRef, useEffect } from 'react';
import kebabCase from 'lodash/kebabCase';
import isObject from 'lodash/isObject';
import merge from 'lodash/merge';
import isEqual from 'lodash/isEqual';
import { themes } from './design-tokens';
const allThemesNames = Object.keys(themes);
type ThemeName = keyof typeof themes;
const toVars = (obj: Record<string, string>) =>
Object.fromEntries(
Object.entries(obj).map(([key, value]) => [`--${kebabCase(key)}`, value])
);
// used to cover SSR builds (for instance in Gatsby)
const isBrowser = typeof window !== 'undefined';
const defaultParentSelector = (): HTMLElement | null =>
document.querySelector(':root');
type TApplyTheme = {
newTheme?: string;
parentSelector: typeof defaultParentSelector;
themeOverrides?: Record<string, string>;
};
const applyTheme = ({
newTheme,
parentSelector = defaultParentSelector,
themeOverrides,
}: TApplyTheme): void => {
const target = isBrowser ? parentSelector() : null;
// With no target we can't change themes
if (!target) return;
const validTheme = (
allThemesNames.includes(newTheme || '') ? newTheme! : 'default'
) as ThemeName;
if (newTheme && newTheme !== validTheme) {
console.warn(
`ThemeProvider: the specified theme '${newTheme}' is not supported.`
);
}
const vars = toVars(
themeOverrides && isObject(themeOverrides)
? merge({}, themes.default, themes[validTheme], themeOverrides)
: themes[validTheme]
);
Object.entries(vars).forEach(([key, value]) => {
target.style.setProperty(key, value);
});
target.setAttribute('data-theme', validTheme);
};
type ThemeProviderProps = {
parentSelector: typeof defaultParentSelector;
theme?: string;
themeOverrides?: Record<string, string>;
};
const ThemeProvider = (props: ThemeProviderProps) => {
const parentSelectorRef = useRef(props.parentSelector);
const themeNameRef = useRef<string>();
const themeOverridesRef = useRef<Record<string, string>>();
useLayoutEffect(() => {
// We want to make sure we don't really apply the change when the props
// provided include a new object with the same theme overrides
// (eg: users providing an inline object as prop to the ThemeProvider)
if (
themeNameRef.current !== props.theme ||
!isEqual(themeOverridesRef.current, props.themeOverrides)
) {
themeNameRef.current = props.theme;
themeOverridesRef.current = props.themeOverrides;
applyTheme({
newTheme: props.theme,
parentSelector: parentSelectorRef.current,
themeOverrides: props.themeOverrides,
});
}
}, [props.theme, props.themeOverrides]);
return null;
};
ThemeProvider.defaultProps = {
parentSelector: defaultParentSelector,
};
const useTheme = (parentSelector = defaultParentSelector) => {
const [theme, setTheme] = useState<string>('default');
const parentSelectorRef = useRef(parentSelector);
// If we use 'useLayoutEffect' here, we would be trying to read the
// data attribute before it gets set from the effect in the ThemeProvider
useEffect(() => {
setTheme(parentSelectorRef.current()?.dataset.theme || 'default');
}, []);
return theme;
};
export { ThemeProvider, useTheme };