样式被 Material-UI 样式覆盖

Styles being overwritten by Material-UI style

前言

几天前我问了一个 这个问题,虽然本质上是相关的,但我相信解决方案最终会有所不同,所以我在另一个线程中再次提问。

CodeSanbox Example(已更新以反映已接受的答案)

问题:

我希望通过 className 属性传入的任何外部样式比我的自定义组件内部样式具有更高的特异性。这样使用它的人可以调整边距和填充。但是,我的组件默认内部样式覆盖了我的外部样式,我希望它是相反的方式。

详情:

我正在 material-ui 之上创建自定义组件库 built。我想让自定义组件 api 类似于 @material-ui,这样我们的开发人员会发现它们更易于使用。我正在构建的每个组件uilding 都有自己的内部样式,覆盖了默认的 material-ui 样式,在这种情况下它被定义为 class button。此外,像 @material-ui 我接受颜色道具 <TestButton color={'default'}/>。最后,如果需要的话,我希望我的自定义按钮可以被外部样式覆盖。我正在使用 clsx 库来生成 uild class 名称字符串。

代码:

import React, { useState } from "react";
import { makeStyles } from "@material-ui/styles";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import { Button } from "@material-ui/core";
import clsx from "clsx";

const useAppStyles = makeStyles({
  gButton: { margin: "150px" }
});

export default function App() {
  const classes = useAppStyles();

  return (
    <div className={classes.example}>
      <div className={classes.separator}>
        <div>Buttons:</div>
        <TestButton
          className={classes.gButton}
          color={"default"}
        >
          Default
        </TestButton>
        <TestButton
          className={classes.gButton}
          color={"primary"}
        >
          Primary
        </TestButton>
    </div>
  );
}

function TestButton(props) {

  const classes = GrangeButtonStyles();
  let color = props.color === 'default' ? classes.default : classes.primary 

  const GrangeButtonStyles = makeStyles({
    button: {
     height: "45px",
     padding: "13px 30px 13px 30px",
     borderRadius: "5px",
     border: "none",
     margin: "15px",
    },
    default: {
     backgroundColor: "black",
     border: 'solid #2e7d32 1px',
     color: "white",
    },
    primary: {
     backgroundColor: 'white',
     color: 'black',
     fontFamily: 'Montserrat, sans-serif',
     border: 'solid black 1px',
    }
  });

  return (
    <Button
      className={clsx(classes.button, color, props.className)}
      variant="contained"
      disabled={props.disabled}
      disableElevation
    >
      {props.children}
    </Button>
  );
}

注意:

我在这个问题和代码沙箱示例中大大简化了 space 的代码。请不要评论说您认为我正在做的事情因为这个例子没有意义。

<TestButton className={classes.gButton} color={"default"}>

// should be

<TestButton classes={{button:classes.gButton}} color={"default"}>

// and

<TestSelect className={classes.gSelect}

// should be

<TestSelect className={{dropdown:classes.gSelect}}

^在处理material-ui的样式解决方案时,不要将"className"传递给组件(只把这个支持 DOM 个元素!!!!)

function TestButton(props) {
  const classes = GrangeButtonStyles();
  ...

// should be


function TestButton(props) {
  const classes = GrangeButtonStyles(props);
  ...


^ 这将导致 prop classes.button(看起来像 jss-1233)与出现的按钮 class 合并在 GrangeButtonStyles 中,所以 classes 现在看起来像这样:

{
  button: 'jss-7382 jss-1233' <- jss-1233 is the classname that got generated in the previous component
}

    <Button
      className={clsx(classes.button, color, props.className)}

// should be

    <Button
      classes={{root:classes.button)}

^ See material-ui docs for button

实际上不幸的是 material-ui 将它的引用转发给 DOM 元素 而没有检查 className 因为这允许人们将 class 名称放在 material-ui 组件上并且它 "kind of" 有效。他们真的应该添加警告以改用 classes 感谢评论者纠正我的错误,我仍然喜欢通过 classes 而不是 className 的冗长,因为将两者混合会导致混淆!

编辑:

我还注意到你弄乱了 TestSelect 中的样式 - 永远记住这一点 - 不要将 className 作为属性传递给 material-ui组件,只传classes。如果你想将样式从父组件传递到子组件,那么你必须使用相同的键:

让我们举个例子,我知道,这东西很难理解,但最终它会 "click":

const useParentStyles = makeStyles({
   childStyles: { ... some jss }
   childStyles2: { ... some jss }
});

const Parent = props => {
   const classes = useParentStyles(props);
   return <Child classes={{root:classes.childStyles,someOtherKey:classes.childStyles2}}/> <- object with a keys of "root" and "someOtherKey"
}

const useChildStyles = makeStyles({
  root: { ... some jss } <- root
  someOtherKey: { ... some jss } <- someOtherKey 
});

const Child = props => {
   const classes = useChildStyles(props); <- classes have been merged together
   return <div className={classes.root}>...</div>
}

来自 https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity:

When multiple declarations have equal specificity, the last declaration found in the CSS is applied to the element.

因此,如果您在自定义组件(例如 TestButton)和使用该组件的代码中定义 CSS classes,则特异性由这些 CSS classes 在 <head> 元素中出现的顺序。此顺序由 index that is set when makeStyles is called 确定,因此稍后调用 makeStyles 定义的 classes 将稍后出现在 <head> 元素中,因此具有更大的特异性。

你的例子中有两个问题:

  1. TestButton 在使用它的代码之后定义,因此在 makeStyles 调用之后定义旨在覆盖 TestButton 中的样式。由于 makeStylesgButton 的调用首先发生,相应的 CSS class 将首先出现在 <head> 元素中。但在实际使用中,TestButton(您的自定义组件)将在单独的文件中定义并导入。由于导入必须在顶部,所以任何 makeStyles 调用 在导入文件的顶层 将在文件中使用的任何 makeStyles 调用之前执行导入组件。

  2. TestButtonmakeStyles 调用是 而不是 在顶层完成的。相反,它是在 TestButton 函数内部完成的,这意味着它将在呈现 TestButton 时执行,而不是在导入 TestButton 时执行。对 makeStyles 的调用应该始终在顶层而不是嵌套在组件函数中。另一个小问题是从 makeStyles 返回的变量的名称(即您的示例中的 GrangeButtonStyles )。由于 makeStyles returns a custom hook, you should always have a name that starts with "use" (e.g. useGrangeButtonStyles). This will ensure that the eslint rules for hooks 将其识别为钩子并警告您任何钩子误用。

相关答案和参考资料: