Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(theming): add ColorSchemeProvider #1991

Merged
merged 9 commits into from
Jan 7, 2025
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} />}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this is currently empty – the Story was needed to get generated API docs. We can fill it with something meaningful in the future if/when πŸ’‘ strikes

</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
Loading