diff --git a/src/components/theme/ThemeContext.ts b/src/components/theme/ThemeContext.ts index e899d6e877..e7011affdb 100644 --- a/src/components/theme/ThemeContext.ts +++ b/src/components/theme/ThemeContext.ts @@ -7,12 +7,14 @@ export interface ThemeContextProps { theme: Theme; themeValue: RealTheme; direction: Direction; + default: boolean; } const initialValue: ThemeContextProps = { theme: DEFAULT_THEME, themeValue: DEFAULT_LIGHT_THEME, direction: DEFAULT_DIRECTION, + default: true, }; export const ThemeContext = React.createContext(initialValue); diff --git a/src/components/theme/ThemeProvider.tsx b/src/components/theme/ThemeProvider.tsx index 4d135f1ea7..c194970c77 100644 --- a/src/components/theme/ThemeProvider.tsx +++ b/src/components/theme/ThemeProvider.tsx @@ -29,21 +29,33 @@ export interface ThemeProviderProps extends React.PropsWithChildren<{}> { } export function ThemeProvider({ - theme = DEFAULT_THEME, - systemLightTheme = DEFAULT_LIGHT_THEME, - systemDarkTheme = DEFAULT_DARK_THEME, - direction = DEFAULT_DIRECTION, - nativeScrollbar = false, - scoped = false, + theme: themeProp, + systemLightTheme: systemLightThemeProp, + systemDarkTheme: systemDarkThemeProp, + direction: directionProp, + nativeScrollbar, + scoped: scopedProp = false, rootClassName = '', children, }: ThemeProviderProps) { - const systemTheme = ( - useSystemTheme() === 'light' ? systemLightTheme : systemDarkTheme - ) as RealTheme; + const parentThemeState = React.useContext(ThemeContext); + const systemThemeState = React.useContext(ThemeSettingsContext); + + const hasParentProvider = !parentThemeState.default; + const scoped = hasParentProvider || scopedProp; + const parentTheme = parentThemeState.theme ?? DEFAULT_THEME; + const theme = themeProp ?? parentTheme; + const systemLightTheme = + systemLightThemeProp ?? systemThemeState?.systemLightTheme ?? DEFAULT_LIGHT_THEME; + const systemDarkTheme = + systemDarkThemeProp ?? systemThemeState?.systemDarkTheme ?? DEFAULT_DARK_THEME; + const parentDirection = parentThemeState.direction ?? DEFAULT_DIRECTION; + const direction = directionProp ?? parentDirection; + + const systemTheme = useSystemTheme() === 'light' ? systemLightTheme : systemDarkTheme; const themeValue = theme === 'system' ? systemTheme : theme; - React.useEffect(() => { + React.useLayoutEffect(() => { if (!scoped) { updateBodyClassName({ theme: themeValue, @@ -59,6 +71,7 @@ export function ThemeProvider({ theme, themeValue, direction, + default: false, }), [theme, themeValue, direction], ); @@ -68,16 +81,20 @@ export function ThemeProvider({ [systemLightTheme, systemDarkTheme], ); + const isNeedToSetTheme = !hasParentProvider || themeValue !== parentThemeState.themeValue; return ( {scoped ? (
{children}
diff --git a/src/components/theme/__stories__/Theme.stories.tsx b/src/components/theme/__stories__/Theme.stories.tsx new file mode 100644 index 0000000000..38cd6ed0ac --- /dev/null +++ b/src/components/theme/__stories__/Theme.stories.tsx @@ -0,0 +1,70 @@ +import React from 'react'; + +import type {Meta, StoryObj} from '@storybook/react'; + +import {Button} from '../../Button'; +import {Text} from '../../Text'; +import {ThemeProvider} from '../ThemeProvider'; +import {useDirection} from '../useDirection'; + +const meta: Meta = { + title: 'Components/Utils/ThemeProvider', + component: ThemeProvider, + tags: ['nodocs'], + argTypes: { + theme: { + options: ['none', 'light', 'dark', 'light-hc', 'dark-hc', 'system'], + control: { + type: 'select', + }, + mapping: { + none: undefined, + }, + }, + direction: { + options: ['none', 'ltr', 'rtl'], + control: { + type: 'radio', + }, + mapping: { + none: undefined, + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +function ScopedComponent() { + return ( +
+ +
+ ); +} + +export const Scoped: Story = { + render: function ThemeScoped(props) { + return ( +
+ + + +
+ Inside scoped theme provider + +
+
+
+ ); + }, + argTypes: { + scoped: { + table: { + disable: true, + }, + }, + }, +}; diff --git a/src/components/theme/types.ts b/src/components/theme/types.ts index 27327dad58..6ba4dc5196 100644 --- a/src/components/theme/types.ts +++ b/src/components/theme/types.ts @@ -1,4 +1,4 @@ -export type RealTheme = 'light' | 'light-hc' | 'dark' | 'dark-hc' | string; +export type RealTheme = 'light' | 'light-hc' | 'dark' | 'dark-hc' | (string & {}); export type ThemeType = 'light' | 'dark'; export type Theme = 'system' | RealTheme; export type Direction = 'ltr' | 'rtl'; diff --git a/src/components/theme/useDirection.ts b/src/components/theme/useDirection.ts index fa53f75d99..3ac67ffa25 100644 --- a/src/components/theme/useDirection.ts +++ b/src/components/theme/useDirection.ts @@ -1,8 +1,6 @@ -import React from 'react'; - -import {ThemeContext} from './ThemeContext'; import type {ThemeContextProps} from './ThemeContext'; +import {useThemeContext} from './useThemeContext'; export function useDirection(): ThemeContextProps['direction'] { - return React.useContext(ThemeContext).direction; + return useThemeContext().direction; } diff --git a/src/components/theme/useTheme.ts b/src/components/theme/useTheme.ts index 9e39ac0225..95c1416e67 100644 --- a/src/components/theme/useTheme.ts +++ b/src/components/theme/useTheme.ts @@ -1,8 +1,6 @@ -import React from 'react'; - -import {ThemeContext} from './ThemeContext'; import type {ThemeContextProps} from './ThemeContext'; +import {useThemeContext} from './useThemeContext'; export function useTheme(): ThemeContextProps['theme'] { - return React.useContext(ThemeContext).theme; + return useThemeContext().theme; } diff --git a/src/components/theme/useThemeContext.ts b/src/components/theme/useThemeContext.ts new file mode 100644 index 0000000000..b889986be2 --- /dev/null +++ b/src/components/theme/useThemeContext.ts @@ -0,0 +1,11 @@ +import React from 'react'; + +import {ThemeContext} from './ThemeContext'; + +export function useThemeContext() { + const state = React.useContext(ThemeContext); + if (state === undefined) { + throw new Error('useTheme* hooks must be used within ThemeProvider'); + } + return state; +} diff --git a/src/components/theme/useThemeValue.ts b/src/components/theme/useThemeValue.ts index b5f2ca0ec2..5bba35f301 100644 --- a/src/components/theme/useThemeValue.ts +++ b/src/components/theme/useThemeValue.ts @@ -1,7 +1,6 @@ -import React from 'react'; - -import {ThemeContext, ThemeContextProps} from './ThemeContext'; +import type {ThemeContextProps} from './ThemeContext'; +import {useThemeContext} from './useThemeContext'; export function useThemeValue(): ThemeContextProps['themeValue'] { - return React.useContext(ThemeContext).themeValue; + return useThemeContext().themeValue; } diff --git a/src/components/theme/withDirection.tsx b/src/components/theme/withDirection.tsx index 5d286be06f..5469bb220a 100644 --- a/src/components/theme/withDirection.tsx +++ b/src/components/theme/withDirection.tsx @@ -1,26 +1,23 @@ import React from 'react'; -import type {Subtract} from 'utility-types'; - import {getComponentName} from '../utils/getComponentName'; -import {ThemeContext} from './ThemeContext'; import type {ThemeContextProps} from './ThemeContext'; +import {useDirection} from './useDirection'; export interface WithDirectionProps extends Pick {} export function withDirection( WrappedComponent: React.ComponentType, -): React.ComponentType> { +): React.ComponentType> { const componentName = getComponentName(WrappedComponent); - return class WithDirectionComponent extends React.Component> { - static displayName = `withDirection(${componentName})`; - static contextType = ThemeContext; - context!: React.ContextType; - - render() { - return ; - } + const component = function WithDirectionComponent(props: Omit) { + const direction = useDirection(); + return ; }; + + component.displayName = `withDirection(${componentName})`; + + return component; } diff --git a/src/components/theme/withTheme.tsx b/src/components/theme/withTheme.tsx index 055829a088..a81c3e9d19 100644 --- a/src/components/theme/withTheme.tsx +++ b/src/components/theme/withTheme.tsx @@ -1,26 +1,23 @@ import React from 'react'; -import type {Subtract} from 'utility-types'; - import {getComponentName} from '../utils/getComponentName'; -import {ThemeContext} from './ThemeContext'; import type {ThemeContextProps} from './ThemeContext'; +import {useTheme} from './useTheme'; export interface WithThemeProps extends Pick {} export function withTheme( WrappedComponent: React.ComponentType, -): React.ComponentType> { +): React.ComponentType> { const componentName = getComponentName(WrappedComponent); - return class WithThemeComponent extends React.Component> { - static displayName = `withTheme(${componentName})`; - static contextType = ThemeContext; - context!: React.ContextType; - - render() { - return ; - } + const component = function WithThemeComponent(props: Omit) { + const theme = useTheme(); + return ; }; + + component.displayName = `withTheme(${componentName})`; + + return component; } diff --git a/src/components/theme/withThemeValue.tsx b/src/components/theme/withThemeValue.tsx index 990145d9e2..f66f730d98 100644 --- a/src/components/theme/withThemeValue.tsx +++ b/src/components/theme/withThemeValue.tsx @@ -1,26 +1,23 @@ import React from 'react'; -import type {Subtract} from 'utility-types'; - import {getComponentName} from '../utils/getComponentName'; -import {ThemeContext} from './ThemeContext'; import type {ThemeContextProps} from './ThemeContext'; +import {useThemeValue} from './useThemeValue'; export interface WithThemeValueProps extends Pick {} export function withThemeValue( WrappedComponent: React.ComponentType, -): React.ComponentType> { +): React.ComponentType> { const componentName = getComponentName(WrappedComponent); - return class WithThemeValueComponent extends React.Component> { - static displayName = `withThemeValue(${componentName})`; - static contextType = ThemeContext; - context!: React.ContextType; - - render() { - return ; - } + const component = function WithThemeValueComponent(props: Omit) { + const themeValue = useThemeValue(); + return ; }; + + component.displayName = `withThemeValue(${componentName})`; + + return component; } diff --git a/styles/styles.scss b/styles/styles.scss index 2b7a9b3bb6..cba12f8fb4 100644 --- a/styles/styles.scss +++ b/styles/styles.scss @@ -51,9 +51,7 @@ background: var(--g-color-scroll-handle-hover); } - // stylelint-disable-next-line property-no-unknown scrollbar-width: var(--g-scrollbar-width); - // stylelint-disable-next-line property-no-unknown scrollbar-color: var(--g-color-scroll-handle) var(--g-color-scroll-track); } @@ -65,3 +63,10 @@ background-position: 0 0; } } + +body.g-root { + // default direction is ltr + --g-flow-direction: 1; + --g-flow-is-ltr: 1; + --g-flow-is-rtl: 0; +} diff --git a/styles/themes/common/_index.scss b/styles/themes/common/_index.scss index 04399d6490..8366949d30 100644 --- a/styles/themes/common/_index.scss +++ b/styles/themes/common/_index.scss @@ -14,4 +14,16 @@ --g-border-radius-xl: 10px; --g-focus-border-radius: 2px; + + &[dir='ltr'] { + --g-flow-direction: 1; + --g-flow-is-ltr: 1; + --g-flow-is-rtl: 0; + } + + &[dir='rtl'] { + --g-flow-direction: -1; + --g-flow-is-ltr: 0; + --g-flow-is-rtl: 1; + } }