Skip to content

Commit

Permalink
feat!: <ThemeProvider> add wrapping div (!), rename theme prop to the…
Browse files Browse the repository at this point in the history
…meConfig (#1869)

Co-authored-by: Shlomi Toussia-Cohen <[email protected]>
  • Loading branch information
SergeyRoyt and shlomitc authored Jan 29, 2024
1 parent 59e260b commit 4a52cfa
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 61 deletions.
43 changes: 30 additions & 13 deletions src/components/ThemeProvider/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,55 @@ import {
shouldGenerateTheme
} from "./ThemeProviderUtils";
import { withStaticProps } from "../../types";
import { backwardCompatibilityForProperties } from "../../helpers/backwardCompatibilityForProperties";

export interface ThemeProviderProps {
/**
* The theme to apply, consists of name - name of css class that will be added to the children, which should be unique - and the object of colors overrides for each system theme.
* @deprecated use themeConfig instead
*/
theme?: Theme;
/**
* The theme config to apply, consists of a "name" - the name of css class that will be added to the children, which should be unique, and the object of colors overrides for each system theme.
*/
themeConfig?: Theme;
/**
* The children to render with the theme
*/
children: ReactElement;
/**
* String which adds up to theme name selector to make it more specific (in case if theme.name is colliding with some other class name)
* String which adds up to theme name selector to make it more specific (in case if themeConfig.name is colliding with some other class name)
*/
themeClassSpecifier?: string;
/**
* The system theme to apply to the body element on mount, if there is no theme class name on the body element already
*/
systemTheme?: SystemTheme;
/**
* ClassName to add to the wrapping div
*/
className?: string;
}

const ThemeProvider: FC<ThemeProviderProps> & {
systemThemes?: typeof SystemTheme;
colors?: typeof ThemeColor;
} = ({ theme, children, themeClassSpecifier: customThemeClassSpecifier, systemTheme }) => {
} = ({ themeConfig, theme, children, themeClassSpecifier: customThemeClassSpecifier, systemTheme, className }) => {
const overrideThemeConfig = backwardCompatibilityForProperties([themeConfig, theme]);
const [stylesLoaded, setStylesLoaded] = useState(false);
const themeClassSpecifier = useMemo(
() => customThemeClassSpecifier || generateRandomAlphaString(),
[customThemeClassSpecifier]
);

useEffect(() => {
if (theme) {
console.warn(
"vibe ThemeProvider: theme prop is deprecated and will be removed soon, please use themeConfig prop instead - ",
theme
);
}
}, [theme]);

// Add the systemTheme class name to the body on mount
useLayoutEffect(() => {
if (!systemTheme) {
Expand All @@ -60,18 +79,18 @@ const ThemeProvider: FC<ThemeProviderProps> & {
}, [systemTheme]);

useEffect(() => {
if (!shouldGenerateTheme(theme)) {
if (!shouldGenerateTheme(overrideThemeConfig)) {
return;
}
if (document.getElementById(theme.name)) {
if (document.getElementById(overrideThemeConfig.name)) {
setStylesLoaded(true);
return;
}

const styleElement = document.createElement("style");
styleElement.type = "text/css";
styleElement.id = theme.name;
const themeCssOverride = generateThemeCssOverride(theme, themeClassSpecifier);
styleElement.id = overrideThemeConfig.name;
const themeCssOverride = generateThemeCssOverride(overrideThemeConfig, themeClassSpecifier);

try {
styleElement.appendChild(document.createTextNode(themeCssOverride));
Expand All @@ -84,17 +103,15 @@ const ThemeProvider: FC<ThemeProviderProps> & {
return () => {
document.head.removeChild(styleElement);
};
}, [themeClassSpecifier, theme]);
}, [themeClassSpecifier, overrideThemeConfig]);

if (!stylesLoaded && shouldGenerateTheme(theme)) {
if (!stylesLoaded && shouldGenerateTheme(overrideThemeConfig)) {
// Waiting for styles to load before children render
return null;
}

// Pass the theme name as a class to the children - to scope the effect of the theme
return React.cloneElement(children, {
className: cx(theme?.name, themeClassSpecifier, children?.props?.className)
});
// Pass the theme name as a class to the div wrapping children - to scope the effect of the theme
return <div className={cx(overrideThemeConfig?.name, themeClassSpecifier, className)}>{children}</div>;
};

export default withStaticProps(ThemeProvider, {
Expand Down
16 changes: 8 additions & 8 deletions src/components/ThemeProvider/ThemeProviderUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SystemTheme, SystemThemeClassMap, Theme, ThemeColorTokenValueMap } from "./ThemeProviderConstants";
import { Theme, ThemeColorTokenValueMap, SystemTheme, SystemThemeClassMap } from "./ThemeProviderConstants";

const generateCss = (object: ThemeColorTokenValueMap, stack: string, parentSelector: string) => {
for (const key of Object.keys(object)) {
Expand All @@ -22,22 +22,22 @@ const generateCss = (object: ThemeColorTokenValueMap, stack: string, parentSelec
return stack;
};

export const shouldGenerateTheme = (theme: Theme) => {
return !!theme?.colors && !!theme?.name;
export const shouldGenerateTheme = (themeConfig: Theme) => {
return !!themeConfig?.colors && !!themeConfig?.name;
};

export const generateThemeCssOverride = (theme: Theme, themeClassSpecifier: string) => {
if (!shouldGenerateTheme(theme)) {
export const generateThemeCssOverride = (themeConfig: Theme, themeClassSpecifier: string) => {
if (!shouldGenerateTheme(themeConfig)) {
return null;
}

let css = "";
for (const systemTheme of Object.keys(theme.colors) as SystemTheme[]) {
for (const systemTheme of Object.keys(themeConfig.colors) as SystemTheme[]) {
css +=
generateCss(
theme.colors[systemTheme],
themeConfig.colors[systemTheme],
"",
`.${SystemThemeClassMap[systemTheme]} .${themeClassSpecifier}.${theme.name}`
`.${SystemThemeClassMap[systemTheme]} .${themeClassSpecifier}.${themeConfig.name}`
) + "\n";
}

Expand Down
12 changes: 10 additions & 2 deletions src/components/ThemeProvider/__stories__/ThemeProvider.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Canvas, Meta, Story } from "@storybook/blocks";
import {
DescriptionWithLinkMondaySdkIntegration,
ThemeProviderNegativeExampleTemplate,
ThemeProviderPositiveExampleTemplate,
TipDev,
Expand All @@ -17,12 +18,13 @@ import * as ThemeProviderStories from "./ThemeProvider.stories";
- [Props](#props)
- [Usage](#usage)
- [Variants](#variants)
- [Use cases and examples](#use-cases-and-examples)
- [Do’s and don’ts](#dos-and-donts)
- [Feedback](#feedback)

## Overview

This component helps to customize the colors of library's components by overriding a specific css variables with a values provided within theme object.
This component helps to customize the colors of library's components by overriding a specific css variables with a values provided within <code>themeConfig</code> object.
There are 2 levels of theming: **system theme** and **product theme**.
System theme is a one of a 3 predefined themes: <code>light</code>(<code>.light-app-theme</code>), <code>dark</code>(<code>.dark-app-theme</code>) and <code>black</code>(<code>.black-app-theme</code>).
Product theme is a custom theme that can be provided by a user, there you can specify the values of specific color tokens for each of the system themes.
Expand Down Expand Up @@ -53,7 +55,7 @@ There are 3 system themes <code>light</code>, <code>dark</code> and <code>black<

### Theming scope

Only components wrapped with ThemeProvider will be affected by the theme.
Only components wrapped with ThemeProvider will be affected by the <code>themeConfig</code>.

<Canvas>
<Story of={ThemeProviderStories.ThemingScope} />
Expand Down Expand Up @@ -91,6 +93,12 @@ It will set the <code>systemTheme</code> on <code>body</code> tag and will apply

<Canvas of={ThemeProviderStories.WithSystemTheme} />

### Integration with monday.com SDK in external applications

<DescriptionWithLinkMondaySdkIntegration />

<Story of={ThemeProviderStories.MondaySdkIntegration} />

## Do’s and Don’ts

<ComponentRules
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { useState } from "react";
import { useState } from "react";
import ThemeProvider, { ThemeProviderProps } from "../ThemeProvider";
import Button from "../../Button/Button";
import Dropdown from "../../Dropdown/Dropdown";
import Flex from "../../Flex/Flex";
import { productTheme1, productTheme2, productTheme3, productTheme4 } from "./product-themes";
import ColorsDescription from "../../../storybook/stand-alone-documentaion/colors/colors-description/colors-description";
import { ThemeColor } from "../ThemeProviderConstants";
import { Tip, UsageGuidelines } from "vibe-storybook-components";
import { Link, Tip, UsageGuidelines } from "vibe-storybook-components";
import styles from "./ThemeProvider.stories.module.scss";

export const ColorsEligibleForThemingTemplate = () => <ColorsDescription colorNames={Object.values(ThemeColor)} />;
Expand All @@ -25,7 +25,7 @@ export const ThemeProviderThemingScopeTemplate = (_args: JSX.IntrinsicAttributes
return (
<>
<ThemeProvider
theme={{
themeConfig={{
name: "theming-scope-theme",
colors: {
[ThemeProvider.systemThemes.LIGHT]: {
Expand Down Expand Up @@ -53,7 +53,7 @@ export const ThemeProviderThemingScopeTemplate = (_args: JSX.IntrinsicAttributes
export const ThemeProviderFoldedThemingTemplate = (_args: JSX.IntrinsicAttributes & ThemeProviderProps) => {
return (
<ThemeProvider
theme={{
themeConfig={{
name: "outer-theme",
colors: {
[ThemeProvider.systemThemes.LIGHT]: {
Expand All @@ -65,7 +65,7 @@ export const ThemeProviderFoldedThemingTemplate = (_args: JSX.IntrinsicAttribute
>
<div>
<ThemeProvider
theme={{
themeConfig={{
name: "inner-theme",
colors: {
[ThemeProvider.systemThemes.LIGHT]: {
Expand All @@ -92,7 +92,7 @@ export const ThemeProviderProductThemingTemplate = (_args: JSX.IntrinsicAttribut
const [selectedTheme, setSelectedTheme] = useState(null);

return (
<ThemeProvider theme={selectedTheme?.value}>
<ThemeProvider themeConfig={selectedTheme?.value}>
<Flex gap={Flex.gaps.LARGE} align={Flex.align.START} wrap className={styles.productThemingContainer}>
<Dropdown
// @ts-ignore
Expand All @@ -115,7 +115,7 @@ export const ThemeProviderProductThemingTemplate = (_args: JSX.IntrinsicAttribut
export const ThemeProviderCustomClassTemplate = (_args: JSX.IntrinsicAttributes & ThemeProviderProps) => {
return (
<ThemeProvider
theme={{
themeConfig={{
name: "theme-with-custom-class-selector",
colors: {
[ThemeProvider.systemThemes.LIGHT]: {
Expand All @@ -142,7 +142,7 @@ export const ThemeProviderCustomClassTemplate = (_args: JSX.IntrinsicAttributes
export const ThemeProviderPositiveExampleTemplate = () => {
return (
<ThemeProvider
theme={{
themeConfig={{
name: "positive-example-theme",
colors: {
[ThemeProvider.systemThemes.LIGHT]: {
Expand All @@ -160,7 +160,7 @@ export const ThemeProviderPositiveExampleTemplate = () => {
export const ThemeProviderNegativeExampleTemplate = () => {
return (
<ThemeProvider
theme={{
themeConfig={{
name: "negative-example-theme",
colors: {
[ThemeProvider.systemThemes.LIGHT]: {
Expand All @@ -186,17 +186,61 @@ export const UsageGuidelinesThemeProvider = () => (
guidelines={[
<>
Control themes in your application by setting theme classes (e.g. <code>.light-app-theme</code>) on your{" "}
<code>body</code> and render everything else inside it
<code>body</code> and render everything else inside it. Or use <code>systemTheme</code> prop to make
ThemeProvider set the theme class on the <code>body</code> for you.
</>,
<>
In most common case ThemeProvider should be rendered only once on the root level of the application - below the{" "}
<code>body</code>
<code>body</code>.
</>,
<>
ThemeProvider is populating theme name <code>className</code> to {"it's child, so don't put "}
<code>{"<Fragment>"}</code> (<code>{"<>"}</code>) inside - {` it's not accepting `}
<code>className</code> prop
ThemeProvider is populating theme name <code>className</code> to the new added <code>div</code> container which
wraps the children.
</>
]}
/>
);

export const DescriptionWithLinkMondaySdkIntegration = () => (
<>
When developing an external application for monday.com (iframe). You can use <code>ThemeProvider</code> in
combination with the{" "}
<Link
href="https://developer.monday.com/apps/docs/mondayget#sample-context-objects-for-each-feature-type"
withoutSpacing
>
monday.com SDK
</Link>
, to apply monday.com <b>system</b> and <b>product</b> themes to your application. This will allow your application
to be consistent with the monday.com UI.
</>
);

export const MondaySdkIntegrationSourceCode = `
import { ThemeProvider } from "monday-ui-react-core";
import mondaySdk from "monday-sdk-js";
const monday = mondaySdk();
const useGetContext = () => {
const [context, setContext] = useState({});
useEffect(() => {
monday.listen("context", (res) => {
setContext(res.data);
});
}, []);
return context;
};
const AppWrapper = () => {
const context = useGetContext();
return (
<ThemeProvider themeConfig={context.themeConfig} systemTheme={context.theme}>
<App />
</ThemeProvider>
);
};
`;
13 changes: 11 additions & 2 deletions src/components/ThemeProvider/__stories__/ThemeProvider.stories.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useState } from "react";
import { Source } from "@storybook/blocks";
import ThemeProvider from "../ThemeProvider";
import { createStoryMetaSettingsDecorator } from "../../../storybook";
import {
ColorsEligibleForThemingTemplate,
MondaySdkIntegrationSourceCode,
ThemeProviderCustomClassTemplate,
ThemeProviderFoldedThemingTemplate,
ThemeProviderProductThemingTemplate,
Expand Down Expand Up @@ -32,7 +34,7 @@ export const Overview = {
name: "Overview",

args: {
theme: {
themeConfig: {
name: "overview-theme",
colors: {
[ThemeProvider.systemThemes.LIGHT]: {
Expand Down Expand Up @@ -97,7 +99,7 @@ export const WithSystemTheme = {
return (
<Flex direction={Flex.directions.ROW} gap={Flex.gaps.LARGE}>
<ThemeProvider
theme={{
themeConfig={{
name: "with-system-theme",
colors: {
[ThemeProvider.systemThemes.DARK]: {
Expand All @@ -118,3 +120,10 @@ export const WithSystemTheme = {
name: "With systemTheme",
play: themeProviderSystemThemeSuite
};

export const MondaySdkIntegration = {
render: () => {
return <Source code={MondaySdkIntegrationSourceCode}></Source>;
},
name: "monday.com SDK integration"
};
Loading

0 comments on commit 4a52cfa

Please sign in to comment.