Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add RTL support for Material UI React

I'm building an LTR application and I want to add RTL support. The application is based on top of Material UI React. I'm able to rotate the application to RTL since I'm using CSS Flex Box, just by adding dir="rtl" to the body. I also added direction="rtl" to the theme as mentioned here.

However not everything was changed.

Let's take this as an example : Select Country LTR As you can see here I have padding left to the text element. In the RTL version, since everything was reversed the padding left has no effect in the UI, I mean it must be padding right to show the small space between the two element : Select Country RTL

It seems that I'm doing something wrong because in Material UI documentation here this feature must be out of the box after adding this snippet and wrap the component around it.

This is my Parent component App :

import React, { PureComponent } from "react";
import { theme } from "./styling/theme";
import Routes from "./Routes";
// Redux
import { Provider } from "react-redux";
import store from "./app/store";

import LoadingBar from "react-redux-loading-bar";
// CSS
import { MuiThemeProvider } from "@material-ui/core/styles";
// import { ThemeProvider } from "@material-ui/styles";
import { create } from "jss";
import rtl from "jss-rtl";
import JssProvider from "react-jss/lib/JssProvider";
// import { StylesProvider, jssPreset } from "@material-ui/styles";
import { createGenerateClassName, jssPreset } from "@material-ui/core/styles";
import { themeObject, colors } from "./styling/theme";
// Helpers
import get from "lodash/get";

// Configure JSS
const jss = create({ plugins: [...jssPreset().plugins, rtl()] });
const generateClassName = createGenerateClassName();

function RTL(props) {
  return (
    <JssProvider jss={jss} generateClassName={generateClassName}>
      {
        props.children
      }
    </JssProvider>
  );
}

class App extends PureComponent {
  render() {
    const isRtl = get(store, "classified.language.rtl", false);
    return (
      <Provider store={store}>
        <RTL>
          <MuiThemeProvider
            theme={
              isRtl
                ? { ...theme, direction: "rtl" }
                : { ...theme, direction: "ltr" }
            }
          >
            <LoadingBar
              style={{
                backgroundColor: colors.primary[500],
                height: themeObject.spacing.unit,
                zIndex: 9999
              }}
            />
            <Routes />
          </MuiThemeProvider>
        </RTL>
      </Provider>
    );
  }
}

export default App;

And this is an example of my components (The one in the pictures above : CLList) :

import React, { Component } from "react";
import PropTypes from "prop-types";
import { withStyles } from "@material-ui/core/styles";
// Helpers
import isFunction from "lodash/isFunction";
import cloneDeep from "lodash/cloneDeep";

import styles from "./CLList.styles";

const defaultImg = "IMAGE_URL_HERE";
class CLList extends Component {
  static propTypes = {
    classes: PropTypes.object.isRequired,
    items: PropTypes.arrayOf(
      PropTypes.shape({
        img: PropTypes.string,
        name: PropTypes.string
      })
    ).isRequired,
    onClick: PropTypes.func
  };
  render() {
    const { classes, items, onClick } = this.props;
    return (
      <ul className={classes.list}>
        {items.map((item, key) => (
          <li
            className={classes.item}
            onClick={() => isFunction(onClick) && onClick(cloneDeep(item))}
            key={key}
          >
            <img
              className={classes.image}
              src={item.img || defaultImg}
              alt={item.name}
              title={item.name}
            />
            <span className={classes.label}>{item.name}</span>
          </li>
        ))}
      </ul>
    );
  }
}

export default withStyles(styles)(CLList);

And the last file is the CSS for CLList :

import { colors } from "../..";
const styles = theme => ({
  list: {
    display: "flex",
    flexDirection: "column",
    listStyle: "none",
    padding: 5,
    margin: 0,
    "& > li:not(:last-child)": {
      marginBottom: 10
    }
  },
  item: {
    flex: 1,
    display: "flex",
    cursor: "pointer",
    "&:hover": {
      backgroundColor: colors.primary[50]
    }
  },
  image: {
    flex: "0 0 15%",
    maxWidth: "40px",
    maxHeight: "40px"
  },
  label: {
    flex: "1",
    alignSelf: "center",
    paddingLeft: 20
  }
});

export default styles;

I'm expecting that paddingLeft of label to be => paddingRight. Is this possible ? Is it a feature out of the box ? Or should I just use RTL-CSS-JS and wrap all my styles object when the body contains dir="RTL" to change automatically the style ?

I'm also so confused between these two libraries :

  • @material-ui/core/styles
  • @material-ui/styles

Should I use the first or the second one ? What is the difference ?

Thanks for you time.

EDIT 1 :

I used rtlCSSJS on my CSS Object and I get the expected result. But I'm not sure if this is the best way to do it. The CSS of CLList now look like this :

import rtlCSSJS from "rtl-css-js";
import { colors } from "../..";
const defaultDir = document.body.getAttribute("dir");
const styles = theme =>
  defaultDir === 'rtl' ? rtlCSSJS({...CSS_HERE....}) : {...CSS_HERE....};
export default styles;
like image 408
Zied Hf Avatar asked May 27 '19 21:05

Zied Hf


People also ask

How can you enable Right to Left support in angular application using material UI for styling?

Right To Left (RTL) can be enabled for Syncfusion Angular UI components by calling enableRtl with true . This will render all the Syncfusion Angular UI components in right to left direction. We can enable the feature by setting the property enableRtl value as true.

What is RTL in React?

In order to support the habits of languages ​​such as Arabic and Hebrew, the UI of the language read from right to left (RTL).


2 Answers

I think I found a solution to my own question, however feel free to add any enhancement or better solution.

Material UI is using jss-rtl by default and this last one is a wrapper for rtl-css-js. So there is no need to use rtl-css-js directly in since Material UI will do the job.

I changed my Parent App component to :

import React, { PureComponent } from "react";
import Routes from "./Routes";
import RTL from "./RTL";
// Redux
import { Provider } from "react-redux";
import store from "./app/store";

import LoadingBar from "react-redux-loading-bar";

import { themeObject, colors } from "./styling/theme";

class App extends PureComponent {
  render() {
    return (
      <Provider store={store}>
        <RTL>
          <>
            <LoadingBar
              // className="loading"
              style={{
                backgroundColor: colors.primary[500],
                height: themeObject.spacing.unit,
                zIndex: 9999
              }}
            />
            <Routes />
          </>
        </RTL>
      </Provider>
    );
  }
}

export default App;

And I added RTL component which will connect to Redux to define the correct theme with the right direction. I'm saving the language data in Redux, and according the this data I will define the theme provided for my application.

This is the RTL component :

import React, { PureComponent } from "react";
import PropTypes from "prop-types";
// Redux
import { connect } from "react-redux";
// CSS
import { MuiThemeProvider, createMuiTheme } from "@material-ui/core/styles";
import { create } from "jss";
import rtl from "jss-rtl";
import JssProvider from "react-jss/lib/JssProvider";
import { createGenerateClassName, jssPreset } from "@material-ui/core/styles";

// Theme
import { themeObject } from "./styling/theme";

// Helpers
import get from "lodash/get";
// Configure JSS
const jss = create({ plugins: [...jssPreset().plugins, rtl()] });
const generateClassName = createGenerateClassName();

const G_isRtl = document.body.getAttribute("dir") === "rtl";

class RTL extends PureComponent {
  static propTypes = {
    children: PropTypes.oneOfType([
      PropTypes.array,
      PropTypes.object,
      PropTypes.node
    ]),
    language: PropTypes.object
  };

  render() {
    const { children, language } = this.props;
    const isRtl = get(language, "rtl", G_isRtl);

    const theme = createMuiTheme({
      ...themeObject,
      direction: isRtl ? "rtl" : "ltr"
    });

    return (
      <JssProvider jss={jss} generateClassName={generateClassName}>
        <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>
      </JssProvider>
    );
  }
}

const mapStateToProps = ({ classified }) => ({
  language: classified.language
});
export default connect(mapStateToProps)(RTL);

All the children components now will switch between RTL and LTR according to the language and we can focus on only one layout, all the reversing job is done thanks to this Plugins.

Also I want to say folowing the instructions in the official documentation does not work for me ! Most of this solution that I found is based on the answer here.

like image 166
Zied Hf Avatar answered Oct 14 '22 17:10

Zied Hf


import React, { useState, createContext, useMemo, useEffect } from 'react';
import PropTypes from 'prop-types';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { SnackbarProvider } from 'notistack';
import { Box } from '@mui/material';
import languageList from 'shared/languageList';
import { useTranslation } from 'react-i18next';
import rtlPlugin from 'stylis-plugin-rtl';
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import { prefixer } from 'stylis';

export const AppThemeContext = createContext({});

const AppTheme = ({ children }) => {
  const { i18n } = useTranslation();
  const [dir, setDir] = useState(i18n.language === 'ar' ? 'rtl' : 'ltr');
  const [language, setLanguage] = useState(i18n.language);      

  const toggleLanguage = async (language) => {
    setLanguage(language.value);
    switch (language.value) {
      case 'ar':
        document.body.setAttribute('dir', 'rtl');
        setDir('rtl');
        await i18n.changeLanguage('ar');
        break;
      case 'en':
        document.body.setAttribute('dir', 'ltr');
        setDir('ltr');
        await i18n.changeLanguage('en');
        break;
    }
  };

  const theme = useMemo(() => {
    const arabicFont = '""serif", "Arial", "sans-serif"';
    const englishFont = '"Roboto","Helvetica","Arial",sans-serif';

    const typography = {
      button: {
        textTransform: 'capitalize',
      },
      fontSize: dir === 'rtl' ? 15 : 14,
      fontFamily: dir === 'rtl' ? arabicFont : englishFont,
    };

    return createTheme({
      direction: dir,
      typography,
    });
  }, [dir, colorMode]);

  const direction = useMemo(() => {
    return dir === 'ltr' ? 'left' : 'right';
  }, [dir]);
  // this is the most important part
  const cacheRtl = useMemo(() => {
    if (dir === 'rtl') {
      return createCache({
        key: 'muirtl',
        stylisPlugins: [prefixer, rtlPlugin],
      });
    } else {
      return createCache({ key: 'css' });
    }
  }, [dir]);

  useEffect(async () => {
    await toggleLanguage({ value: language });
  }, []);

  const toggleColorMode = () =>
    setColorMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'));

  return (
    <AppThemeContext.Provider
      value={{
        language,
        toggleLanguage,
        languageList,
        direction,
        colorMode,
        toggleColorMode,
      }}>
      <Box component="main">
        <CacheProvider value={cacheRtl}>
          <ThemeProvider theme={theme}>
            <SnackbarProvider maxSnack={3}>{children}</SnackbarProvider>
          </ThemeProvider>
        </CacheProvider>
      </Box>
    </AppThemeContext.Provider>
  );
};

AppTheme.propTypes = {
  children: PropTypes.any,
};

export default AppTheme;

Important notes

  • I use MUI version 5
  • MUI version 5 uses emotion css as default styling engine
  • CacheProvider is used to configure the RTL or LTR
  • ThemeProvider must be wrapped inside CacheProvider
  • make sure to use useMemo when passing the value to CacheProvider or if you're going to use other Providers when changing default styling engine like ( StylesProvider for JSS & StyleSheetManager for styled-components )
like image 37
Mohammed Ramadan Avatar answered Oct 14 '22 17:10

Mohammed Ramadan