JSS keyframes not working when passing props - reactjs

I have a Spinner component that's basically a loading icon. I'm trying to pass props to the JSS styles so that it can be customized. But the animations don't seem to work if I pass props to the keyframes.
Below is the component. When I use the animation $spinnertest it works fine. If I use $spinners, it doesn't load the animation (when inspecting the elements, animation-name doesn't even show up in the class, leading me to believe it doesn't get generated. ).
**Example CodeSandBox of issue (just change animation to spinners): https://codesandbox.io/s/exciting-shirley-pqt1o?fontsize=14&hidenavigation=1&theme=dark
const useStyles = makeStyles(theme => ({
root: props => ({
width: props.size,
height: props.size,
position: 'relative',
contain: 'paint',
display: 'inline-block',
}),
spinner: props => ({
width: props.size*0.3125,
height: props.size*0.3125,
background: props.color,
position: 'absolute',
animationDuration: props.duration,
animationIterationCount: 'infinite',
animationTimingFunction: 'ease-in-out',
}),
spinnerAnimation: {
animationName: '$spinners',
},
square2: props => ({
animationDelay: -props.duration/2,
}),
'#keyframes spinnertest': {
'25%': {
transform: 'translateX(22px) rotate(-90deg) scale(.5)',
},
'50%': {
transform: 'translateX(22px) translateY(22px) rotate(-180deg)',
},
'75%': {
transform: 'translateX(0) translateY(22px) rotate(-270deg) scale(.5)',
},
'to': {
transform: 'rotate(-1turn)',
},
},
'#keyframes spinners': props => ({
'25%': {
transform: `translateX(${props.translate}px) rotate(-90deg) scale(.5)`,
},
'50%': {
transform: `translateX(${props.translate}px) translateY(${props.translate}px) rotate(-180deg)`,
},
'75%': {
transform: `translateX(0) translateY(${props.translate}px) rotate(-270deg) scale(.5)`,
},
'to': {
transform: `rotate(-1turn)`,
},
}),
}));
export default function Spinner(props) {
const {duration, size, color} = props;
const classes = useStyles({
duration: duration,
size: size,
color: color,
translate: size*(1-0.3125),
});
return (
<Box className={classes.root}>
<Box className={clsx(classes.spinner, classes.spinnerAnimation)} />
<Box className={clsx(classes.spinner, classes.square2, classes.spinnerAnimation)} />
</Box>
)
}
Spinner.defaultProps = {
duration: 1800,
size: 32,
color: #fff,
}

I have a turnaround solution, which works (not that pretty). You would turn your withStyles into a currying function, that takes keyframesProps, and at your key frame definition you would use an IIFE that returns the object with its properties:
const useStyles = keyframesProps => makeStyles((theme) => ({
... all other styles,
// you need to call an IIFE because keyframes doesn't receive a function
"#keyframes spinners": ((props) => ({
"25%": {
transform: `translateX(${props.translate}px) rotate(-90deg) scale(.5)`
},
"50%": {
transform: `translateX(${props.translate}px) translateY(${props.translate}px) rotate(-180deg)`
},
"75%": {
transform: `translateX(0) translateY(${props.translate}px) rotate(-270deg) scale(.5)`
},
to: {
transform: `rotate(-1turn)`
}
}))(keyframesProps)
}));
at your component you would define your classes like:
const styleProps = {
duration: duration,
size: size,
color: color
}
const framesProps = {
translate: size * (1 - 0.3125)
}
const classes = useStyles(framesProps)(styleProps);

It sounds that MUI has a bug around props in makeStyles #keyframes
#16673
as Olivier Tassinari stated, this bug will be fixed in v5 where MUI gonna use a new styling solution styled-components RCF #22342.
The problem is even more general:
The arrow functions (with or without props) do not work within makeStyles
#21011
Passing the props to rules in your defined keyframes will fix it (after v5 has been available, hopefully)
"#keyframes spinners": {
"25%": {
transform: (props) =>
// console.log(props) and template generation will be created correctly.
`translateX(${props.translate}px) rotate(-90deg) scale(.5)`
},
// ...
}
Until then you can use higher-order useStyle creator for embedding your keyframes, as #buzatto suggested.
Or define your animation presets in your theme object and uses them globally around your project.
const theme = createMuiTheme({
animation: {
presets: {
duration: 180,
// or even function
rotateDeg: (angle) => `{angle}deg`
//...
}
}
});
// usage
const useStyles = makeStyles(theme => ({
"#keyframes spinners": {
"25%": {
transform: `translateX(${
theme.animation.presets.duration * 10
}px) rotate(${theme.animation.presets.rotateDeg(-90)}) scale(.5)`,
},
},
}

Related

React material-ui (MUI) 5 conditional CSS classes

I'm moving from React material-ui 4 to MUI 5.
How do I achieve this type of pattern using the new styled API (or whatever makes sense)?
I'm using Typescript.
const useStyles = makeStyles(theme => ({
topBar: {
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp,
duration: TRANSITION_DURATION,
}),
marginLeft: DRAWER_WIDTH,
},
topBarShift: {
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut,
duration: TRANSITION_DURATION,
}),
marginLeft: 0,
},
}));
function Header({ drawer }: IHeader) {
const classes = useStyles();
...
return (
<div className={`${classes.topBar} ${!drawer && classes.topBarShift}`}>
...
</div>
);
}
If I understand your question clearly, you just want a conditional string.
This can be done by creating a util function for reusability:
type ConditionalClasses = {
[className: string]: bool
}
const conditionalClass = (classes: ConditionalClass) =>
Object.keys(classes).reduce((combinedClassName, className) => classes[className] ? `${combinedClassName} ${className}` : combinedClassName, "")
Usage goes as follows
// Output: "a"
const newClassName = conditionalClasses({a: true, b: false})
Alternatively, you could use clsx
-- Edit
Looks like I misread and hence misunderstood the question.
If you want to use the styled API styles conditionally, you can use the overridesResolver option provided by the styled API.
const CustomDivComponent = styled("div", {
overridesResolver: (props, styles) => {
// Do your conditional logic here.
// Return the new style.
return {};
}
})(({ theme }) => ({
// Your original styles here...
}));
More documentation can be found here

Sibling selector in styled mui#v5

Todo: Achieve a sibling selector in material-ui#v5 styled function.
.root + .root {
margin-top: 8px;
}
To achieve the same with material-ui#v4 makestyles is simple. Have a look at below code:
root: {
width: '100%',
'& + $root': {
marginTop: spacing(1),
},
},
but I have no success with mui new api's for styled(). I have tried a few alternatives, something like this will generate such code.
const Root = styled(Box)(({ theme: { spacing } }) => ({
[`& + .${Root}`]: {
marginTop: spacing(1),
},
}));
<style data-emotion="css" data-s="">.css-43e1lt+.NO_COMPONENT_SELECTOR{margin-top:8px;}</style>
Combining emotion's sibling selector with mui styled function, this is how we can make it work.
const Root = styled(Box)(({ theme: { spacing } }) => ({
'& + &': {
marginTop: spacing(1),
},
}));

Material UI doesn't use overridden styles when using useTheme hook

I'm trying to use custom theming in Material UI like so:
const theme = createMuiTheme({
palette: {
primary: {
main: PRIMARY_COLOR, // "#121212"
},
secondary: {
main: SECONDARY_COLOR, // "#F7D600"
},
},
});
const Wrapper = ({ children }) => {
return (
<ThemeProvider theme={theme}>{children}</ThemeProvider>
);
};
This works for things like buttons:
<Button
variant="contained"
color="secondary"
/>
In this case, the hex color #F7D600 gets applied.
But when I try to use the same color on my components using makeStyles, it doesn't seem to recognize it. It just uses the default by Material UI:
const useStyles = makeStyles((theme) => ({
someElement: {
backgroundColor: theme.palette.primary.main // <- not working. it uses the default purple color
}
});
I also tried useTheme but it's the same result:
const SomeComponent = () => {
const theme = useTheme();
return (
<Box style={{ backgroundColor: theme.palette.primary.main }}></Box>
);
}
Any ideas what I could be missing?
I can't see your import statement, but I use MuiThemeProvider. That could be your issue. Everything else looks right to me
import { MuiThemeProvider } from '#material-ui/core';
Use the below style to override any component in material-UI
import { createMuiTheme, colors } from '#material-ui/core'
const theme = createMuiTheme({
palette: {
background: {
dark: '#F4F6F8',
default: colors.common.white,
paper: colors.common.white,
},
primary: {
main: colors.indigo[500],
},
secondary: {
main: colors.indigo[500],
},
text: {
primary: colors.blueGrey[900],
secondary: colors.blueGrey[600],
},
common: {
tableHeader: '#DEF3FA',
},
action: {
oddRowColor: '#def3fa2e',
},
},
zIndex: {
modal: 10010, // override modal zIndex
appBar: 1000, // override Appbar
},
overrides: { 'MTableHeader-header': { root: { width: '143px !important' } } },
})
// use in your component
const StyledTableRow = withStyles((theme) => ({
root: {
'&:nth-of-type(odd)': {
backgroundColor: theme.palette.action.oddRowColor,//defined in theme
},
padding: 'dense',
},
}))(TableRow)
[https://material-ui.com/customization/default-theme/][1]

How do you access the Material UI theme inside event handlers?

I have event handlers for things like onClick or onFocus, and I can't figure out how to use the theme inside of the handler code. I want to change the color of an iconButton and I don't want to hard-code the color because we want components that can be general use, and eventually work with themes using completely different colors.
Tried using withTheme in addition to withStyles, so I can get the theme inside of the render(), but I can't get to it from a handler called from that rendering. Tried passing it, calling as a prop, declaring constants based upon theme values in the class (both inside and outside of render), nothing.
I don't know if this is possible, or not built in, or what. I'm hoping that I'm just missing something.
Environment: CodeSandBox, so CreateReactApp. Material-UI plus React-Select, withStyles and withTheme (useTheme help here?).
handleInfoClick = (e) => {
if (this.instructionsContent.current.style.display !== "block") {
this.instructionsContent.current.style.display = "block";
this.instructionsButton.current.style.color = "#f9be00"; //works
} else {
this.instructionsContent.current.style.display = "none";
this.instructionsButton.current.style.color = this.theme.palette.text.disabled; // doesn't work
also tried this:
handleSelectFocus = () => {
if (this.state.visited === false) {
this.instructionsContent.current.style.display = "block";
this.instructionsButton.current.style.color = this.activeButtonColor;
this.setState({ visited: true });
}
};
...
render() {
const { theme } = this.props;
...
const activeButtonColor = theme.palette.secondary.main;
Finally, also tried to use the classes I can use within render(), but it doesn't recognize those either:
const styles = theme => ({
...
infoButton: {
position: "absolute",
bottom: 0,
left: 0,
marginBottom: 20,
width: 48,
color: theme.palette.text.disabled,
"&:active": {
color: theme.palette.secondary.main
}
},
infoButtonActive: {
position: "absolute",
bottom: 0,
left: 0,
marginBottom: 20,
width: 48,
color: theme.palette.secondary.main
},
....
Hoping one of these approaches would give me a color for my <IconButton> - from my theme:
<div className={classes.infoButtonDiv}>
<IconButton
aria-label="Instructions"
className={classes.infoButton}
buttonRef={this.instructionsButton}
onClick={this.handleInfoClick}
>
<HelpOutline />
</IconButton>
</div>
(in a different theme.js file applied to the root element:
const theme = createMuiTheme({
typography: {
fontFamily: ["Roboto", '"Helvetica Neue"', "Arial", "sans-serif"].join(",")
},
palette: {
primary: {
main: "#00665e"
},
secondary: {
main: "#f9be00"
}
},
overrides: {
LeftNav: {
drawerDiv: {
backgroundColor: "#00665e",
width: 300
}
}
},
direction: "ltr",
typography: {
useNextVariants: true
}
});
Triggering a state change onClick will update the color, but only if you pass one of the supported values for the IconButton color prop ("primary" or "secondary").
import React, { Component } from "react";
import IconButton from "#material-ui/core/IconButton";
import DeleteIcon from "#material-ui/icons/Delete";
class ButtonStyle extends Component {
constructor(props) {
super(props);
this.state = {
buttonColor: "primary"
};
}
handleClick = e => {
this.setState({
buttonColor: "secondary"
});
};
render() {
const buttonColor = this.state.buttonColor;
return (
<div>
<IconButton
aria-label="Delete"
color={buttonColor}
onClick={this.handleClick}
>
<DeleteIcon />
</IconButton>
</div>
);
}
}
export default ButtonStyle;

How to test if styles are dynamically applied on a React component

I've written a React component, Button:
import PropTypes from 'prop-types'
import Radium from 'radium'
import React from 'react'
import { Icon } from 'components'
import { COLOURS, GLOBAL_STYLES, ICONS, MEASUREMENTS } from 'app-constants'
#Radium
export default class Button extends React.Component {
static propTypes = {
children: PropTypes.string,
dark: PropTypes.bool,
icon: PropTypes.oneOf(Object.values(ICONS)).isRequired,
style: PropTypes.object,
}
render() {
const { children, dark, icon, style } = this.props
let mergedStyles = Object.assign({}, styles.base, style)
if (!children)
mergedStyles.icon.left = 0
if (dark)
mergedStyles = Object.assign(mergedStyles, styles.dark)
return (
<button
className="btn btn-secondary"
style={mergedStyles}
tabIndex={-1}>
<Icon name={icon} style={mergedStyles.icon} />
{children &&
<span style={mergedStyles.text}>{children}</span>
}
</button>
)
}
}
export const styles = {
base: {
backgroundColor: COLOURS.WHITE,
border: `1px solid ${COLOURS.BORDER_LIGHT}`,
borderRadius: GLOBAL_STYLES.BORDER_RADIUS,
cursor: 'pointer',
padding: GLOBAL_STYLES.BUTTON_PADDING,
':focus': {
outline: 'none',
},
':hover': {
boxShadow: GLOBAL_STYLES.BOX_SHADOW,
},
icon: {
fontSize: GLOBAL_STYLES.ICON_SIZE_TINY,
left: '-3px',
verticalAlign: 'middle',
},
text: {
fontSize: GLOBAL_STYLES.FONT_SIZE_TINY,
fontWeight: GLOBAL_STYLES.FONT_2_WEIGHT_MEDIUM,
marginLeft: `${MEASUREMENTS.BUTTON_PADDING.HORIZONTAL}px`,
verticalAlign: 'middle',
},
},
dark: {
backgroundColor: COLOURS.PRIMARY_3,
border: `1px solid ${COLOURS.PRIMARY_2}`,
color: COLOURS.WHITE,
':hover': {
boxShadow: GLOBAL_STYLES.BOX_SHADOW_DARK,
},
},
}
I've also written a test for Button with Jest and Enzyme, which validates if its dark styles are applied when its dark prop is set to true:
import { ICONS } from 'app-constants'
import Button, { styles } from 'components/Button'
describe("<Button>", () => {
let props
let mountedComponent
const getComponent = () => {
if (!mountedComponent)
mountedComponent = shallow(
<Button {...props} />
)
return mountedComponent
}
beforeEach(() => {
mountedComponent = undefined
props = {
children: undefined,
dark: undefined,
icon: ICONS.VIBE,
style: undefined,
}
})
describe("when `dark` is `true`", () => {
beforeEach(() => {
props.dark = true
})
it("applies the component's `dark` styles", () => {
const componentStyles = getComponent().props().style
expect(componentStyles).toEqual(expect.objectContaining(styles.dark))
})
})
})
As you can see, I do this by checking if the properties of styles.dark are inside the rendered Button's style attribute. If they are, then it means the styles have applied successfully.
The issue is that styles.dark and componentStyles don't match:
Output of console.log(styles.dark)
ObjectContaining{
":hover": {
"boxShadow": "0px 0px 0px 2px rgba(0,0,0,0.2)"
},
"backgroundColor": [Object],
"border": "1px solid rgb(47, 52, 63)",
"color": [Object]
}
Output of console.log(componentStyles)
{
"backgroundColor": "rgb(31, 34, 40)",
"border": "1px solid rgb(47, 52, 63)",
"borderRadius": "4px",
"color": "rgb(255, 255, 255)",
"cursor": "pointer",
"padding": "3px 5px 3px 5px"
}
I notice a few issues here:
styles.dark has several Color() [Object]s from the color library. They haven't outputted their rgb() value as a string, but the same properties in componentStyles have, thus resulting in a mismatch.
componentStyles has Radium's interactive styles stripped, such as :focus and :hover (I assume Radium does this during rendering triggered by Enzyme's shallow() function). This causes a mismatch with styles.dark, which doesn't have these properties stripped.
As a result, I'm not sure how to test this. I can't think of any alternative solutions to validate that styles.dark has been applied. I think that doing the following to styles.dark during testing would be a solution:
Recursively cause all Color() [Object]s to process so they output their rgb() value as a string.
Recursively remove all interactive Radium styles (like :focus and :hover)
Doing so would cause styles.dark to equal the value of componentStyles, thus passing the test. I'm just not sure how to do it.
I came back to this a few days later with fresh eyes and thought of a solution:
describe("<Button>", () => {
let props
let mountedComponent
let defaultComponent
const getComponent = () => {
if (!mountedComponent)
mountedComponent = shallow(
<Button {...props} />
)
return mountedComponent
}
beforeEach(() => {
props = {
children: undefined,
dark: undefined,
icon: ICONS.VIBE,
style: undefined,
}
defaultComponent = getComponent()
mountedComponent = undefined
})
describe("when `dark` is `true`", () => {
beforeEach(() => {
props.dark = true
})
it("applies the component's `dark` styles", () => {
const darkStyles = getComponent().props().style
expect(defaultComponent.props().style).not.toEqual(darkStyles)
})
})
})
Rather than asserting that the rendered component's style prop contains the styles.dark (which is brittle), it just checks to see if the styles have changed at all when the dark prop is set to true.

Resources