Skip to content

Commit 919df82

Browse files
authored
feat(theming): add ColorSchemeProvider (#1991)
...and associated `useColorScheme` hook
1 parent a7dc5a4 commit 919df82

11 files changed

+461
-34
lines changed

.storybook/preview.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@ export const parameters = {
4545
};
4646

4747
const GlobalPreviewStyling = createGlobalStyle`
48-
body {
48+
html {
4949
background-color: ${p => getColor({ theme: p.theme, variable: 'background.default' })};
50+
color: ${p => getColor({ theme: p.theme, variable: 'foreground.default' })};
51+
}
52+
53+
body {
5054
/* stylelint-disable-next-line declaration-no-important */
5155
padding: 0 !important;
52-
color: ${p => getColor({ theme: p.theme, variable: 'foreground.default' })};
5356
font-family: ${p => p.theme.fonts.system};
5457
}
5558
`;

packages/theming/README.md

+31-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,37 @@ complex, depending on your needs:
4242
behavior and RTL layout of Garden's tabs component with an alternate visual
4343
design (i.e. closer to the look of browser tabs).
4444

45-
### RTL
45+
#### Color scheme
46+
47+
The `ColorSchemeProvider` and `useColorScheme` hook add the capability for a
48+
user to persist a preferred system color scheme (`'light'`, `'dark'`, or
49+
`'system'`). See
50+
[Storybook](https://zendeskgarden.github.io/react-components/?path=/docs/packages-theming-colorschemeprovider--color-scheme-provider)
51+
for more details.
52+
53+
```jsx
54+
import {
55+
useColorScheme,
56+
ColorSchemeProvider,
57+
ThemeProvider,
58+
DEFAULT_THEME
59+
} from '@zendeskgarden/react-theming';
60+
61+
const ThemedApp = ({ children }) => {
62+
const { colorScheme } = useColorScheme();
63+
const theme = { ...DEFAULT_THEME, colors: { ...DEFAULT_THEME.colors, base: colorScheme } };
64+
65+
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
66+
};
67+
68+
const App = ({ children }) => (
69+
<ColorSchemeProvider>
70+
<ThemedApp>{children}</ThemedApp>
71+
</ColorSchemeProvider>
72+
);
73+
```
74+
75+
#### RTL
4676

4777
```jsx
4878
import { ThemeProvider, DEFAULT_THEME } from '@zendeskgarden/react-theming';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Meta, ArgsTable, Canvas, Story, Markdown } from '@storybook/addon-docs';
2+
import { ColorSchemeProvider } from '@zendeskgarden/react-theming';
3+
import { ColorSchemeProviderStory } from './stories/ColorSchemeProviderStory';
4+
import README from '../README.md';
5+
6+
<Meta title="Packages/Theming/ColorSchemeProvider" component={ColorSchemeProvider} />
7+
8+
# API
9+
10+
<ArgsTable />
11+
12+
# Demo
13+
14+
## ColorSchemeProvider
15+
16+
<Canvas>
17+
<Story name="ColorSchemeProvider" args={{ initialColorScheme: 'system' }}>
18+
{args => <ColorSchemeProviderStory {...args} />}
19+
</Story>
20+
</Canvas>
21+
22+
<Markdown>{README}</Markdown>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Copyright Zendesk, Inc.
3+
*
4+
* Use of this source code is governed under the Apache License, Version 2.0
5+
* found at http://www.apache.org/licenses/LICENSE-2.0.
6+
*/
7+
8+
import React, { useEffect, useState } from 'react';
9+
import styled, { ThemeProvider, useTheme } from 'styled-components';
10+
import { StoryFn } from '@storybook/react';
11+
import ClearIcon from '@zendeskgarden/svg-icons/src/16/x-stroke.svg';
12+
import DarkIcon from '@zendeskgarden/svg-icons/src/16/moon-stroke.svg';
13+
import LightIcon from '@zendeskgarden/svg-icons/src/16/sun-stroke.svg';
14+
import SystemIcon from '@zendeskgarden/svg-icons/src/16/monitor-stroke.svg';
15+
import {
16+
ColorScheme,
17+
ColorSchemeProvider,
18+
getColor,
19+
IColorSchemeProviderProps,
20+
IGardenTheme,
21+
useColorScheme,
22+
useWindow
23+
} from '@zendeskgarden/react-theming';
24+
import { Grid } from '@zendeskgarden/react-grid';
25+
import { IconButton } from '@zendeskgarden/react-buttons';
26+
import { IMenuProps, Item, ItemGroup, Menu } from '@zendeskgarden/react-dropdowns';
27+
import { Field, Input } from '@zendeskgarden/react-forms';
28+
import { Code } from '@zendeskgarden/react-typography';
29+
import { Tooltip } from '@zendeskgarden/react-tooltips';
30+
31+
const StyledGrid = styled(Grid)`
32+
background-color: ${p => getColor({ theme: p.theme, variable: 'background.default' })};
33+
`;
34+
35+
const StyledIconButton = styled(IconButton)`
36+
position: absolute;
37+
right: ${p => p.theme.space.base * 3}px;
38+
bottom: ${p => p.theme.space.base}px;
39+
`;
40+
41+
const Content = ({
42+
colorSchemeKey = 'color-scheme'
43+
}: {
44+
colorSchemeKey: IColorSchemeProviderProps['colorSchemeKey'];
45+
}) => {
46+
const win = useWindow();
47+
const localStorage = win?.localStorage;
48+
const { colorScheme, isSystem, setColorScheme } = useColorScheme();
49+
const [inputValue, setInputValue] = useState('');
50+
const _theme = useTheme() as IGardenTheme;
51+
const theme = { ..._theme, colors: { ..._theme.colors, base: colorScheme } };
52+
53+
const handleChange: IMenuProps['onChange'] = changes => {
54+
if (changes.value) {
55+
setColorScheme(changes.value as ColorScheme);
56+
}
57+
};
58+
59+
const handleClear = () => {
60+
localStorage?.removeItem(colorSchemeKey);
61+
setInputValue('');
62+
};
63+
64+
useEffect(() => {
65+
setInputValue(localStorage?.getItem(colorSchemeKey) || '');
66+
}, [colorSchemeKey, colorScheme, isSystem, localStorage]);
67+
68+
return (
69+
<ThemeProvider theme={theme}>
70+
<StyledGrid gutters="xl">
71+
<Grid.Row style={{ height: 'calc(100vh - 80px)' }}>
72+
<Grid.Col alignSelf="center" sm={5}>
73+
<div style={{ position: 'relative' }}>
74+
<Field>
75+
<Field.Label>
76+
Local {!!colorSchemeKey && <Code>{colorSchemeKey}</Code>} storage
77+
</Field.Label>
78+
<Input placeholder="unspecified" readOnly value={inputValue} />
79+
{!!inputValue && (
80+
<Tooltip content={`Clear ${colorSchemeKey} storage`}>
81+
<StyledIconButton focusInset onClick={handleClear} size="small">
82+
<ClearIcon />
83+
</StyledIconButton>
84+
</Tooltip>
85+
)}
86+
</Field>
87+
</div>
88+
</Grid.Col>
89+
<Grid.Col textAlign="center" alignSelf="center">
90+
<Menu
91+
/* eslint-disable-next-line react/no-unstable-nested-components */
92+
button={props => (
93+
<IconButton {...props}>
94+
{theme.colors.base === 'dark' ? <DarkIcon /> : <LightIcon />}
95+
</IconButton>
96+
)}
97+
onChange={handleChange}
98+
placement="bottom-end"
99+
selectedItems={[{ value: isSystem ? 'system' : colorScheme }]}
100+
>
101+
<ItemGroup type="radio">
102+
<Item icon={<LightIcon />} value="light">
103+
Light
104+
</Item>
105+
<Item icon={<DarkIcon />} value="dark">
106+
Dark
107+
</Item>
108+
<Item icon={<SystemIcon />} isSelected value="system">
109+
System
110+
</Item>
111+
</ItemGroup>
112+
</Menu>
113+
</Grid.Col>
114+
</Grid.Row>
115+
</StyledGrid>
116+
</ThemeProvider>
117+
);
118+
};
119+
120+
export const ColorSchemeProviderStory: StoryFn<IColorSchemeProviderProps> = ({
121+
colorSchemeKey,
122+
initialColorScheme
123+
}) => (
124+
<ColorSchemeProvider
125+
key={initialColorScheme}
126+
colorSchemeKey={colorSchemeKey}
127+
initialColorScheme={initialColorScheme}
128+
>
129+
<Content colorSchemeKey={colorSchemeKey} />
130+
</ColorSchemeProvider>
131+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Meta, ArgsTable, Canvas, Story, Markdown } from '@storybook/addon-docs';
2+
import { ThemeProvider, DEFAULT_THEME, PALETTE } from '@zendeskgarden/react-theming';
3+
import { PaletteStory } from './stories/PaletteStory';
4+
import README from '../README.md';
5+
6+
<Meta
7+
title="Packages/Theming/ThemeProvider"
8+
component={ThemeProvider}
9+
subcomponents={{ DEFAULT_THEME, PALETTE }}
10+
/>
11+
12+
# API
13+
14+
<ArgsTable />
15+
16+
# Demo
17+
18+
## ThemeProvider
19+
20+
<Canvas>
21+
<Story name="ThemeProvider" args={{ theme: DEFAULT_THEME }}>
22+
{args => <ThemeProvider {...args} />}
23+
</Story>
24+
</Canvas>
25+
26+
## PALETTE
27+
28+
<Canvas>
29+
<Story
30+
name="PALETTE"
31+
args={{ palette: PALETTE }}
32+
argTypes={{
33+
palette: { control: { type: 'object' }, name: 'PALETTE' },
34+
colorSchemeKey: { table: { disable: true } },
35+
initialColorScheme: { table: { disable: true } },
36+
theme: { table: { disable: true } }
37+
}}
38+
>
39+
{args => <PaletteStory {...args} />}
40+
</Story>
41+
</Canvas>
42+
43+
<Markdown>{README}</Markdown>

packages/theming/demo/utilities.stories.mdx

+5-31
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,15 @@
1-
import { Meta, ArgsTable, Canvas, Story, Markdown } from '@storybook/addon-docs';
2-
import { ThemeProvider, DEFAULT_THEME, PALETTE } from '@zendeskgarden/react-theming';
3-
import { PaletteStory } from './stories/PaletteStory';
1+
import { Meta, Canvas, Story, Markdown } from '@storybook/addon-docs';
2+
import { DEFAULT_THEME } from '@zendeskgarden/react-theming';
43
import { ArrowStylesStory } from './stories/ArrowStylesStory';
54
import { MenuStylesStory } from './stories/MenuStylesStory';
65
import { GetColorStory } from './stories/GetColorStory';
76
import { ARROW_POSITIONS, MENU_POSITIONS } from './stories/data';
87
import README from '../README.md';
98

10-
<Meta
11-
title="Packages/Theming"
12-
component={ThemeProvider}
13-
subcomponents={{ DEFAULT_THEME, PALETTE }}
14-
/>
15-
16-
# API
17-
18-
<ArgsTable />
9+
<Meta title="Packages/Theming/utilities" />
1910

2011
# Demo
2112

22-
## PALETTE
23-
24-
<Canvas>
25-
<Story
26-
name="PALETTE"
27-
args={{ palette: PALETTE }}
28-
argTypes={{
29-
palette: { control: { type: 'object' }, name: 'PALETTE' },
30-
theme: { control: false }
31-
}}
32-
>
33-
{args => <PaletteStory {...args} />}
34-
</Story>
35-
</Canvas>
36-
3713
## arrowStyles()
3814

3915
<Canvas>
@@ -50,8 +26,7 @@ import README from '../README.md';
5026
argTypes={{
5127
position: { control: 'select', options: ARROW_POSITIONS },
5228
size: { control: { type: 'range', min: 2, max: 10, step: 1 } },
53-
inset: { control: { type: 'range', min: -4, max: 4, step: 1 } },
54-
theme: { control: false }
29+
inset: { control: { type: 'range', min: -4, max: 4, step: 1 } }
5530
}}
5631
>
5732
{args => <ArrowStylesStory {...args} />}
@@ -99,8 +74,7 @@ import README from '../README.md';
9974
isAnimated: true
10075
}}
10176
argTypes={{
102-
position: { control: 'radio', options: MENU_POSITIONS },
103-
theme: { control: false }
77+
position: { control: 'radio', options: MENU_POSITIONS }
10478
}}
10579
>
10680
{args => <MenuStylesStory {...args} />}

0 commit comments

Comments
 (0)