-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement multi theme feature based on data attributes (#1756)
<!-- How to write a good PR title: - Follow [the Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). - Give as much context as necessary and as little as possible - Prefix it with [WIP] while it’s a work in progress --> ## Self Checklist - [x] I wrote a PR title in **English** and added an appropriate **label** to the PR. - [x] I wrote the commit message in **English** and to follow [**the Conventional Commits specification**](https://www.conventionalcommits.org/en/v1.0.0/). - [x] I [added the **changeset**](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md) about the changes that needed to be released. (or didn't have to) - [x] I wrote or updated **documentation** related to the changes. (or didn't have to) - [ ] I wrote or updated **tests** related to the changes. (or didn't have to) - [x] I tested the changes in various browsers. (or didn't have to) - Windows: Chrome, Edge, (Optional) Firefox - macOS: Chrome, Edge, Safari, (Optional) Firefox ## Related Issue <!-- Please link to issue if one exists --> Fixes #1690 ## Summary <!-- Please brief explanation of the changes made --> data attributes를 기반으로 한 멀티 테마 기능을 구현합니다. ## Details <!-- Please elaborate description of the changes --> foundation 객체를 주입받는 형식에서, themeNam 문자열을 주입받아 하위 엘리먼트(루트 엘리먼트가 되길 기대)의 data attribute를 변경 & 해당하는 토큰 객체를 컨텍스트로 전달하는 AppProvider를 구현합니다. - features prop: 사용 편의성을 위해 `FeatureProvider` 를 빌트인으로 가지는 방향으로 변경했습니다. 기본값은 빈 배열입니다. - BezierProvider to AppProvider: 앱의 루트에 적용하는 Provider라는 점을 강조하기 위해 Bezier 대신 App이라는 접두어를 붙였습니다. 라이트테마, 다크테마 혹은 테마 역전(Tooltip 등)이 필요한 곳에서 테마를 고정해서 사용할 수 있는 ThemeProvider를 구현합니다. 이제 `--inverted-` 토큰이 사라지는 대신, 하위 엘리먼트에 radix-ui Slot을 통해 data theme attribute를 전달하여 토큰을 스위칭하는 방식으로 동작하게 됩니다. ### Breaking change? (Yes/No) <!-- If Yes, please describe the impact and migration path for users --> No. 기존 BezierProvider는 그대로 유지합니다. ## References <!-- Please list any other resources or points the reviewer should be aware of --> - https://www.radix-ui.com/themes/docs/components/theme - https://panda-css.com/docs/guides/multiple-themes - https://github.com/Shopify/polaris/blob/main/polaris-react/src/components/AppProvider/AppProvider.tsx
- Loading branch information
1 parent
49e464b
commit 8f88a03
Showing
8 changed files
with
275 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@channel.io/bezier-react": minor | ||
--- | ||
|
||
Implement multi theme feature based on data attributes. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export type { Feature } from './Feature' | ||
export { FeatureType } from './Feature' | ||
|
||
export { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import React, { useEffect } from 'react' | ||
|
||
import { | ||
type Feature, | ||
FeatureProvider, | ||
} from '~/src/features' | ||
import { window as defaultWindow } from '~/src/utils/dom' | ||
|
||
import { TooltipProvider } from '~/src/components/Tooltip' | ||
|
||
import { | ||
type ThemeName, | ||
TokenProvider, | ||
} from './ThemeProvider' | ||
import { WindowProvider } from './WindowProvider' | ||
|
||
export interface AlphaAppProviderProps { | ||
children: React.ReactNode | ||
/** | ||
* Name of the theme to use for the app. | ||
* @default 'light' | ||
*/ | ||
themeName?: ThemeName | ||
/** | ||
* List of features to enable for the app. | ||
* @default [] | ||
*/ | ||
features?: Feature[] | ||
/** | ||
* Window object to use for the app. | ||
* @default window | ||
*/ | ||
window?: Window | ||
} | ||
|
||
/** | ||
* `AlphaAppProvider` is a required wrapper component that provides context for the app. | ||
* | ||
* @example | ||
* | ||
* ```tsx | ||
* import React from 'react' | ||
* import { createRoot } from 'react-dom/client' | ||
* import { AlphaAppProvider } from '@channel.io/bezier-react' | ||
* | ||
* const container = document.getElementById('root') | ||
* const root = createRoot(container) | ||
* | ||
* root.render( | ||
* <AlphaAppProvider themeName="light"> | ||
* <App /> | ||
* </AlphaAppProvider>, | ||
* ) | ||
* ``` | ||
*/ | ||
export function AlphaAppProvider({ | ||
children, | ||
themeName = 'light', | ||
features = [], | ||
window = defaultWindow, | ||
}: AlphaAppProviderProps) { | ||
useEffect(function updateRootThemeDataAttribute() { | ||
const rootElement = window.document.documentElement | ||
// TODO: Change data attribute constant to import from bezier-tokens | ||
rootElement.setAttribute('data-bezier-theme', themeName) | ||
return function cleanup() { | ||
rootElement.removeAttribute('data-bezier-theme') | ||
} | ||
}, [ | ||
window, | ||
themeName, | ||
]) | ||
|
||
return ( | ||
<WindowProvider window={window}> | ||
<FeatureProvider features={features}> | ||
<TokenProvider themeName={themeName}> | ||
<TooltipProvider> | ||
{ children } | ||
</TooltipProvider> | ||
</TokenProvider> | ||
</FeatureProvider> | ||
</WindowProvider> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import React, { | ||
forwardRef, | ||
useMemo, | ||
} from 'react' | ||
|
||
import { tokens } from '@channel.io/bezier-tokens' | ||
import { Slot } from '@radix-ui/react-slot' | ||
|
||
import { createContext } from '~/src/utils/react' | ||
|
||
type Tokens = typeof tokens | ||
type GlobalTokens = Tokens['global'] | ||
type SemanticTokens = Omit<Tokens, 'global'> | ||
|
||
interface ThemedTokenSet { | ||
global: GlobalTokens | ||
semantic: SemanticTokens[keyof SemanticTokens] | ||
} | ||
|
||
// TODO: Change theme name constant to import from bezier-tokens | ||
export type ThemeName = 'light' | 'dark' | ||
|
||
interface TokenContextValue { | ||
themeName: ThemeName | ||
tokens: ThemedTokenSet | ||
} | ||
|
||
const [TokenContextProvider, useTokenContext] = createContext<TokenContextValue | null>(null, 'TokenProvider') | ||
|
||
const tokenSet: Record<ThemeName, ThemedTokenSet> = Object.freeze({ | ||
light: { | ||
global: tokens.global, | ||
semantic: tokens.lightTheme, | ||
}, | ||
dark: { | ||
global: tokens.global, | ||
semantic: tokens.darkTheme, | ||
}, | ||
}) | ||
|
||
interface TokenProviderProps { | ||
themeName: ThemeName | ||
children: React.ReactNode | ||
} | ||
|
||
/** | ||
* @private For internal use only. | ||
*/ | ||
export function TokenProvider({ | ||
themeName, | ||
children, | ||
}: TokenProviderProps) { | ||
return ( | ||
<TokenContextProvider value={useMemo(() => ({ | ||
themeName, | ||
tokens: tokenSet[themeName], | ||
}), [themeName])} | ||
> | ||
{ children } | ||
</TokenContextProvider> | ||
) | ||
} | ||
|
||
/** | ||
* `useThemeName` is a hook that returns the current theme name. | ||
*/ | ||
export function useThemeName() { | ||
return useTokenContext('useThemeName').themeName | ||
} | ||
|
||
/** | ||
* `useToken` is a hook that returns the design token for the current theme. | ||
*/ | ||
export function useToken() { | ||
return useTokenContext('useToken').tokens | ||
} | ||
|
||
export interface ThemeProviderProps { | ||
themeName: ThemeName | ||
children: React.ReactElement | ||
} | ||
|
||
export type FixedThemeProviderProps = Omit<ThemeProviderProps, 'themeName'> | ||
|
||
/** | ||
* `ThemeProvider` is a wrapper component that provides theme context. | ||
*/ | ||
export const ThemeProvider = forwardRef<HTMLElement, ThemeProviderProps>(function ThemeProvider({ | ||
themeName, | ||
children, | ||
}, forwardedRef) { | ||
return ( | ||
<TokenProvider themeName={themeName}> | ||
<Slot | ||
ref={forwardedRef} | ||
// TODO: Change data attribute constant to import from bezier-tokens | ||
data-bezier-theme={themeName} | ||
> | ||
{ children } | ||
</Slot> | ||
</TokenProvider> | ||
) | ||
}) | ||
|
||
/** | ||
* `LightThemeProvider` is a wrapper component that provides light theme context. | ||
*/ | ||
export const LightThemeProvider = forwardRef<HTMLElement, FixedThemeProviderProps>(function LightTheme({ | ||
children, | ||
}, forwardedRef) { | ||
return ( | ||
<ThemeProvider | ||
ref={forwardedRef} | ||
themeName="light" | ||
> | ||
{ children } | ||
</ThemeProvider> | ||
) | ||
}) | ||
|
||
/** | ||
* `DarkThemeProvider` is a wrapper component that provides dark theme context. | ||
*/ | ||
export const DarkThemeProvider = forwardRef<HTMLElement, FixedThemeProviderProps>(function DarkTheme({ | ||
children, | ||
}, forwardedRef) { | ||
return ( | ||
<ThemeProvider | ||
ref={forwardedRef} | ||
themeName="dark" | ||
> | ||
{ children } | ||
</ThemeProvider> | ||
) | ||
}) | ||
|
||
/** | ||
* `InvertedThemeProvider` is a wrapper component that provides inverted theme context. | ||
*/ | ||
export const InvertedThemeProvider = forwardRef<HTMLElement, FixedThemeProviderProps>(function InvertedTheme({ | ||
children, | ||
}, forwardedRef) { | ||
return ( | ||
<ThemeProvider | ||
ref={forwardedRef} | ||
themeName={useThemeName() === 'light' ? 'dark' : 'light'} | ||
> | ||
{ children } | ||
</ThemeProvider> | ||
) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters