How to apply multiple classes to components on React 5? - reactjs

Before v5, I was able to apply multiple class to the component like the following:
import React from "react";
import { makeStyles } from '#mui/styles';
const useStyles = makeStyles({
image: {
borderRadius: "50%",
width: 256,
height: 256,
},
shadow: {
boxShadow: "-4px -3px 45px 21px rgba(0,0,0,0.35)",
},
}));
const MyComponent = (props) => {
const { data } = props;
const classes = useStyles();
return (
<img src={data.image} className={`${classes.image} ${classes.shadow}`} alt={data.title} />
);
};
export default MyComponent;
However, since the change of styling engine(?), it seems like I can only apply a single styling like:
import React from "react";
import { styled } from "#mui/system";
const ProfileImage = styled("img")({
borderRadius: "50%",
width: 256,
height: 256,
});
const MyComponent= (props) => {
const { data } = props;
return (
<ProfileImage src={data.image} alt={data.title} />
);
};
export default MyComponent;
Would there be any possible solution to achieve the same behavior as the one above?
Thank you!

You could use an object and just spread the properties you want to use
const classes = {
image: {
borderRadius: "50%",
width: 256,
height: 256
},
shadow: {
boxShadow: "-4px -3px 45px 21px rgba(0,0,0,0.35)"
}
};
const ProfileImage1 = styled("img")({
...classes.image,
...classes.shadow
});
If you are using JS and want to benefit from type intellisense, you could use type annotation to get the hint of CSS properties
import { styled, SxProps, Theme } from "#mui/system";
/** #type{Record<string, SxProps<Theme>>} */
const classes = {
image: {
borderRadius: "50%",
width: 256,
height: 256
},
shadow: {
boxShadow: "-4px -3px 45px 21px rgba(0,0,0,0.35)"
}
};
Also, if you want to make the class conditional, styled component allows you to use prop from the component
const ProfileImage1 = styled("img")(({ shadow }) => ({
...classes.image,
...(shadow ? classes.shadow : {})
}));
export default function App() {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: 5
}}
>
<ProfileImage1 src={"https://via.placeholder.com/256"} shadow />
<ProfileImage1 src={"https://via.placeholder.com/256"} />
</Box>
);
}

Related

Test accordion isCollapsed with overflow hidden and maxHeight

What I Need
I need a way to test if an accordion isCollapsed or not. I tried seeing if there was a way to grab if the maxHeight hasChanged, but from what I have read, it doesn't include measurements within the dom objects for the tests
The Problem
Writing the following test:
import React from 'react';
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import Accordion from './Accordion';
import AccordionItem from './AccordionItem';
const accordionTitle = 'Title: Hello World';
const accordionContent = 'Content: Hello World';
function TestContainer() {
return (
<Accordion>
<AccordionItem title={accordionTitle}>
<p>{accordionContent}</p>
</AccordionItem>
</Accordion>
);
}
describe('Accordion', () => {
const userViewing = userEvent.setup();
it.only('expands content on item control click', async () => {
render(<TestContainer />);
expect(await screen.findByText(accordionContent)).not.toBeInTheDocument();
const accordionItem = screen.getByRole('button', { name: accordionTitle });
userViewing.click(accordionItem);
expect(await screen.findByText(accordionContent)).toBeInTheDocument();
});
});
Results in the following Error:
The Reason
I think this is because the content component still exists in the DOM, but is only being hidden by the overflow: hidden; and maxHeight: 0 or heightOfContent(for animation purposes).
The Component Code
Accordion.tsx:
import React, { ReactNode } from 'react';
function Accordion({ children }: PROPS): JSX.Element {
return <div>{children}</div>;
}
interface PROPS {
children: ReactNode;
}
export default Accordion;
AccordionItem.tsx
import React, { ReactNode, useState } from 'react';
import AccordionControlClick from './AccordionControlClick';
import AccordionContent from './AccordionContent';
import { useStyles } from './Styles';
function AccordionItem({ title, children }: PROPS): JSX.Element {
const classes = useStyles();
const [isCollapsed, setIsCollapsed] = useState(true);
return (
<div className={isCollapsed ? classes.accordionItemClosed : classes.accordionItemOpen}>
<AccordionControlClick
title={title}
isCollapsed={isCollapsed}
toggleIsCollapsed={setIsCollapsed}
/>
{children !== null && (
<AccordionContent isCollapsed={isCollapsed}>{children}</AccordionContent>
)}
</div>
);
}
interface PROPS {
title: string;
children?: ReactNode;
}
export default AccordionItem;
AccordionControlClick.tsx
import React from 'react';
import { useStyles } from './Styles';
function AccordionControlClick({ title, isCollapsed, toggleIsCollapsed }: PROPS): JSX.Element {
const classes = useStyles();
return (
<button
className={classes.accordionControlClick}
type="button"
onClick={() => toggleIsCollapsed(!isCollapsed)}
>
<h2>{title}</h2>
<span className={isCollapsed ? classes.iconChevronWrapper : classes.iconChevronWrapperRotate}>
<i class="fa-solid fa-chevron-down" />
</span>
</button>
);
}
interface PROPS {
title: string;
isCollapsed: boolean;
toggleIsCollapsed: (isOpen: boolean) => void;
}
export default AccordionControlClick;
AccordionContent.tsx
import React, { ReactNode, useRef, useLayoutEffect } from 'react';
import { useStyles } from './Styles';
function AccordionContent({ isCollapsed, children }: PROPS): JSX.Element {
// variables
const componentDomRef = useRef<any>(null);
const componentHeight = useRef(0);
const classes = useStyles();
// setup
useLayoutEffect(() => {
componentHeight.current = componentDomRef.current ? componentDomRef.current.scrollHeight : 0;
}, []);
// render
return (
<div
ref={componentDomRef}
className={classes.accordionContent}
style={isCollapsed ? { maxHeight: '0px' } : { maxHeight: `${componentHeight.current}px` }}
>
{children}
</div>
);
}
interface PROPS {
isCollapsed: boolean;
children: ReactNode;
}
export default AccordionContent;
Styles.ts
import { createUseStyles } from 'react-jss';
import { cssColors, cssSpacing } from '../../utils';
const accordionBoxShadow = '2px 3px 8px 1px rgba(0, 0, 0, 0.2)';
const accordionBoxShadowTransition = 'box-shadow 0.3s ease-in-out 0s;';
export const useStyles = createUseStyles({
accordionContent: {
boxSizing: 'border-box',
width: '100%',
padding: `0 ${cssSpacing.m}`,
overflow: 'hidden',
transition: 'max-height 0.3s ease-in-out'
},
accordionControlClick: {
boxSizing: 'border-box',
display: 'flex',
width: '100%',
alignItems: 'center',
padding: `9px ${cssSpacing.m}`,
border: 'none',
borderRadius: '8px',
outline: 'none',
backgroundColor: `${cssColors.backgroundLevel2}`,
cursor: 'pointer'
},
accordionItemClosed: {
boxSizing: 'border-box',
width: '100%',
marginBottom: cssSpacing.l,
border: `2px solid ${cssColors.accordionTitleBorder}`,
borderRadius: '8px',
boxShadow: 'none',
transition: 'none',
'&:hover': {
border: `2px solid ${cssColors.accordionTitleBorder}`,
boxShadow: accordionBoxShadow,
transition: accordionBoxShadowTransition
}
},
accordionItemOpen: {
boxSizing: 'border-box',
width: '100%',
marginBottom: cssSpacing.l,
border: `2px solid ${cssColors.accordionTitleBorder}`,
borderRadius: '8px',
boxShadow: accordionBoxShadow,
transition: accordionBoxShadowTransition,
'&:hover': {
border: `2px solid ${cssColors.accordionTitleBorder}`,
boxShadow: accordionBoxShadow,
transition: accordionBoxShadowTransition
}
},
iconChevronWrapper: {
marginLeft: 'auto',
transform: 'none',
transition: 'transform 300ms ease'
},
iconChevronWrapperRotate: {
marginLeft: 'auto',
transform: 'rotate(180deg)',
transition: 'transform 300ms ease'
}
});
In the AccordionContent.tsx file, I added in the following visibility
style={ isCollapsed ? { maxHeight: '0px', visibility: 'hidden' } : { maxHeight: `${componentHeightRef.current}px`, visibility: 'visible' } }
In the test file I make the following changes to test if the content is ToBeVisible vs toBeInTheDocument:
it.only('expands content on item control click', async () => {
render(<TestContainerOneItem />);
const accordionContentContainer = (await screen.findByText(accordionContent)).parentElement;
await waitFor(() => expect(accordionContentContainer).not.toBeVisible());
const accordionControlButton = screen.getByRole('button', { name: accordionTitle });
await userViewing.click(accordionControlButton);
await waitFor(() => expect(accordionContentContainer).toBeVisible());
});

custom style rules on html and body tags with react jss, #global does not get applied

i am attempting to apply custom style rules to: all elements, html, and body tags using react jss, in a React TS library.
when the component inspected in storybook the '#global' rules non present
here is the relevant code section:
import React from 'react';
import jss from 'jss';
import { AppShellProps } from '../interfaces/AppShellProps';
import { StyledThemeContextProvider } from 'ui-context';
const globalStyles = {
'#global': {
'*': {
margin: 0,
padding: 0,
boxSizing: 'border-box',
},
html: {
fontSize: '16px',
overflowY: 'hidden',
overflowX: 'hidden',
},
body: {
width: '100%',
minHeight: '100vh',
}
};
export const AppShell: React.FC<AppShellProps> = ({ ...props }) => {
const { classes } = jss
.createStyleSheet({
shellMain: {
border: '3px dashed purple',
},
...globalStyles,
})
.attach();
return (
<main className={classes.shellMain}>
<StyledThemeContextProvider themeName={props.themeName}>{props.children}</StyledThemeContextProvider>
</main>
);
};
and here is the result of the inspection:
Q: how would it be possible to achieve to apply the globalStyle style rules. and stay close to the react jss implementation? or simply just what do i do wrong :)
as it turns out i had left the jss import when i was experimenting, i replaced
import jss from 'jss';
with:
import { createUseStyles } from 'react-jss';
and the classes is implemented like such:
export const AppShell: React.FC<AppShellProps> = ({ ...props }) => {
const classes = createUseStyles({
shellMain: {
border: '3px dashed purple',
},
...globalStyles,
}).apply({});
return (
<main className={classes.shellMain}>
<StyledThemeContextProvider themeName={props.themeName}>{props.children}</StyledThemeContextProvider>
</main>
);
};
this implementation results in global style rules get applied:

Video element in react does not play the stream

The html video element below does not play the webcam stream.
setState works fine since the browser notifies me that the site is accessing the camera.
It still invokes the webcam but the html video element is not activated after state change.
What i see is the black screen even if the webcam is active.
There are no error messages on browser console
any help appreciated
import React, {useState,useEffect} from 'react';
import PropTypes from "prop-types";
import { withStyles } from "#material-ui/core/styles";
import Paper from "#material-ui/core/Paper";
import { connect } from "react-redux";
const styles = () => ({
root: {
display: "flex",
flexDirection: "column",
width: "20%",
height: "25%",
overflowY: "auto",
},
videoPreview:{
alignSelf: "center",
width: "30%",
backgroundColor: "rgba(0, 0, 0, 0.25)",
marginTop: 20,
},
});
const Preview = (props) => {
const {classes} = props;
const [videoPreviewTrack, setVideoPreviewTrack] = useState(navigator.mediaDevices.getUserMedia({video:true}) );
useEffect(() => {
//something here maybe?
});
return (
<div className={classes.videoPreview}>
<video src={videoPreviewTrack} autoPlay={true} id={"videoPreviewElement"}>
</video ></div>
);
};
Preview.propTypes = {
classes: PropTypes.object.isRequired,
};
export default connect()(withStyles(styles)(Preview));
import React, {useState,useEffect,useRef} from 'react';
import PropTypes from "prop-types";
import { withStyles } from "#material-ui/core/styles";
import Paper from "#material-ui/core/Paper";
import { connect } from "react-redux";
import Select from '#material-ui/core/Select';
const styles = () => ({
root: {
display: "flex",
flexDirection: "column",
width: "20%",
height: "25%",
overflowY: "auto",
},
previewBackground:{
alignSelf: "center",
backgroundColor: "rgba(0, 0, 0, 0.25)",
marginTop: 20,
},
videoPreview:{
alignSelf: "center",
width: "15%",
backgroundColor: "rgba(0, 0, 0, 0.25)",
marginTop: 5,
},
});
const Preview = (props) => {
const {classes} = props;
const videoRef = useRef(null);
useEffect(() => {
getVideo();
}, [videoRef]);
const getVideo = () => {
navigator.mediaDevices
.getUserMedia( {video: true} )
.then(stream => {
let video = videoRef.current;
video.srcObject = stream;
video.play();
})
.catch(err => {
console.error("error:", err);
});
};
return (<div align={"center"} className={classes.previewBackground} >
<div >
<video className={classes.videoPreview} ref={videoRef} >
</video >
</div>
</div>
);
};
Preview.propTypes = {
classes: PropTypes.object.isRequired,
};
export default (withStyles(styles)(Preview));

React + Aphrodite Fonts

I have a problem with add font in React with use Aphrodite.
In the Aphrodite documentation (https://github.com/Khan/aphrodite) we have described how to add our font to project, but it didn't work for me.
In terminal (vsc), after npm start I don't see any errors,
but in the browser (chrome) I have this: TypeError: _nested.set is not a function.
import React, { FC, useState } from "react";
import ScreenWrapper from "components/ScreenWrapper";
import StyledInput from "components/StyledInput";
import TextArea from "components/TextArea";
import StyledText from "components/StyledText";
import { StyleSheet, css } from "aphrodite";
import { colors } from "styles";
import { i18n } from "locale";
import "fonts/Roboto.woff";
interface Props {}
const HomeScreen: FC<Props> = () => {
const [inputValue1, setInputValue1] = useState<string>("");
const [textareaValue1, setTextareaValue1] = useState<string>("");
const handleInputValue = (value: string): void => {
setInputValue1(value);
};
const handleTextareaValue = (value: string): void => {
setTextareaValue1(value);
};
return (
<ScreenWrapper>
<div className={css(styles.homeScreen)}>
<h3>{i18n.t("header:title")}</h3>
<StyledInput
value={inputValue1}
onChange={handleInputValue}
placeholder={i18n.t("textAreaPlaceholder:search")}
/>
<TextArea
value={textareaValue1}
onChange={handleTextareaValue}
placeholder={i18n.t("textAreaPlaceholder:type")}
/>
<StyledText style={styles.testText}>{i18n.t("test:lorem")}</StyledText>
</div>
</ScreenWrapper>
);
};
const Roboto = {
fontFamily: "Roboto",
fontStyle: "normal",
fontWeight: "normal",
src: "url('fonts/Roboto.woff') format('woff')",
};
const styles = StyleSheet.create({
homeScreen: {
flexDirection: "column",
alignItems: "stretch",
display: "flex",
flexGrow: 1,
backgroundColor: colors.white,
textAlign: "center",
textTransform: "uppercase",
color: colors.blue4,
},
testText: {
margin: "20px 10vw",
fontFamily: Roboto,
},
});
export default HomeScreen;
Had anyone similar problem?
Try the following.
const styles = StyleSheet.create({
testText: {
margin: "20px 10vw",
fontFamily: [AldotheApacheFont, "sans-serif"],
},
});
Or check below https://www.npmjs.com/package/aphrodite-local-styles#font-faces

Styled-components - dynamic CSS?

I have a simple React app that gets CSS from an external source in JSON form, the styles look like this:
{
"DOMElements": [
{
"padding": "1rem",
"margin": "5rem",
},
{
"boxSizing": "border-box",
"height": "10px",
}
]
}
So when I get a response like above I want end up with something like this:
import styled from 'styled-components';
const DOMElement1 = styled.div`
padding: 1rem,
margin: 5rem,
`;
const DOMElement2 = styled.div`
boxSizing: border-box,
height: 10px,
`;
const Page = ({ children }) => (
<>
<DOMElement1>{children[1]}</DOMElement1>
<DOMElement2>{childrem[2]}</DOMElement2>
</>
);
It's worth noting that number of DOMElements in unknown, it might be 1, it might be 50.
The component part is easy, I can do just do indexed map and increase index every loop.
The problem I face is - how do I create dynamic styled-components props based on JSON response? I need to do it within the component itself as that's where I know how DOMElements look like, but styled-components are supposed to be outside of the component function... What am I missing? Is it even doable?
You can give props for styled components.
const DOMElement1 = styled.div`
padding: ${({padding}) => padding}rem,
margin: ${({margin}) => margin}rem,
`;
const Page = ({ children }) => (
<>
<DOMElement1 padding={valueFromJson} margin={valueFromJson}>{children[1]}</DOMElement1>
<DOMElement2 padding={valueFromJson} margin={valueFromJson}>{childrem[2]}</DOMElement2>
</>
);
Something like this:
const items = [
{
background: "blue",
width: "30px",
height: "30px",
padding:'10px'
},
{
height: "40px",
background: "red",
width: "30px",
padding:'10px'
}
];
const components = items.map((item) =>
styled("div")({
...item
})
);
export default function App() {
return (
<div style={{ background: "lightblue", width: "100vw", height: "100vh" }}>
{components.map(Comp => <Comp>hi</Comp>)}
</div>
);
}
A simple & straightforward solution would be taking props dynamically in your styled component and this way you don't need to worry about any incoming css property.
Refer the following example/ this snippet -
const DOMElement = styled.div`
${props => props}
`;
const Page = () => {
const data = {
"DOMElements": [
{
"padding": "1rem",
"margin": "5rem",
},
{
"boxSizing": "border-box",
"height": "10px",
}
]
};
return (
<>
{data?.DOMElements?.map((obj, index) =>
<DOMElement {...obj} key={index}>abcd</DOMElement>
)}
</>
);
}
Output snap -

Resources