Как добавить поддержку RTL для Material UI React

Я создаю приложение LTR и хочу добавить поддержку RTL. Приложение основано на материале пользовательского интерфейса React. Я могу повернуть приложение в RTL, так как я использую CSS Flex Box, просто добавив dir="rtl" в тело. Я также добавил direction="rtl" к теме, как упомянуто здесь.

Однако не все было изменено.

Давайте возьмем это в качестве примера: Как вы можете видеть здесь, у меня есть отступ слева от текстового элемента. В версии RTL, поскольку все было перевернуто, отступы слева не влияют на пользовательский интерфейс, я имею в виду, что отступы справа должны отображать небольшое пространство между двумя элементами:

Кажется, что я делаю что-то не так, потому что в документации Material UI здесь эта функция должна быть из коробки после добавления этого фрагмента и обернуть компонент вокруг него.

Это мой родительский компонент 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;

И это пример моих компонентов (тот, что на картинках выше: 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);

И последний файл - это CSS для 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;

Я ожидаю, что paddingLeft метки будет => paddingRight. Это возможно? Это особенность из коробки? Или я должен просто использовать RTL-CSS-JS и обернуть все мои объекты стилей, когда тело содержит dir="RTL" для автоматического изменения стиля?

Я также так запутался между этими двумя библиотеками:

  • @ материал-UI / ядро ​​/ стили
  • @ материал-UI / стили

Должен ли я использовать первый или второй? В чем разница?

Спасибо за ваше время.

РЕДАКТИРОВАТЬ 1:

Я использовал rtlCSSJS на своем объекте CSS, и я получил ожидаемый результат. Но я не уверен, что это лучший способ сделать это. CSS из CLList теперь выглядит следующим образом:

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;

3 ответа

Я думаю, что нашел решение для своего собственного вопроса, однако не стесняйтесь добавлять любые улучшения или более качественное решение.

Пользовательский интерфейс материала по умолчанию использует jss-rtl, и последний является оболочкой для rtl-css-js. Таким образом, нет необходимости использовать rtl-css-js напрямую, так как Material UI сделает эту работу.

Я изменил компонент родительского приложения на:

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;

И я добавил компонент RTL, который подключится к Redux, чтобы определить правильную тему в правильном направлении. Я сохраняю языковые данные в Redux, и в соответствии с этими данными я определю тему, предоставленную для моего приложения.

Это компонент RTL:

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);

Все дочерние компоненты теперь будут переключаться между RTL и LTR в соответствии с языком, и мы можем сосредоточиться только на одной компоновке, вся работа по обращению выполняется благодаря этим плагинам.

Также я хочу сказать, что следующие инструкции в официальной документации не работают для меня! Большая часть этого решения, которое я нашел, основано на ответе здесь.

      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;

Важные заметки

  • Я использую MUI версии 5
  • MUI версии 5 использует эмоции css в качестве движка стилей по умолчанию.
  • CacheProvider используется для настройки RTL или LTR.
  • ThemeProvider должен быть обернут внутри CacheProvider
  • обязательно используйте useMemo при передаче значения в CacheProvider или если вы собираетесь использовать другие провайдеры при изменении механизма стилей по умолчанию, например ( StylesProvider для JSS и StyleSheetManager для стилизованных компонентов )

Пожалуйста, все, забудьте о документации и обо всем, что написано вокруг, потому что ни одна из них не является ни ясной, ни полной, если вы используете v4.mui.com, я покажу вам, как сделать это RTL, и вы собираетесь реализовать переключение между RTL и LTR создайте файл, сохраните его в utils/stylesprovider.js

со следующим содержанием

      import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {Suspense} from 'react';
import {createStore, applyMiddleware, compose} from 'redux'
import {Provider} from 'react-redux'
import thunk from 'redux-thunk'
import reducers from './reducers/'
import './i18n';

import RTL from "./utils/stylesprovider";
import {createTheme} from "@material-ui/core";
const store = createStore(reducers, applyMiddleware(thunk))


ReactDOM.render(
    <RTL>
    <Provider store={store} >
        <Suspense fallback="...is loading">
            <App/>
        </Suspense>
    </Provider>
    </RTL>,
    document.getElementById('root')
);

затем перейдите в свой индекс и сделайте что-то вроде этого

      import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {Suspense} from 'react';
import {createStore, applyMiddleware, compose} from 'redux'
import {Provider} from 'react-redux'
import thunk from 'redux-thunk'
import reducers from './reducers/'
import './i18n';

import RTL from "./utils/stylesprovider";
const store = createStore(reducers, applyMiddleware(thunk))


ReactDOM.render(
    <RTL>
    <Provider store={store} >
        <Suspense fallback="...is loading">
            <App/>
        </Suspense>
    </Provider>
    </RTL>,
    document.getElementById('root')
);

не забудьте установить

npm установить jss-rtl

Другие вопросы по тегам