How to switch between themes in Ant design v4 dynamically? - css

I'd like to implement switching between dark/light theme dynamically with Ant design v4.
It's possible to customize the theme with other CSS/LESS imports as it's written here:
https://ant.design/docs/react/customize-theme#Use-dark-theme
But I'm not sure how to switch between those themes dynamically from the code. I have a variable in my React app (darkMode) which indicates if the dark theme is currently used. I have to provide correct CSS files when this variable is changed. But I can't import CSS dynamically only when some condition is fulfilled, because it's not way how the imports work.
I tried to do something messy with require like in the following code, but it's a very very bad approach and it's still not working properly (because CSS is injected but probably not withdrawn.
):
const Layout = () => {
...
useEffect(() => {
if (darkMode === true) {
require("./App.dark.css")
} else {
require("./App.css")
}
}, [darkMode])
return (
<Home />
)
}
It should be possible to switch themes somehow because it's already implemented in Ant design docs (https://ant.design/components/button/):
Do you have any idea how to do it?
Thanks!

This is what I am using for now -
PS -
I don't know if this will yield optimal bundle size.
changing theme results in a page reload.
make a folder called "themes" - it would have 6 files -> dark-theme.css, dark-theme.jsx, light-theme.css, light-theme.jsx, use-theme.js, theme-provider.jsx. Each of them is described below.
dark-theme.css
import "~antd/dist/antd.dark.css";
dark-theme.jsx
import "./dark-theme.css";
const DarkTheme = () => <></>;
export default DarkTheme;
light-theme.css
#import "~antd/dist/antd.css";
light-theme.jsx
import "./light-theme.css";
const LightTheme = () => <></>;
export default LightTheme;
use-theme.js A custom hook that different components can use -
import { useEffect, useState } from "react";
const DARK_MODE = "dark-mode";
const getDarkMode = () => JSON.parse(localStorage.getItem(DARK_MODE)) || false;
export const useTheme = () => {
const [darkMode, setDarkMode] = useState(getDarkMode);
useEffect(() => {
const initialValue = getDarkMode();
if (initialValue !== darkMode) {
localStorage.setItem(DARK_MODE, darkMode);
window.location.reload();
}
}, [darkMode]);
return [darkMode, setDarkMode];
};
theme-provider.jsx
import { lazy, Suspense } from "react";
import { useTheme } from "./use-theme";
const DarkTheme = lazy(() => import("./dark-theme"));
const LightTheme = lazy(() => import("./light-theme"));
export const ThemeProvider = ({ children }) => {
const [darkMode] = useTheme();
return (
<>
<Suspense fallback={<span />}>
{darkMode ? <DarkTheme /> : <LightTheme />}
</Suspense>
{children}
</>
);
};
change index.js to -
ReactDOM.render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>,
document.getElementById("root")
);
now, in my navbar suppose I have a switch to toggle the theme. This is what it would look like -
const [darkMode, setDarkMode] = useTheme();
<Switch checked={darkMode} onChange={setDarkMode} />

you must create 2 components
the first one :
import './App.dark.css'
const DarkApp =() =>{
//the app container
}
and the second :
import './App.light.css'
const LightApp =() =>{
//the app container
}
and create HOC to handle darkMode like this :
const AppLayout = () =>{
const [isDark , setIsDark] = useState(false);
return (
<>
{
isDark ?
<DarkApp /> :
<LightApp />
}
</>
)
}

ANTD internally uses CSS variables for the "variable.less" file. We can override these variables to make the default variables have dynamic values at runtime.
This is one way it can be achieved:
app-colors.less file:
:root{
--clr-one: #fff;
--clr-two: #000;
--clr-three: #eee;
}
// Dark theme colors
[data-thm="dark"]{
--clr-one: #f0f0f0;
--clr-two: #ff0000;
--clr-three: #fff;
}
We can now override the default ANTD variables in antd-overrides.less file:
#import <app-colors.less file-path>;
#primary-color: var(--clr-one);
#processing-color: var(--clr-two);
#body-background: var(--clr-three);
We usually import the antd.less file to get the antd styles, We would change that to "antd.variable.less"(to use css variables).
index.less file:
#import "~antd/dist/antd.variable.less";
#import <antd-overrides.less file-path>;
Now we need to toggle the "data-thm" attribute on a parent container(body tag recommended) to change the set of CSS variables that get used.
const onThemeToggle = (themeType) => {
const existingBodyAttribute = document.body.getAttribute("data-thm");
if (themeType === "dark" && existingBodyAttribute !== "dark") {
document.body.setAttribute("data-thm", "dark");
} else if (themeType === "light" && existingBodyAttribute) {
document.body.removeAttribute("data-thm");
}
};
The above piece of code can be called on a Theme toggle button or during component mount.

In Ant's example one suggestion is to import your "dark mode" CSS or LESS file into your main style sheet.
// inside App.css
#import '~antd/dist/antd.dark.css';
Instead of trying to toggle stylesheets, the "dark" styles are combined with base styles in one stylesheet. There are different ways to accomplish this, but the common pattern will be:
have a dark-mode selector of some sort in your CSS
put that selector in your HTML
have a way to toggle it on or off.
Here is a working example:
https://codesandbox.io/s/compassionate-elbakyan-f7tun?file=/src/App.js
In this example, toggling the state of darkMode will add or remove a dark-mode className to the top level container.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [darkMode, setDarkMode] = useState(false);
return (
<div className={`App ${darkMode && "dark-mode"}`}>
<label>
<input
type="checkbox"
checked={darkMode}
onChange={() => setDarkMode((darkMode) => !darkMode)}
/>
Dark Mode?
</label>
<h1>Hello CodeSandbox</h1>
</div>
);
}
If darkMode is true, and the dark-mode className is present, those styles will be used:
h1 {
padding: 0.5rem;
border: 3px dotted red;
}
.dark-mode {
background: black;
color: white;
}
.dark-mode h1 {
border-color: aqua;
}

Ant Design newly start to support dynamic theme support. But its on experimental usage. You can find details on this link.

Conditional require won't block using previously required module. So, whenever your condition matches the require will available in your app. So, your both required module will be used. Instead of requiring them, insert stylesheet and remove to toggle between them:
const head = document.head
const dark = document.createElement('link')
const light = document.createElement('link')
dark.rel = 'stylesheet'
light.rel = 'stylesheet'
dark.href = 'antd.dark.css'
light.href = 'antd.light.css'
useEffect(() => {
const timer = setTimeout(() => {
if (darkMode) {
if (head.contains(light)) {
head.removeChild(light)
}
head.appendChild(dark)
} else {
if (head.contains(dark)) {
head.removeChild(dark)
}
head.appendChild(light)
}
}, 500)
return () => clearTimeout(timer)
}, [darkMode])

This package will help you to export and use theme vars without losing performance

Using less compiler in runtime:
https://medium.com/#mzohaib.qc/ant-design-dynamic-runtime-theme-1f9a1a030ba0
Import less code into wrapper
https://github.com/less/less.js/issues/3232
.any-scope {
#import url('~antd/dist/antd.dark.less');
}

Related

How can I test css properties for a React component using react-testing-library?

The philosophy behind the react-testing-library makes sense to me, but I am struggling to apply it to css properties.
For example, let's say I have a simple toggle component that shows a different background color when clicked:
import React, { useState } from "react";
import "./Toggle.css";
const Toggle = () => {
const [ selected, setSelected ] = useState(false);
return (
<div className={selected ? "on" : "off"} onClick={() => setSelected(!selected)}>
{selected ? "On" : "Off"}
</div>
);
}
export default Toggle;
.on {
background-color: green;
}
.off {
background-color: red;
}
How should I test this component? I wrote the following test, which works for inline component styles, but fails when using css classes as shown above.
import React from "react";
import { render, screen, fireEvent } from "#testing-library/react";
import Toggle from "./Toggle";
const backgroundColor = (element) => window.getComputedStyle(element).backgroundColor;
describe("Toggle", () => {
it("updates the background color when clicked", () => {
render(<Toggle />);
fireEvent.click(screen.getByText("Off"));
expect(backgroundColor(screen.getByText("On"))).toBe("green");
});
});
So that's not what unit or integration test frameworks do. They only test logic.
If you want to test styling then you need an end-to-end/snapshot testing framework like Selenium.
For making sure the styling is okay, I prefer snapshot testing. How about firing the event and taking snapshots for both states/cases. Here is what it would look like:
import React from 'react'
import {render} from '#testing-library/react'
it('should take a snapshot when button is toggled', () => {
const { asFragment } = render(<App />)
// Fire the event
expect(asFragment(<App />)).toMatchSnapshot()
})
});

How to override classes using makeStyles and useStyles in material-ui?

Consider a component that renders a button and says this button should have a red background and a yellow text color. Also there exists a Parent component that uses this child but says, the yellow color is fine, but I want the background color to be green.
withStyles
No problem using the old withStyles.
import React from "react";
import { withStyles } from "#material-ui/core/styles";
import { Button } from "#material-ui/core";
const parentStyles = {
root: {
background: "green"
}
};
const childStyles = {
root: {
background: "red"
},
label: {
color: "yellow"
}
};
const ChildWithStyles = withStyles(childStyles)(({ classes }) => {
return <Button classes={classes}>Button in Child withStyles</Button>;
});
const ParentWithStyles = withStyles(parentStyles)(({ classes }) => {
return <ChildWithStyles classes={classes} />;
});
export default ParentWithStyles;
https://codesandbox.io/s/passing-classes-using-withstyles-w17xs?file=/demo.tsx
makeStyles/useStyles
Let's try the makeStyles/useStyles instead and follow the guide Overriding styles - classes prop on material-ui.com.
import React from "react";
import { makeStyles } from "#material-ui/styles";
import { Button } from "#material-ui/core";
const parentStyles = {
root: {
background: "green"
}
};
const childStyles = {
root: {
background: "red"
},
label: {
color: "yellow"
}
};
// useStyles variant does NOT let me override classes
const useParentStyles = makeStyles(parentStyles);
const useChildStyles = makeStyles(childStyles);
const ChildUseStyles = ({ classes: classesOverride }) => {
const classes = useChildStyles({ classes: classesOverride });
return (
<>
<Button classes={classes}>Button1 in Child useStyles</Button>
<Button classes={classesOverride}>Button2 in Child useStyles</Button>
</>
);
};
const AnotherChildUseStyles = props => {
const classes = useChildStyles(props);
return (
<>
<Button classes={classes}>Button3 in Child useStyles</Button>
</>
);
};
const ParentUseStyles = () => {
const classes = useParentStyles();
return <>
<ChildUseStyles classes={classes} />
<AnotherChildUseStyles classes={classes} />
</>
};
export default ParentUseStyles;
https://codesandbox.io/s/passing-classes-using-usestyles-6x5hf?file=/demo.tsx
There seems no way to get the desired effect that I got using withStyles. A few questions, considering I still want the same effect (green button yellow text) using some method of classes overriding (which seemed to make sense to me before).
How is my understanding wrong about how to pass classes as means to override parts of them using useStyles?
How should I approach it alternatively?
And if I'm using the wrong approach, why is material-ui still giving me a warning when the parent has something in the styles that the child doesn't have?
the key something provided to the classes prop is not implemented in [Child]
Is the migration from the old approach (withStyles) vs the new approach documented somewhere?
Btw, I'm aware of this solution but that seems cumbersome when you have too much you want to override.
const useStyles = makeStyles({
root: {
backgroundColor: 'red',
color: props => props.color, // <-- this
},
});
function MyComponent(props) {
const classes = useStyles(props);
return <div className={classes.root} />;
}
withStyles has very little functionality in it. It is almost solely a wrapper to provide an HOC interface to makeStyles / useStyles. So all of the functionality from withStyles is still available with makeStyles.
The reason you aren't getting the desired effect is simply because of order of execution.
Instead of:
const useParentStyles = makeStyles(parentStyles);
const useChildStyles = makeStyles(childStyles);
you should have:
const useChildStyles = makeStyles(childStyles);
const useParentStyles = makeStyles(parentStyles);
The order in which makeStyles is called determines the order of the corresponding style sheets in the <head> and when specificity is otherwise the same, that order determines which styles win (later styles win over earlier styles). It is harder to get that order wrong using withStyles since the wrapper that you are using to override something else will generally be defined after the thing it wraps. With multiple calls to makeStyles it is easier to do an arbitrary order that doesn't necessarily put the overrides after the base styles they should impact.
The key to understanding this is to recognize that you aren't really passing in overrides, but rather a set of classes to be merged with the new classes. If childClasses.root === 'child_root_1' and parentClasses.root === 'parent_root_1', then the merged result is mergedClasses.root === 'child_root_1 parent_root_1' meaning any elements that have their className set to mergedClasses.root are receiving both CSS classes. The end result (as far as what overrides what) is fully determined by CSS specificity of the styles in the two classes.
Related answers:
Material UI v4 makeStyles exported from a single file doesn't retain the styles on refresh
Internal implementation of "makeStyles" in React Material-UI?
In Material-ui 4.11.x while creating styles using makeStyles wrap the enclosing styles with createStyles, and this style will have highest priority than the default one.
const useStyles = makeStyles((theme: Theme) =>
createStyles({
backdrop: {
zIndex: theme.zIndex.drawer + 1,
color: '#fff',
},
}),
);
You could try removing the createStyles and see the difference.
code source from https://material-ui.com/components/backdrop/
One way to achieve this using withStyles is the following and can be helpful to override css classes.
Supposing that you want to override a class called ".myclass" which contains "position: absolute;":
import { withStyles } from '#material-ui/styles';
const styles = {
"#global": {
".myClass": {
position: "relative",
}
}
};
const TestComponent = (props) => (
<>
<SomeComponent {...props}>
</>
);
export default withStyles(styles)(TestComponent);
After doing this, you override the definition of .myClass defined on <SomeComponent/> to be "position: relative;".

Generate RTL CSS file in create-react-app and switch between them based on change in state

I'm using create-react-app for a multi-language project.
I want to use some library like "cssJanus" or "rtlcss" to convert the Sass generated CSS file into a separate file and then use that newly generated file when I switch to another language.
Here's how my index.js looks like ...
import React from "react";
import ReactDOM from "react-dom";
import * as serviceWorker from "./serviceWorker";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider } from "react-redux";
import App from "./App";
import { configureStore } from "./store/configureStore";
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>,
document.getElementById("root")
);
serviceWorker.unregister();
And here's how my "App.js" looks like ...
import React, { Component } from "react";
import "./App.scss";
import { Route, Switch } from "react-router-dom";
import SignIn from "./features/signin/SignIn";
class App extends Component {
render() {
return (
<>
<Switch>
<Route path="/" exact component={SignIn} />
</Switch>
</>
);
}
}
export default App;
As you can see I'm using "./App.scss" file that simply have a bunch of #import statements to another ".scss" files in the "./src/css/" directory ...
/* autoprefixer grid: on */
#import "css/reset";
#import "css/variables";
#import "css/global";
I need your advice on how to do that. How to convert the generated CSS from App.scss to RTL into their own .css file and switch between them and the original generated CSS based on a change in the global state.
I searched a lot for something like this but with no luck.
Or if you have a better approach I'm all ears.
Here is a simple solution that requires ejecting and adding a lightweight webpack-rtl-plugin.
After running
npx create-react-app react-rtl
cd react-rtl
yarn eject
yarn add -D webpack-rtl-plugin #babel/plugin-transform-react-jsx-source
Go to config/webpack.config.js and make some tweaks:
// import the plugin
const WebpackRTLPlugin = require('webpack-rtl-plugin')
// ...
module: { ... }
plugins: [
// ...,
// use the plugin
new WebpackRTLPlugin({ diffOnly: true })
].filter(Boolean),
// ...
On this stage, if you run yarn build and look up build/static/css folder, you should hopefully see additional .rtl.css file that contains your rtl styles.
Then we need to tell webpack to use MiniCssExtractPlugin.loader for development as well so it will serve styles through link tags instead of inline styles:
// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && { loader: MiniCssExtractPlugin.loader }, // <-- use this
// isEnvDevelopment && require.resolve('style-loader'), <-- instead of this
and don't forget the plugin, lol:
module: { ... }
plugins: [
// ...,
// isEnvProduction && <-- comment this out
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// ...
].filter(Boolean),
And from here you can finally grab your default stylesheet href and use to insert rtl styles. Here's how you could implement it:
class RtlCssBundleService {
constructor() {
this.rtlApplied = false
this.rtlStyles = [];
this.ltrStyles = Array.from(
document.querySelectorAll('link[rel="stylesheet"]')
)
}
insert = () => {
if (this.rtlApplied) { return }
this.rtlApplied = true
if (this.rtlStyles.length) {
return this.rtlStyles.forEach(style => {
document.body.appendChild(style)
})
}
this.rtlStyles = this.ltrStyles.map(styleSheet => {
const link = document.createElement("link")
link.href = styleSheet.href.replace(/\.css$/, '.rtl.css')
link.rel = "stylesheet"
document.body.appendChild(link)
return link
})
}
detach = () => {
this.rtlApplied = false
this.rtlStyles.forEach(style => {
document.body.removeChild(style)
})
}
toggle = () => {
return this.rtlApplied
? this.detach()
: this.insert()
}
}
const rtlStyles = new RtlCssBundleService()
export default rtlStyles
Then use this from any of your components.
So anyway, I'm sure I've missed something and maybe that is a terrible approach, but it seems to work and here is the demo
If you use flexbox and css grid they have RTL support built in. Then use CSS Logical Properties for margin, padding, border, etc. If that is not enough, then you can use [dir="rtl"] .your-class as a fallback.
Now you don't have two separate css files to maintain.
Here is a cross browser margin-right example.
-webkit-margin-end: 25px;
margin-inline-end: 25px;
#supports (not (-webkit-margin-end: 0)) and (not (margin-inline-end: 0)) {
margin-right: 25px;
}
You could wrap that up into a mixin for easier use across your app.
Looking around there is a library called react-with-direction from airbnb that provides a DirectionProvider - component you could wrap your components in based on the language. Hope that helps.

passing mixins to styled-component

I am trying to pass a mixin from Material UI to a styled-component. The issue is that I can't figure out a way to pass the mixin value to the styled-component without assigning it to to a css property. For example, this is not possible:
const Example = styled.div`
${p => p.theme.mixins.toolbar};
`;
Edit: The issue ended up being the semi-colon next to the closing '}'. I believe adding a semi colon makes the styled-component think that you are adding a normal property, not a mixin.
You need to spread the mixins not calling it, like so:
const Example = styled.div`
${props => ({ ...props.theme.mixins.toolbar })}
`;
Yet that will return style object, you might wanna convert the resulting object into css-compatible syntax as follows:
const Example = styled.div`
${props => (Object.entries({ ...props.theme.mixins.toolbar }).reduce((styleString, [propName, propValue]) => {
if (propName.indexOf('#') !== -1) {
// iterate over media queries
return `${styleString}${propName} { ${Object.entries(propValue).reduce((ss, [pn, pv]) => {
pn = pn.replace(/([A-Z])/g, m => `-${m[0].toLowerCase()}`);
return `${ss}${pn}:${pv+(Number.isInteger(pv) ? 'px' : '')};`;
}, '')}; }`;
}
propName = propName.replace(/([A-Z])/g, matches => `-${matches[0].toLowerCase()}`); // convert camel-case properties into dash-splitted attributes
return `${styleString}${propName}:${propValue+(Number.isInteger(propValue) ? 'px' : '')};`; // append css pixel unit to integer values
}, ''))}
`;
I read your edit but deleting the semicolon didn't work for me. This worked:
import React from "react";
import { createMuiTheme } from "#material-ui/core/styles";
import styled, { ThemeProvider } from "styled-components";
const Content = styled.div`
toolbar: ${props => props.theme.mixins.toolbar};
`;
const theme = createMuiTheme();
const Wrapper = props => {
return (
<ThemeProvider theme={theme}>
<Content children={props.children} />
</ThemeProvider>
);
};
export default Wrapper;
I read this documentation, and very simple code:
const Offset = styled('div')(({ theme }) => theme.mixins.toolbar);
function App() {
return (
<React.Fragment>
<AppBar position="fixed">
<Toolbar>{/* content */}</Toolbar>
</AppBar>
<Offset />
</React.Fragment>
);
}

css-in-javascript use in a React component

According to this reference https://github.com/airbnb/javascript/tree/master/css-in-javascript, a styled JS component should be written like this:
function MyComponent({ styles }) {
return (
<div {...css(styles.container)}>
Never doubt that a small group of thoughtful, committed citizens can
change the world. Indeed, it’s the only thing that ever has.
</div>
);
}
export default withStyles(() => ({
container: {
display: 'inline-block',
},
}))(MyComponent);
I'm trying to write a simple React component like following, without success (I'm receiving a withStyles is not defined error):
import React from 'react';
const MyComponent = ({styles}) => {
return (
<div {...css(styles.container)}>Hello World</div>
)
}
export default withStyles(() => ({
container: {
color: 'red'
},
}))(MyComponent);
What am I doing wrong? Is it possible to use this convention for a React component?
You can do it like this
const divStyle = {
color: 'blue',
fontSize:10px
};
function HelloWorldComponent() {
return <div style={divStyle}>Hello World!</div>;
}
However you can try other better approaches.Please refer to the link below:
https://medium.com/#aghh1504/4-four-ways-to-style-react-components-ac6f323da822

Resources