Apply a different theme on a per story basis storybook - javascript

I am making a library of react components in storybook (v6.1.14)
I have two different themes, basically and light and dark version.
I wrap all of my stories in the <ThemeProvider> like this:
import React from "react"
import {dark} from "../src/Themes/Theme";
import { ThemeProvider } from "styled-components";
import { GlobalStyles } from "../src/lib/Themes";
// apply our projects theme to our stories
const ThemeDecorator = storyFn => (
<ThemeProvider theme={dark}>
<GlobalStyles />
{storyFn()}
</ThemeProvider>
)
export default ThemeDecorator;
This is applied through the preview.js file like so:
const { addDecorator } = require("#storybook/react");
import ThemeDecorator from "./themeDecorator"
// Add our project theme, add a global stylesheet
addDecorator(ThemeDecorator);
So now everything has the dark theme applied, however, for each component I want to show two stories: One for the light theme and one for the dark theme - how do I make a story take on a different theme to the one globally declared?

I had the same problem as well. From the Storybook docs, I found this answer.
So basically, you can use decorators with your template and apply styling to it.
const Template: Story<LogoProps> = (args) => {
const { version } = args;
return <Logo {...args} version={version} />;
};
export const Light = Template.bind({});
Light.args = {
version: 'light',
};
Light.decorators = [
(LightLogo) => (
<div style={{ backgroundColor: 'black', padding: '5px 7px' }}>
<LightLogo />
</div>
),
];

Related

Material ui use palette primary color based on selected theme mode

While using mui react, I want to color some non material ui custom components made with div using colors from my theme's primary palette.
I currently can use theme.palette.primary.main or the .light colors directly, including the theme.palette.text.primary for texts colors.
But if I change the theme to dark mode, I will have to change the color reference as well, by checking on theme.mode with conditionals like the following:
<div
style={{
backgroundColor:
theme.palette.mode == "light"
? theme.palette.primary.dark
: theme.palette.primary.light
}}
>
Test
</div>
So is there a way we can make this work just like in the material ui components? Passing theme.palette.primary will work with theme mode changes?
Maybe something simpler like:
<div style={{ backgroundColor: theme.palette.primary }}></div>
You could use Context to save your theme setting globally and also you need to separate the themes like light and dark.
// SettingsContext.js
import React, {
createContext,
useState
} from 'react';
const defaultSettings = {
// you could add sort of global settings here
theme: 'light'
};
const SettingsContext = createContext({
settings: defaultSettings,
saveSettings: () => {}
});
export const SettingsProvider = ({ settings, children }) => {
const [currentSettings, setCurrentSettings] = useState(settings || defaultSettings);
return (
<SettingsContext.Provider
value={{
settings: currentSettings,
}}
>
{children}
</SettingsContext.Provider>
);
};
export const SettingsConsumer = SettingsContext.Consumer;
export default SettingsContext;
You can build the settings context as a hook but you can skip this.
// useSettings.js
import { useContext } from 'react';
import SettingsContext from './SettingsContext';
const useSettings = () => useContext(SettingsContext);
export default useSettings;
And then try to create your custom theme which includes the dark and light mode.
// theme.js
import { createTheme as createMuiTheme } from '#mui/material/styles';
const themes = {
'light': {
...
},
'dark': {
...
}
];
export const createTheme = (name) => {
return createMuiTheme(themes[name]);
}
After that, you need to pass the theme which you have selected in App.js or index.js whatever top level file.
// App.js
...
import { ThemeProvider } from '#mui/material/styles';
import useSettings from './useSettings';
import { createTheme } from './theme.js';
const App = () => {
const { settings } = useSettings();
const theme = createTheme(settings.theme);
return (
<ThemeProvider theme={theme}>
...
</ThemeProvider>
);
};
export default App;
...
That's all.
Now you can use the selected theme without conditional render in your components.
<div style={{ backgroundColor: theme.palette.primary }}></div>
But this will not prevent the selected theme after hard refresh.
So if you want to keep the theme as selected even after refresh your browser, then you could save the theme setting in the localStorage.

Storybook not showing styles

I have a dialog component which is using the Primereact dialog internally. When I make a storybook for the same, the custom css for button is being imported as it is imported inside dialog.jsx. But the default css of Primereact dialog is not loading and reflecting in the storybook. Although it is being loaded in my React app.
dialogComp.jsx
import { Dialog } from "primereact/dialog";
const DialogComp = (props) => {
return (
<Dialog
className="dialog-modal"
header={props.header}
visible={true}
>
{props.children}
</Dialog>
);
};
export default DialogModal;
dialog.storybook.js
import React from "react";
import DialogModal from "./dialogComp";
import { addDecorator, addParameters } from "#storybook/react";
import { Store, withState } from "#sambego/storybook-state";
import { store } from "./../../utils/storyStore";
const DialogModalComp = (props) => {
return [
<div>
<DialogModal
header="Dialog Modal"
displayModal={true}
>
Modal content
</DialogModal>
</div>,
];
};
addDecorator(withState());
addParameters({
state: {
store,
},
});
export default {
title: "dialog",
};
export const DialogModalComponent = () => DialogModalComp;
storybook---main.js
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.#(js|jsx|ts|tsx)"
],
"addons": [
"#storybook/addon-links",
"#storybook/addon-essentials",
"#storybook/preset-create-react-app"
]
}
Am I missing something in the configuration?
You'll need to import any styles you use in App.js globally in Storybook, by importing them in .storybook/preview.js (create the file if it doesn't already exist).
Every component in React is self contained - your DialogModal component won't get styled because in Storybook it is not being rendered within your App component (where you're importing your styles).
To simulate your app when using Storybook, you import the css in a preview.js file.
Docs:
To control the way stories are rendered and add global decorators and
parameters, create a .storybook/preview.js file. This is loaded in the
Canvas tab, the “preview” iframe that renders your components in
isolation. Use preview.js for global code (such as CSS imports or
JavaScript mocks) that applies to all stories.
TL;DR
import your styles in .storybook/preview.js
import "../src/index.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
If you use storybook and emotion, and if you implement Global styles or Theming, you may add a decorator into the .storybook/preview.js like this:
I'm using Create React App, therefore I'm using jsxImportSource
/** #jsxImportSource #emotion/react */
import { Global } from '#emotion/react'
import { GlobalStyles } from '../src/styles'
const withGlobalProvider = (Story) => (
<>
<Global styles={GlobalStyles} />
<Story />
</>
)
export const decorators = [withGlobalProvider]
You may find more information on: https://storybook.js.org/docs/react/essentials/toolbars-and-globals#global-types-and-the-toolbar-annotation

How can I customize the style of a React component shared between lazy-loaded pages?

I'm building a React application and I started using CRA. I configured the routes of the app using React Router. Pages components are lazy-loaded.
There are 2 pages: Home and About.
...
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
...
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/about" component={About} />
<Route path="/" component={Home} />
</Switch>
</Suspense>
...
Each page uses the Button component below.
import React from 'react';
import styles from './Button.module.scss';
const Button = ({ children, className = '' }) => (
<button className={`${styles.btn} ${className}`}>{children}</button>
);
export default Button;
The Button.module.scss file just sets the background color of the button to red.
.btn {
background: red;
}
The Button component accepts a className prop which is then added to the rendered button. This is because I want to give freedom to the consumer of the component. For example, in some pages margins could be needed or the background should be yellow instead of red.
To make it simple, I just want to have a different background color for the Button based on the current page, so that:
Home page => Blue button
About page => Yellow button
Each page is defined as below:
import React from 'react';
import Button from './Button';
import styles from './[PageName].module.scss';
const [PageName] = () => (
<div>
<h1>[PageName]</h1>
<Button className={styles.pageBtn}>[ExpectedColor]</Button>
</div>
);
export default [PageName];
where [PageName] is the name of the page and [ExpectedColor] is the corresponding expected color based on the above bullet list (blue or yellow).
The imported SCSS module, exports a class .pageBtn which sets the background property to the desired color.
Note: I could use a prop on the Button component which defines the variant to display (Blue/Yellow) and based on that prop add a class defined in the SCSS file. I don't want to do that since the change could be something that doesn't belong to a variant (e.g. margin-top).
The problem
If I run the application using yarn start, the application works fine. However, if I build the application (yarn build) and then I start serving the application (e.g. using serve -s build), the behavior is different and the application doesn't work as expected.
When the Home page is loaded, the button is correctly shown with a blue background. Inspecting the loaded CSS chunk, it contains:
.Button_btn__2cUFR {
background: red
}
.Home_pageBtn__nnyWK {
background: blue
}
That's fine. Then I click on the navigation link to open the About page. Even in this case, the button is shown correctly with a yellow background. Inspecting the loaded CSS chunk, it contains:
.Button_btn__2cUFR {
background: red
}
.About_pageBtn__3jjV7 {
background: yellow
}
When I go back to the Home page, the button is now displayed with a red background instead of yellow. That's because the About page has loaded the CSS above which defines again the Button_btn__2cUFR class. Since the class is now after the Home_pageBtn__nnyWK class definition, the button is displayed as red.
Note: the Button component is not exported on the common chunk because its size is too small. Having that in a common chunk could solve the problem. However, my question is about small shared components.
Solutions
I have thought to 2 solutions which, however, I don't like too much:
Increase selectors specificity
The classes specified in the [PageName].module.scss could be defined as:
.pageBtn.pageBtn {
background: [color];
}
This will increase the selector specificity and will override the default Button_btn__2cUFR class. However, each page chunk will include the shared components in case the component is quite small (less than 30kb). Also, the consumer of the component has to know that trick.
Eject and configure webpack
Ejecting the app (or using something like react-app-rewired) would allow specifying the minimum size for common chunk using webpack. However, that's not what I would like for all the components.
To summarize, the question is: what is the correct working way of overriding styles of shared components when using lazy-loaded routes?
You can use the following logic with config file for any pages. Also, You can send config data from remote server (req/res API) and handle with redux.
See Demo: CodeSandBox
create components directory and create files like below:
src
|---components
|---Button
| |---Button.jsx
| |---Button.module.css
Button Component:
// Button.jsx
import React from "react";
import styles from "./Button.module.css";
const Button = props => {
const { children, className, ...otherProps } = props;
return (
<button className={styles[`${className}`]} {...otherProps}>
{children}
</button>
);
};
export default Button;
...
// Button.module.css
.Home_btn {
background: red;
}
.About_btn {
background: blue;
}
create utils directory and create AppUtils.js file:
This file handle config files of pages and return new object
class AppUtils {
static setRoutes(config) {
let routes = [...config.routes];
if (config.settings) {
routes = routes.map(route => {
return {
...route,
settings: { ...config.settings, ...route.settings }
};
});
}
return [...routes];
}
static generateRoutesFromConfigs(configs) {
let allRoutes = [];
configs.forEach(config => {
allRoutes = [...allRoutes, ...this.setRoutes(config)];
});
return allRoutes;
}
}
export default AppUtils;
create app-configs directory and create routesConfig.jsx file:
This file lists and organizes routes.
import React from "react";
import AppUtils from "../utils/AppUtils";
import { pagesConfig } from "../pages/pagesConfig";
const routeConfigs = [...pagesConfig];
const routes = [
...AppUtils.generateRoutesFromConfigs(routeConfigs),
{
component: () => <h1>404 page not found</h1>
}
];
export default routes;
Modify index.js and App.js files to:
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
rootElement
);
...
react-router-config: Static route configuration helpers for React
Router.
// App.js
import React, { Suspense } from "react";
import { Switch, Link } from "react-router-dom";
import { renderRoutes } from "react-router-config";
import routes from "./app-configs/routesConfig";
import "./styles.css";
export default function App() {
return (
<div className="App">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
<Suspense fallback={<h1>loading....</h1>}>
<Switch>{renderRoutes(routes)}</Switch>
</Suspense>
</div>
);
}
create pages directory and create files and subdirectory like below:
src
|---pages
|---about
| |---AboutPage.jsx
| |---AboutPageConfig.jsx
|
|---home
|---HomePage.jsx
|---HomePageConfig.jsx
|
|---pagesConfig.js
About Page files:
// AboutPage.jsx
import React from "react";
import Button from "../../components/Button/Button";
const AboutPage = props => {
const btnClass = props.route.settings.layout.config.buttonClass;
return (
<>
<h1>about page</h1>
<Button className={btnClass}>about button</Button>
</>
);
};
export default AboutPage;
...
// AboutPageConfig.jsx
import React from "react";
export const AboutPageConfig = {
settings: {
layout: {
config: {
buttonClass: "About_btn"
}
}
},
routes: [
{
path: "/about",
exact: true,
component: React.lazy(() => import("./AboutPage"))
}
]
};
Home Page files:
// HomePage.jsx
import React from "react";
import Button from "../../components/Button/Button";
const HomePage = props => {
const btnClass = props.route.settings.layout.config.buttonClass;
return (
<>
<h1>home page</h1>
<Button className={btnClass}>home button</Button>
</>
);
};
export default HomePage;
...
// HomePageConfig.jsx
import React from "react";
export const HomePageConfig = {
settings: {
layout: {
config: {
buttonClass: "Home_btn"
}
}
},
routes: [
{
path: "/",
exact: true,
component: React.lazy(() => import("./HomePage"))
}
]
};
...
// pagesConfig.js
import { HomePageConfig } from "./home/HomePageConfig";
import { AboutPageConfig } from "./about/AboutPageConfig";
export const pagesConfig = [HomePageConfig, AboutPageConfig];
Edited section:
With HOC Maybe this way: CodeSandBox
create hoc dir and withPage.jsx file:
src
|---hoc
|---withPage.jsx
...
// withPage.jsx
import React, { useEffect, useState } from "react";
export function withPage(Component, path) {
function loadComponentFromPath(path, setStyles) {
import(path).then(component => setStyles(component.default));
}
return function(props) {
const [styles, setStyles] = useState();
useEffect(() => {
loadComponentFromPath(`../pages/${path}`, setStyles);
}, []);
return <Component {...props} styles={styles} />;
};
}
And then pages like below:
src
|---pages
|---about
| |---About.jsx
| |---About.module.css
|
|---home
|---Home.jsx
|---Home.module.css
About.jsx file:
// About.jsx
import React from "react";
import { withPage } from "../../hoc/withPage";
const About = props => {
const {styles} = props;
return (
<button className={styles && styles.AboutBtn}>About</button>
);
};
export default withPage(About, "about/About.module.css");
About.module.css file:
// About.module.css
.AboutBtn {
background: yellow;
}
Home.jsx file:
// Home.jsx
import React from "react";
import { withPage } from "../../hoc/withPage";
const Home = props => {
const { styles } = props;
return <button className={styles && styles.HomeBtn}>Home</button>;
};
export default withPage(Home, "home/Home.module.css");
Home.module.css file:
// Home.module.css
.HomeBtn {
background: red;
}
I would suggest instead of adding both the default styles and the consumer styles, use the consumer's styles over yours and use your as a callback if not supplied. The consumer can still compose your defaults with the composes keyword.
Button.js
import React from 'react';
import styles from './Button.module.scss';
const Button = ({ children, className}) => (
<button className={className ?? styles.btn}>{children}</button>
);
export default Button;
SomePage.module.scss
.pageBtn {
// First some defaults
composes: btn from './Button.module.scss';
// And override some of the defautls here
background: yellow;
}
If you wish, use sass #extends or #mixin instead
EDIT: Haven't tested it, but could it be that just by using composes webpack will make sure to bundle the defaults only once? Thus you're no longer needed to change your Button.js code with the ??
Solution 1
I know this is very obvious, but would work anyway:
Set !important on your overwriting css rules, thus bypassing specificity:
[PageName].module.scss:
.btn {
color: yellow !important;
}
However, most of the strict devs I know would avoid this keyword at all cost.
Why ?
Because when you start to have a lot of !important your css is a nightmare to debug. If you start writing !important rules with higher specificity, you know you have gone too far
It is only meant for corner-cases like yours, you might as well use it.
Solution 2
fix CRA config to enforce style tags order.
It is open-source after all :)
You can give your input on this bug here (upvote might give it more visibility):
https://github.com/facebook/create-react-app/issues/7190
Solution 3 (Update)
You could create a SCSS mixin in a new customButton.scss file, to generate css rules with higher specificity:
// customButton.scss
#mixin customBtn() {
:global {
.customBtn.override {
#content;
}
}
}
We will use two static class names (using the :global selector), because that way their name won't change based on where they are imported from.
Now use that mixin in your pages' SCSS:
// [pageName].module.scss
#import 'customButton.scss';
#include customBtn {
color: yellow;
}
css output should be:
.customBtn.override {
// put everything you want to customize here
color: yellow;
}
In Button.jsx: apply both class names to your button in addition to styles.btn:
// Button.jsx
const Button = ({ children, className = '' }) => (
<button className={`${styles.btn} customBtn override ${className}`}>
{children}
</button>
);
(Note that these are not referenced through the styles object, but the classname directly)
The main drawback is these are not dynamic class names, so you have to watch out to avoid conflicts yourself like we use to do before.
But I think it should do the trick

Why is it impossible to style a success button in #material-ui

I am using https://material-ui.com/
My objective is to get a success Button, and Chip. Does anyone know how to do this without these hacky solutions: Material-ui #next customize button colors?
The goal isn't to create a styled component, the goal is to be able to use <Button color="success" /> or <Chip color="success" />.
I have tried <Box bgcolor="success.main">{ props => <Chip {...props} />}</Box> to no avail. I would be OK with using Box but not even that works.
It seems a bit ridiculous that these basic UI things are so cumbersome with this library. Success and Error colors should be a default, and they seem to be missing from every single component in this library. Am I missing something?
This is not a full/working example, but hopefully it provides some context. The Main component is included (eventually) via an AuthenticatedRoute within the Router shown in App.js. It doesn't really add anything to show all that complexity, but I wanted to give you some context.
Steps:
Create theme using createMuiTheme.
Add a ThemeProvider component with a theme prop that accepts your theme. This should be a parent to any Component that will make use of the theme, ours is in App.js.
Use the optional argument that makeStyles has to pass your theme into your Component (Main in my example). You can see examples where we access theme properties such as theme.palette.brand.primary.
Use the useStyles hook to create a variable in your component.
Apply those styles to your Component's elements via the className prop. For example, <div className={classes.root}>...</div>
Repeat 3-5 on any Component that needs styling.
I'm still learning my way around Material UI; we just recently migrated from react bootstrap to MUI. We still have some *.scss files laying around, but everything can co-exist. Our eventual plan is to move everything into the makeStyles hooks, but we haven't had time to refactor everything yet.
As a non-CSS guru, I much prefer to write the styling as code (even if I have to write some boilerplate) than to deal with .scss files and specificity/inheritance.
Theme.js:
import { createMuiTheme } from '#material-ui/core/styles';
export default createMuiTheme({
palette: {
brand: {
primary: '#1b3c6b',
primaryLight: '#adceff',
secondary: '#2a5da6',
tertiary: '#bf1e2e',
tertiaryLight: '#c35f69',
},
},
typography: {
color: '#333',
},
sidebar: {
header: {
background: '#2a5da6',
},
content: { background: '#fff' },
},
status: {
danger: 'orange',
},
});
Main.js (partial):
const useStyles = makeStyles(theme => ({
root: {
flexGrow: 1,
},
appBar: {
background: theme.palette.brand.primary,
boxShadow: 'none',
'& .MuiTabs-indicator': {
backgroundColor: theme.palette.brand.primaryLight,
height: 3,
},
position: 'fixed',
zIndex: 1000,
},
}));
const Main = React.memo(props => {
const classes = useStyles();
...
return (
<div className={classes.root}>
<AppBar className={classes.appBar} position="static">
....
</AppBar>
</div>
);
}
App.js (partial):
import { CssBaseline, ThemeProvider } from '#material-ui/core';
import theme from 'Theme/Theme'; // Theme.js from above
...
class App extends Component {
...
render() {
...
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Router>
...
</Router>
</ThemeProvider>
);
}
...
}

Is there a way to theme with emotion without "styled"

With emotion (https://github.com/emotion-js/emotion) I know I can theme with css and styled together with something like:
const carouselCss = ({ theme }) => css`
.. css properties etc
`;
const CarouselDiv = styled('div')`
${carouselCss};
`;
Then in JSX:
<ThemeProvider theme={{ stuff: 'blah' }}>
<CarouselDiv>
..
</CarouselDiv>
</ThemeProvider>
But is there any elegant way to theme with only css - prefer not to use componentized styles because I want to keep to semantic html elements in JSX (also have a massive code base and its easier to migrate from sass to emotion without having to use styled)
I know I can do something like:
const carouselCss = (theme) => css`
.. css properties etc
`;
then jsx:
<div className={carouselCss(this.props.theme)}>
..
</div>
But it means passing a theme prop all the time from the component - which is a little cumbersome
Is there a better way to do this ? Somehow wrap css with something so it has theme vars injected ?
ThemeProvider will get you that. Here is an example with both styled and css options shown :
/** #jsx jsx */
import { jsx } from '#emotion/core'
import styled from '#emotion/styled'
import { ThemeProvider } from 'emotion-theming'
const theme = {
colors: {
primary: 'hotpink'
}
}
const SomeText = styled.div`
color: ${props => props.theme.colors.primary};
`
render(
<ThemeProvider theme={theme}>
<SomeText>some text</SomeText>
<div css={theme => ({ color: theme.colors.primary })}>
some other text
</div>
</ThemeProvider>
)
You could use theme HoC. But if your concern is prop passing to slider, this HoC is basically doing the same, i.e. injects theme into this.props.theme.
Personally, i would use ThemeProvider API if possible, maybe with function-style theme, as pointed out in the docs:
// function-style theme; note that if multiple <ThemeProvider> are used,
// the parent theme will be passed as a function argument
const adjustedTheme = ancestorTheme => ({ ...ancestorTheme, color: 'blue' });

Categories

Resources