Skip to content

Commit

Permalink
fix(ThemeProvider): inner provider should overwrite specified props a…
Browse files Browse the repository at this point in the history
…nd only them
  • Loading branch information
ValeraS committed Jan 3, 2024
1 parent d443fa0 commit 403f5a8
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 66 deletions.
2 changes: 2 additions & 0 deletions src/components/theme/ThemeContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
47 changes: 32 additions & 15 deletions src/components/theme/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -59,6 +71,7 @@ export function ThemeProvider({
theme,
themeValue,
direction,
default: false,
}),
[theme, themeValue, direction],
);
Expand All @@ -68,16 +81,20 @@ export function ThemeProvider({
[systemLightTheme, systemDarkTheme],
);

const isNeedToSetTheme = !hasParentProvider || themeValue !== parentThemeState.themeValue;
return (
<ThemeContext.Provider value={contextValue}>
<ThemeSettingsContext.Provider value={themeSettingsContext}>
{scoped ? (
<div
className={bNew({theme: themeValue, 'native-scrollbar': nativeScrollbar}, [
b({theme: themeValue, 'native-scrollbar': nativeScrollbar}),
rootClassName,
])}
dir={direction === DEFAULT_DIRECTION ? undefined : direction}
className={bNew(
{
theme: isNeedToSetTheme && themeValue,
'native-scrollbar': nativeScrollbar !== false,
},
[b({theme: isNeedToSetTheme && themeValue}), rootClassName],
)}
dir={direction === parentDirection ? undefined : direction}
>
{children}
</div>
Expand Down
70 changes: 70 additions & 0 deletions src/components/theme/__stories__/Theme.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof ThemeProvider> = {
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<typeof ThemeProvider>;

function ScopedComponent() {
return (
<div style={{transform: 'scaleX(var(--g-flow-direction))'}}>
<Button>{`current direction: ${useDirection()}`}</Button>
</div>
);
}

export const Scoped: Story = {
render: function ThemeScoped(props) {
return (
<div>
<ScopedComponent />

<ThemeProvider {...props}>
<div style={{border: '1px red dotted', padding: 10, marginBlockStart: 10}}>
<Text>Inside scoped theme provider</Text>
<ScopedComponent />
</div>
</ThemeProvider>
</div>
);
},
argTypes: {
scoped: {
table: {
disable: true,
},
},
},
};
2 changes: 1 addition & 1 deletion src/components/theme/types.ts
Original file line number Diff line number Diff line change
@@ -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';
6 changes: 2 additions & 4 deletions src/components/theme/useDirection.ts
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 2 additions & 4 deletions src/components/theme/useTheme.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 11 additions & 0 deletions src/components/theme/useThemeContext.ts
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 3 additions & 4 deletions src/components/theme/useThemeValue.ts
Original file line number Diff line number Diff line change
@@ -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;
}
21 changes: 9 additions & 12 deletions src/components/theme/withDirection.tsx
Original file line number Diff line number Diff line change
@@ -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<ThemeContextProps, 'direction'> {}

export function withDirection<T extends WithDirectionProps>(
WrappedComponent: React.ComponentType<T>,
): React.ComponentType<Subtract<T, WithDirectionProps>> {
): React.ComponentType<Omit<T, keyof WithDirectionProps>> {
const componentName = getComponentName(WrappedComponent);

return class WithDirectionComponent extends React.Component<Subtract<T, WithDirectionProps>> {
static displayName = `withDirection(${componentName})`;
static contextType = ThemeContext;
context!: React.ContextType<typeof ThemeContext>;

render() {
return <WrappedComponent {...(this.props as T)} direction={this.context.direction} />;
}
const component = function WithDirectionComponent(props: Omit<T, keyof WithDirectionProps>) {
const direction = useDirection();
return <WrappedComponent {...(props as T)} direction={direction} />;
};

component.displayName = `withDirection(${componentName})`;

return component;
}
21 changes: 9 additions & 12 deletions src/components/theme/withTheme.tsx
Original file line number Diff line number Diff line change
@@ -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<ThemeContextProps, 'theme'> {}

export function withTheme<T extends WithThemeProps>(
WrappedComponent: React.ComponentType<T>,
): React.ComponentType<Subtract<T, WithThemeProps>> {
): React.ComponentType<Omit<T, keyof WithThemeProps>> {
const componentName = getComponentName(WrappedComponent);

return class WithThemeComponent extends React.Component<Subtract<T, WithThemeProps>> {
static displayName = `withTheme(${componentName})`;
static contextType = ThemeContext;
context!: React.ContextType<typeof ThemeContext>;

render() {
return <WrappedComponent {...(this.props as T)} theme={this.context.theme} />;
}
const component = function WithThemeComponent(props: Omit<T, keyof WithThemeProps>) {
const theme = useTheme();
return <WrappedComponent {...(props as T)} theme={theme} />;
};

component.displayName = `withTheme(${componentName})`;

return component;
}
21 changes: 9 additions & 12 deletions src/components/theme/withThemeValue.tsx
Original file line number Diff line number Diff line change
@@ -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<ThemeContextProps, 'themeValue'> {}

export function withThemeValue<T extends WithThemeValueProps>(
WrappedComponent: React.ComponentType<T>,
): React.ComponentType<Subtract<T, WithThemeValueProps>> {
): React.ComponentType<Omit<T, keyof WithThemeValueProps>> {
const componentName = getComponentName(WrappedComponent);

return class WithThemeValueComponent extends React.Component<Subtract<T, WithThemeValueProps>> {
static displayName = `withThemeValue(${componentName})`;
static contextType = ThemeContext;
context!: React.ContextType<typeof ThemeContext>;

render() {
return <WrappedComponent {...(this.props as T)} themeValue={this.context.themeValue} />;
}
const component = function WithThemeValueComponent(props: Omit<T, keyof WithThemeValueProps>) {
const themeValue = useThemeValue();
return <WrappedComponent {...(props as T)} themeValue={themeValue} />;
};

component.displayName = `withThemeValue(${componentName})`;

return component;
}
9 changes: 7 additions & 2 deletions styles/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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;
}
12 changes: 12 additions & 0 deletions styles/themes/common/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

0 comments on commit 403f5a8

Please sign in to comment.