Skip to content

Commit

Permalink
feat(theming): add ColorSchemeProvider (#1991)
Browse files Browse the repository at this point in the history
...and associated `useColorScheme` hook
  • Loading branch information
jzempel authored Jan 7, 2025
1 parent a7dc5a4 commit 919df82
Show file tree
Hide file tree
Showing 11 changed files with 461 additions and 34 deletions.
7 changes: 5 additions & 2 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ export const parameters = {
};

const GlobalPreviewStyling = createGlobalStyle`
body {
html {
background-color: ${p => getColor({ theme: p.theme, variable: 'background.default' })};
color: ${p => getColor({ theme: p.theme, variable: 'foreground.default' })};
}
body {
/* stylelint-disable-next-line declaration-no-important */
padding: 0 !important;
color: ${p => getColor({ theme: p.theme, variable: 'foreground.default' })};
font-family: ${p => p.theme.fonts.system};
}
`;
Expand Down
32 changes: 31 additions & 1 deletion packages/theming/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,37 @@ complex, depending on your needs:
behavior and RTL layout of Garden's tabs component with an alternate visual
design (i.e. closer to the look of browser tabs).

### RTL
#### Color scheme

The `ColorSchemeProvider` and `useColorScheme` hook add the capability for a
user to persist a preferred system color scheme (`'light'`, `'dark'`, or
`'system'`). See
[Storybook](https://zendeskgarden.github.io/react-components/?path=/docs/packages-theming-colorschemeprovider--color-scheme-provider)
for more details.

```jsx
import {
useColorScheme,
ColorSchemeProvider,
ThemeProvider,
DEFAULT_THEME
} from '@zendeskgarden/react-theming';

const ThemedApp = ({ children }) => {
const { colorScheme } = useColorScheme();
const theme = { ...DEFAULT_THEME, colors: { ...DEFAULT_THEME.colors, base: colorScheme } };

return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
};

const App = ({ children }) => (
<ColorSchemeProvider>
<ThemedApp>{children}</ThemedApp>
</ColorSchemeProvider>
);
```

#### RTL

```jsx
import { ThemeProvider, DEFAULT_THEME } from '@zendeskgarden/react-theming';
Expand Down
22 changes: 22 additions & 0 deletions packages/theming/demo/colorSchemeProvider.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Meta, ArgsTable, Canvas, Story, Markdown } from '@storybook/addon-docs';
import { ColorSchemeProvider } from '@zendeskgarden/react-theming';
import { ColorSchemeProviderStory } from './stories/ColorSchemeProviderStory';
import README from '../README.md';

<Meta title="Packages/Theming/ColorSchemeProvider" component={ColorSchemeProvider} />

# API

<ArgsTable />

# Demo

## ColorSchemeProvider

<Canvas>
<Story name="ColorSchemeProvider" args={{ initialColorScheme: 'system' }}>
{args => <ColorSchemeProviderStory {...args} />}
</Story>
</Canvas>

<Markdown>{README}</Markdown>
131 changes: 131 additions & 0 deletions packages/theming/demo/stories/ColorSchemeProviderStory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Copyright Zendesk, Inc.
*
* Use of this source code is governed under the Apache License, Version 2.0
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/

import React, { useEffect, useState } from 'react';
import styled, { ThemeProvider, useTheme } from 'styled-components';
import { StoryFn } from '@storybook/react';
import ClearIcon from '@zendeskgarden/svg-icons/src/16/x-stroke.svg';
import DarkIcon from '@zendeskgarden/svg-icons/src/16/moon-stroke.svg';
import LightIcon from '@zendeskgarden/svg-icons/src/16/sun-stroke.svg';
import SystemIcon from '@zendeskgarden/svg-icons/src/16/monitor-stroke.svg';
import {
ColorScheme,
ColorSchemeProvider,
getColor,
IColorSchemeProviderProps,
IGardenTheme,
useColorScheme,
useWindow
} from '@zendeskgarden/react-theming';
import { Grid } from '@zendeskgarden/react-grid';
import { IconButton } from '@zendeskgarden/react-buttons';
import { IMenuProps, Item, ItemGroup, Menu } from '@zendeskgarden/react-dropdowns';
import { Field, Input } from '@zendeskgarden/react-forms';
import { Code } from '@zendeskgarden/react-typography';
import { Tooltip } from '@zendeskgarden/react-tooltips';

const StyledGrid = styled(Grid)`
background-color: ${p => getColor({ theme: p.theme, variable: 'background.default' })};
`;

const StyledIconButton = styled(IconButton)`
position: absolute;
right: ${p => p.theme.space.base * 3}px;
bottom: ${p => p.theme.space.base}px;
`;

const Content = ({
colorSchemeKey = 'color-scheme'
}: {
colorSchemeKey: IColorSchemeProviderProps['colorSchemeKey'];
}) => {
const win = useWindow();
const localStorage = win?.localStorage;
const { colorScheme, isSystem, setColorScheme } = useColorScheme();
const [inputValue, setInputValue] = useState('');
const _theme = useTheme() as IGardenTheme;
const theme = { ..._theme, colors: { ..._theme.colors, base: colorScheme } };

const handleChange: IMenuProps['onChange'] = changes => {
if (changes.value) {
setColorScheme(changes.value as ColorScheme);
}
};

const handleClear = () => {
localStorage?.removeItem(colorSchemeKey);
setInputValue('');
};

useEffect(() => {
setInputValue(localStorage?.getItem(colorSchemeKey) || '');
}, [colorSchemeKey, colorScheme, isSystem, localStorage]);

return (
<ThemeProvider theme={theme}>
<StyledGrid gutters="xl">
<Grid.Row style={{ height: 'calc(100vh - 80px)' }}>
<Grid.Col alignSelf="center" sm={5}>
<div style={{ position: 'relative' }}>
<Field>
<Field.Label>
Local {!!colorSchemeKey && <Code>{colorSchemeKey}</Code>} storage
</Field.Label>
<Input placeholder="unspecified" readOnly value={inputValue} />
{!!inputValue && (
<Tooltip content={`Clear ${colorSchemeKey} storage`}>
<StyledIconButton focusInset onClick={handleClear} size="small">
<ClearIcon />
</StyledIconButton>
</Tooltip>
)}
</Field>
</div>
</Grid.Col>
<Grid.Col textAlign="center" alignSelf="center">
<Menu
/* eslint-disable-next-line react/no-unstable-nested-components */
button={props => (
<IconButton {...props}>
{theme.colors.base === 'dark' ? <DarkIcon /> : <LightIcon />}
</IconButton>
)}
onChange={handleChange}
placement="bottom-end"
selectedItems={[{ value: isSystem ? 'system' : colorScheme }]}
>
<ItemGroup type="radio">
<Item icon={<LightIcon />} value="light">
Light
</Item>
<Item icon={<DarkIcon />} value="dark">
Dark
</Item>
<Item icon={<SystemIcon />} isSelected value="system">
System
</Item>
</ItemGroup>
</Menu>
</Grid.Col>
</Grid.Row>
</StyledGrid>
</ThemeProvider>
);
};

export const ColorSchemeProviderStory: StoryFn<IColorSchemeProviderProps> = ({
colorSchemeKey,
initialColorScheme
}) => (
<ColorSchemeProvider
key={initialColorScheme}
colorSchemeKey={colorSchemeKey}
initialColorScheme={initialColorScheme}
>
<Content colorSchemeKey={colorSchemeKey} />
</ColorSchemeProvider>
);
43 changes: 43 additions & 0 deletions packages/theming/demo/themeProvider.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Meta, ArgsTable, Canvas, Story, Markdown } from '@storybook/addon-docs';
import { ThemeProvider, DEFAULT_THEME, PALETTE } from '@zendeskgarden/react-theming';
import { PaletteStory } from './stories/PaletteStory';
import README from '../README.md';

<Meta
title="Packages/Theming/ThemeProvider"
component={ThemeProvider}
subcomponents={{ DEFAULT_THEME, PALETTE }}
/>

# API

<ArgsTable />

# Demo

## ThemeProvider

<Canvas>
<Story name="ThemeProvider" args={{ theme: DEFAULT_THEME }}>
{args => <ThemeProvider {...args} />}
</Story>
</Canvas>

## PALETTE

<Canvas>
<Story
name="PALETTE"
args={{ palette: PALETTE }}
argTypes={{
palette: { control: { type: 'object' }, name: 'PALETTE' },
colorSchemeKey: { table: { disable: true } },
initialColorScheme: { table: { disable: true } },
theme: { table: { disable: true } }
}}
>
{args => <PaletteStory {...args} />}
</Story>
</Canvas>

<Markdown>{README}</Markdown>
36 changes: 5 additions & 31 deletions packages/theming/demo/utilities.stories.mdx
Original file line number Diff line number Diff line change
@@ -1,39 +1,15 @@
import { Meta, ArgsTable, Canvas, Story, Markdown } from '@storybook/addon-docs';
import { ThemeProvider, DEFAULT_THEME, PALETTE } from '@zendeskgarden/react-theming';
import { PaletteStory } from './stories/PaletteStory';
import { Meta, Canvas, Story, Markdown } from '@storybook/addon-docs';
import { DEFAULT_THEME } from '@zendeskgarden/react-theming';
import { ArrowStylesStory } from './stories/ArrowStylesStory';
import { MenuStylesStory } from './stories/MenuStylesStory';
import { GetColorStory } from './stories/GetColorStory';
import { ARROW_POSITIONS, MENU_POSITIONS } from './stories/data';
import README from '../README.md';

<Meta
title="Packages/Theming"
component={ThemeProvider}
subcomponents={{ DEFAULT_THEME, PALETTE }}
/>

# API

<ArgsTable />
<Meta title="Packages/Theming/utilities" />

# Demo

## PALETTE

<Canvas>
<Story
name="PALETTE"
args={{ palette: PALETTE }}
argTypes={{
palette: { control: { type: 'object' }, name: 'PALETTE' },
theme: { control: false }
}}
>
{args => <PaletteStory {...args} />}
</Story>
</Canvas>

## arrowStyles()

<Canvas>
Expand All @@ -50,8 +26,7 @@ import README from '../README.md';
argTypes={{
position: { control: 'select', options: ARROW_POSITIONS },
size: { control: { type: 'range', min: 2, max: 10, step: 1 } },
inset: { control: { type: 'range', min: -4, max: 4, step: 1 } },
theme: { control: false }
inset: { control: { type: 'range', min: -4, max: 4, step: 1 } }
}}
>
{args => <ArrowStylesStory {...args} />}
Expand Down Expand Up @@ -99,8 +74,7 @@ import README from '../README.md';
isAnimated: true
}}
argTypes={{
position: { control: 'radio', options: MENU_POSITIONS },
theme: { control: false }
position: { control: 'radio', options: MENU_POSITIONS }
}}
>
{args => <MenuStylesStory {...args} />}
Expand Down
Loading

0 comments on commit 919df82

Please sign in to comment.