Material UI react testing custom styles - reactjs

I'm struggling to come up with a sensible way of asserting that a given style will be added to a Material-UI component when a given prop is specified
import makeStyles from "#material-ui/core/styles/makeStyles";
const useStyles = makeStyles({
myRoot: props => props.inverse && {
border: "2px solid red",
},
})
const MyTextInput = ({ inverse, ...otherProps }) => {
const classes = useStyles({ inverse });
return (
<TextField
label="Standard secondary"
color="secondary"
classes={{
root: classes.myRoot,
}}
/>
);
}
// with inverse
<MyTextInput
label="Standard secondary"
color="secondary"
inverse={true}
/>
// without inverse prop
<MyTextInput
label="Standard secondary"
color="secondary"
/>
Is there a sensible way to test that the correct class has been applied?
I notice that when I render the component multiple times, the className + a number will be added to all instances e.g. makeStyles-myRoot-*.
Is there way of confirming that the correct style has been applied?
I'm using react-testing-library if that helps.

Related

Dynamically style components passed as props

The goal is to style a prop inside a function (if the prop exists).
More specifically, I basically pass an icon (from styled-icons) as a prop to it and it is supposed to add styling to that icon.
This works with warnings:
const InputRounded = ({ type, placeholder, icon }) => {
const Icon = styled(icon)`
color: #807da0;
width: 1.75rem;
margin: 0 1.25rem 0 0.75rem;
`
return (
<Container>
<Input type={type} placeholder={placeholder} />
<Icon />
</Container>
)
}
Here is how I call it:
import { LockPassword } from '#styled-icons/remix-line'
<Input type="password" placeholder="Password" icon={LockPassword} />
I am aware that one shouldn't create a component inside a function, but I have tried numerous ways otherwise and haven't reached anywhere. Any help for a better method would be greatly appreciated.
You can, very simply, pass a style prop to icon.
const InputRounded = ({ type, placeholder, icon: Icon }) => {
return (
<Container>
<Input type={type} placeholder={placeholder} />
<Icon style={{ color: '#807da0', ... }} />
</Container>
)
}
You could probably use the cloneElement method in React
https://reactjs.org/docs/react-api.html#cloneelement
return (
<>
{ React.cloneElement( icon, [props] }
</>
)
It is similar to
<element.type {...element.props} {...props}>{children}</element.type>
To override some values you can do inline styling.
or otherwise you could use some css classes to override with selectors as well.
if you are going to use styled-icons
you can just simply provide it the props directly.
Styled Icons accept all the valid props of an <svg /> element
https://github.com/styled-icons/styled-icons#props

Incorrect classnames in Material UI with styled components inside react frame

I am using react-frame-component to load a custom component I created which is using material ui and styled-components.
The custom component I created is irrelevant but contains the following relevant code:
const StyledCardHeader = styled(({ isRTL, ...rest }) => (
<CardHeader {...rest} />
))`
${({ theme, isRTL }) => `
& .MuiCardHeader-title {
margin-right:${isRTL ? 0 : theme.spacing(2)}px;
margin-left: ${isRTL ? theme.spacing(2) : 0}px;
}
`};
`;
When it renders, the actual classname becomes something else than I expect: MuiCardHeader-title-34 (it adds the 34 as suffix - random number).
Therefore, my custom styled is not being applied.
Here's a sandbox of the above:
https://codesandbox.io/s/sparkling-field-ui82v
You can look into overriding the MUI CardHeader CSS based off of the docs.
<CardHeader
{...rest}
classes={{
title: makeStyles({
customTitleMargin: {
marginRight: `${isRTL ? 0 : theme.spacing(2)}px`,
marginLeft: `${isRTL ? theme.spacing(2) : 0}px`,
}
})().customTitleMargin
}}
/>
I did not want to mess with your code too much so in my example, I just plugged in the makeStyles export from #material-ui/styles, but the logic of future implementations is similar in that you should just override the MUI component itself.
CodeSandBox: https://codesandbox.io/s/lucid-joliot-5mf8n?file=/src/Card.js

Altering multiple component roots for MUI Textfield

According to the MUI Texfield API here, Textfield is a simple abstraction on top of the following components
FormControl
Input
InputLabel
FilledInput
OutlinedInput
Input
FormHelperText
And therefore, to change the styling of the Textfield for any of the components above, like for example notchedOutline class, which is a class for OutlinedInput, i can just do the following
import { TextField } from '#material-ui/core';
const style = theme => ({
notchOutline: { /*style in here*/ }
});
<TextField
inputProps={{ notchedOutline : classes.notchedOutline }}
>
</TextField>
All of this can be achieved if that subcomponent classes are unique for that component only.
My question is, how can i style for the more common naming class, like if say i wanna modify the root classes of OutlinedInput, InputLabel, FormHelperText or more subcomponents inside the TextField all at once? I dont think this will work right?
<TextField
FormControlProps={{ root: classes.root }}
OutlinedInputProps={{ root: classes.root, notchedOutline : classes.notchedOutline }}
>
</TextField>
or
<TextField
inputProps={{
root: classes.OutlinedInputRoot,
root : classes.FormHelperTextRoot
}}
>
</TextField>
Need help on how to aim the specific root of a subcomponent of a TextField, without needing to touch on the global MUI theming, or not using the provided TextField at all, instead building the textfield component using those subcomponents on it.
Below is an example showing how to target each of these.
Targeting TextField root is equivalent to targeting FormControl, since FormControl is the "root" component rendered by TextField.
There is no difference in how to target Input, FilledInput, or OutlinedInput -- they are all reached via InputProps.
As a side note, using the className prop for a given component is also equivalent to classes.root.
import React from "react";
import ReactDOM from "react-dom";
import TextField from "#material-ui/core/TextField";
import { makeStyles } from "#material-ui/core/styles";
const useStyles = makeStyles({
formControlRoot: {
border: "2px solid lightgreen",
padding: 2,
marginTop: 10
},
inputRoot: {
border: "2px solid blue"
},
inputLabelRoot: {
border: "2px solid pink"
},
formHelperTextRoot: {
border: "2px solid red"
}
});
function App() {
const classes = useStyles();
const [variant, setVariant] = React.useState("standard");
return (
<div>
<TextField
variant={variant}
label={`My Label (${variant})`}
helperText="My Helper Text"
classes={{ root: classes.formControlRoot }}
InputProps={{ classes: { root: classes.inputRoot } }}
InputLabelProps={{ classes: { root: classes.inputLabelRoot } }}
FormHelperTextProps={{ classes: { root: classes.formHelperTextRoot } }}
/>
<br />
<br />
<button onClick={() => setVariant("standard")}>Standard</button>
<button onClick={() => setVariant("outlined")}>Outlined</button>
<button onClick={() => setVariant("filled")}>Filled</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Relevant documentation: https://material-ui.com/api/text-field/#props
answering this will depend specifically on the material ui version you are using, but i'm going to assume you are using version > 3.
All the following should be working in version 3.9 as i have used them myself, but they are also supposed to work fine with version > 4;
input props are used to pass props to the underlying regular html input element directly, you can pass things like style, max, value, onchange. Things that are native to the html input element.
If you want to pass classes to the underlying material ui input you need to pass a classes object to the InputProps.
Here is how
<TextField
variant="outlined"
// this passes props to the html element, root for example here does not mean anything
inputProps={{
style: { textAlign: 'center' },
}
// this passes props to the wrapper material input, can be one of the following: Input, FilledInput, OutlinedInput
// You can pass here anything that the underlying material component uses but we are only interested in two things classes and className, because other props like value and onChange you can pass directly to TextField - thats why they said TextField is an Abstraction over theses Components
InputProps={{
className: styles.slider_filter_input, // usually you dont need className, classes will be sufficient, but wanted to show that you can also use it
classes: {
focused: classes.focused
// the list of keys you pass here depend on your variant, if for example you used variant="outlined" then you need to check the css api of the OutlinedInput
}
}}
/>
Finally here is a working codesandbox showing the ideas above https://codesandbox.io/s/material-ui-drawer-8p6wv

Material-UI : how to overwrite makeStyles classes for custom component with spread operator in props

I'm creating a custom component which extends Material-UI's Typography component.
The component is for a template I'm creating, so I want to include the option to overwrite the default classes. A very basic guide is provided by Material-UI here however it doesn't provide details about how to do this when you can't simply pass all props to useStyles(props).
I can't pass all props to useStyles(props) as:
Some of the props take in default values that need to be defined
Since it's extending the Typography component, some of the props are destructured (for use in the new custom component) while all remaining props are passed to the Typography component using a spread operator.
Some of the props are only used for styling purposes (i.e. only used to be passed in as props to useStyles(props). If I pass in all props to useStyles(props) then this causes an issue with point 2 above, as I'm unable to destructure these styling-only props so they get passed into the Typography component as part of the spread operator.
Here's some code to illustrate the issue:
// useStyles()
const useStyles = makeStyles(theme => ({
/** Styles applied to the root element */
root: (props: StylesProps) => ({
color: theme.palette.primary.main,
fontFamily: props.fontFamily,
fontWeight: props.weight,
letterSpacing: props.letterSpacing,
lineHeight: 1,
textTransform: props.casing
}),
/** Styles applied to the second logo text if `name[1] !== undefined` */
nameEnd: {
color: theme.palette.secondary.main
},
/** Styles applied to the root element if `textShadow=true` */
textShadow: {
textShadow: theme.textShadow
}
}));
// Component
const Logo = React.forwardRef(function Logo(props: LogoProps, ref) {
const {
casing = "uppercase", // HAS ISSUES 1, 2 and 3
className,
fontFamily, // HAS ISSUES 2 and 3
letterSpacing, // HAS ISSUES 2 and 3
name,
nameSpacing = false,
textShadow = true, // HAS ISSUES 1, 2 an 3
weight, // HAS ISSUES 2 and 3
...other
} = props;
const stylesProps: StylesProps = {
casing,
fontFamily,
letterSpacing,
weight
};
const tiqClasses = useStyles(stylesProps);
const [nameStart, nameEnd] = name;
function LogoText(): JSX.Element {
return (
<React.Fragment>
{nameStart}
{nameSpacing && " "}
{nameEnd && <span className={tiqClasses.nameEnd}>{nameEnd}</span>}
</React.Fragment>
);
}
return (
<Typography
className={clsx(className, tiqClasses.root, {
[tiqClasses.nameEnd]: nameEnd,
[tiqClasses.textShadow]: textShadow
})}
component="p"
ref={ref}
{...other}
>
<LogoText />
</Typography>
);
});
If I change the code to:
// Component
const Logo = React.forwardRef(function Logo(props: LogoProps, ref) {
const {
className,
name,
nameSpacing = false,
...other
} = props;
const tiqClasses = useStyles(props);
const [nameStart, nameEnd] = name;
function LogoText(): JSX.Element {
return (
<React.Fragment>
{nameStart}
{nameSpacing && " "}
{nameEnd && <span className={tiqClasses.nameEnd}>{nameEnd}</span>}
</React.Fragment>
);
}
return (
<Typography
className={clsx(className, tiqClasses.root, {
[tiqClasses.nameEnd]: nameEnd,
[tiqClasses.textShadow]: textShadow
})}
component="p"
ref={ref}
{...other}
>
<LogoText />
</Typography>
);
});
Then it means that:
casing and textShadow props no longer have default values
casing, fontFamily, letterSpacing and weight are all passed to the Typography component under the ...other spread operator since they're no longer being destructured. These are not valid props for the Typography component so they get passed down as a DOM attribute, which then causes errors as they are not valid DOM attributes either.
What I want to achieve
I want to be able to overwrite the root, nameEnd and textShadow classes by using the component like so:
<Logo tiqClasses={{ root: "rootOverwriteClass", nameEnd: "nameEndOverwriteClass" }} classes={{ body1: "body1OverwriteClass" }} {...otherProps} />
I would much prefer to do this with makeStyles than withStyles.
Note: Just to clarify, I'm using tiqClasses instead of classes as classes is already being used by Material-UI. You can see this in the example above where I use different names for each.
How do I achieve this?

Material UI - Change Font Size of TextField from Functional Component

I have the following functional component and I would like to change the font size of the textfield, but for some reason I can't figure it out. I know if I have a traditional component I can export it withStyles and set the className or InputProps, but I'm not sure how to do that with my current setup:
Class Definition:
const FormObjectText = ({id, multiline, onBlur, onChange, onFocus, placeholder, value, style, ...additionalProps}) => (
<TextField
{...additionalProps}
fullWidth
id={id}
inputProps={additionalProps.maxLength != null ? {maxLength: additionalProps.maxLength} : {}}
margin="normal"
multiline={multiline}
onBlur={onBlur}
onChange={e => onChange({ value: e.target.value })}
onFocus={onFocus}
placeholder={placeholder}
style={{
...style
}}
value={value !== null ? value : ""}
/>
);
Export from that file:
export const FORM_OBJECT_DICT = {
text: FormObjectTextStyled,
date: FormObjectDate,
// Others
};
Where it is called in another file:
{FORM_OBJECT_DICT["text"]({
value: editing ? value : getFormObjectDisplayValue(configuration, value),
onChange: this.onChange
})}
Firstly, is there any way to change the font size of the TextField using inline styles (not withStyles()), or if not, where/how would i apply withStyles() in this instance?
You can change the font size with inline styles this way:
<TextField inputProps={{ style: { fontSize: "5rem" } }} />
There is nothing about withStyles that cares whether your component is a function component or a class, so if you want to use classes, you could do something like:
const FormObjectTextStyled = withStyles(styles)(FormObjectText);
and then access the classes prop inside FormObjectText.
Here's a sandbox showing both approaches:

Resources