Skip to content

Commit 0cf9b18

Browse files
author
Jed Mao
committed
feat: support classNames option
you can now supply class names to useLayoutEffect BREAKING CHANGE: useLayoutEffect now takes an options hash instead of a single optional element
1 parent c0ad1a5 commit 0cf9b18

File tree

3 files changed

+111
-29
lines changed

3 files changed

+111
-29
lines changed

README.md

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,55 @@ _The following example uses [TypeScript](http://www.typescriptlang.org/)._
1515

1616
- You only want to create a single theme context and reuse it throughout your
1717
application.
18-
- You can create as many themes as you want.
19-
- A `Theme` is expected to be of type:
20-
`Record<string, string | number | boolean>`.
18+
- You can create as many themes as you want, so long as they implement the same
19+
[interface](https://www.typescriptlang.org/docs/handbook/interfaces.html).
20+
21+
### Themes
22+
23+
At its simplest, a theme can store colors:
24+
25+
```ts
26+
export default interface Theme {
27+
errorColor: string
28+
anotherColor: string
29+
}
30+
```
31+
32+
Here's an example of a theme using this color:
33+
34+
```ts
35+
import Theme from 'models/Theme'
36+
37+
const colors = {
38+
scarlet: '#ff2400',
39+
white: 'white',
40+
}
41+
42+
const myTheme: Theme = {
43+
errorColor: colors.scarlet,
44+
anotherColor: colors.white,
45+
}
46+
47+
export default myTheme
48+
```
49+
50+
You might also want to base a color off of another:
51+
52+
```ts
53+
class MyTheme implements Theme {
54+
public errorColor = colors.scarlet
55+
public get anotherColor() {
56+
return darken(this.errorColor, 0.2)
57+
}
58+
}
59+
60+
export default new MyTheme()
61+
```
62+
63+
## Setup
64+
65+
Here's a contrived example of setting up an app with a couple of inline themes
66+
and creating a button to switch to the 2nd one.
2167

2268
### themeContext.tsx
2369

@@ -96,14 +142,18 @@ themeContext.prop('primaryColor')
96142
// 'var(--primary-color)'
97143
```
98144

99-
### `#useLayoutEffect(element = document.documentElement)`
145+
### `#useLayoutEffect(options = {})`
100146

101147
_Returns: `[T, Dispatch<SetStateAction<T>>]`_
102148

103149
Sets theme properties as
104150
[CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) on
105-
the provided `element` or the root `documentElement` by default. If the theme is
106-
updated, these props are updated too. This enables live theme switching!
151+
the provided `options.element` or the root `documentElement` by default. If the
152+
theme is updated, these props are updated too. This enables live theme
153+
switching!
154+
155+
You can also add class names to the same element via `options.classNames`, which
156+
is a `string[]`.
107157

108158
_See:
109159
[React Hooks API Reference | useLayoutEffect](https://reactjs.org/docs/hooks-reference.html#uselayouteffect)_

src/ThemeContext.test.tsx

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,9 @@ style.setProperty = setProperty
1414

1515
describe('new ThemeContext(defaultTheme)', () => {
1616
const themeContext = new ThemeContext(defaultTheme)
17-
let rendered: ReturnType<typeof render>
1817

1918
beforeEach(() => {
2019
setProperty.mockReset()
21-
22-
const App = () => {
23-
themeContext.useLayoutEffect()
24-
const [theme, setTheme] = themeContext.use()
25-
return (
26-
<button
27-
onClick={() => {
28-
setTheme({ primaryColor: 'blue' })
29-
}}
30-
>
31-
{theme.primaryColor}
32-
</button>
33-
)
34-
}
35-
36-
rendered = render(
37-
<themeContext.Provider>
38-
<App />
39-
</themeContext.Provider>,
40-
)
4120
})
4221

4322
afterAll(() => {
@@ -52,25 +31,73 @@ describe('new ThemeContext(defaultTheme)', () => {
5231

5332
describe('#Provider', () => {
5433
it('provides a theme', () => {
34+
const rendered = renderApp()
35+
5536
expect(rendered.getByText('red')).toBeDefined()
5637
})
5738
})
5839

5940
describe('#use', () => {
6041
it('returns [theme, setTheme], which respectively gets and sets the theme', () => {
42+
const rendered = renderApp()
43+
6144
fireEvent.click(rendered.getByText('red') as HTMLElement)
45+
6246
expect(rendered.getByText('blue')).toBeDefined()
6347
})
6448
})
6549

6650
describe('#useLayoutEffect', () => {
6751
it('sets a CSS custom property on the root document element', () => {
52+
renderApp()
53+
6854
expect(setProperty).toHaveBeenCalledWith('--primary-color', 'red')
6955
})
7056

7157
it('updates the custom property when the theme changes', () => {
58+
const rendered = renderApp()
59+
7260
fireEvent.click(rendered.getByText('red') as HTMLElement)
61+
7362
expect(setProperty).toHaveBeenCalledWith('--primary-color', 'blue')
7463
})
64+
65+
it('sets class names on provided element', () => {
66+
const element = document.createElement('html')
67+
68+
renderApp({ element, classNames: ['foo', 'bar'] })
69+
70+
expect(element.className).toEqual('foo bar')
71+
})
7572
})
73+
74+
function renderApp({
75+
element,
76+
classNames,
77+
}: {
78+
element?: HTMLElement
79+
classNames?: string[]
80+
} = {}) {
81+
const App = () => {
82+
themeContext.useLayoutEffect(
83+
element || classNames ? { element, classNames } : undefined,
84+
)
85+
const [theme, setTheme] = themeContext.use()
86+
return (
87+
<button
88+
onClick={() => {
89+
setTheme({ primaryColor: 'blue' })
90+
}}
91+
>
92+
{theme.primaryColor}
93+
</button>
94+
)
95+
}
96+
97+
return render(
98+
<themeContext.Provider>
99+
<App />
100+
</themeContext.Provider>,
101+
)
102+
}
76103
})

src/ThemeContext.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,14 @@ export default class ThemeContext<T extends object> {
5959
* return null
6060
* }
6161
*/
62-
public useLayoutEffect(
62+
public useLayoutEffect({
63+
classNames,
6364
element = document.documentElement,
64-
): [T, Dispatch<SetStateAction<T>>] {
65+
}: {
66+
element?: HTMLElement
67+
classNames?: string[]
68+
} = {}): [T, Dispatch<SetStateAction<T>>] {
69+
element.classList.add(...(classNames || []))
6570
const [theme, setTheme] = this.use()
6671
useLayoutEffect(() => {
6772
Object.keys(theme).forEach(setProp)

0 commit comments

Comments
 (0)