Test accordion isCollapsed with overflow hidden and maxHeight - reactjs

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

Related

Uncaught TypeError: Cannot read properties of undefined (reading '100')

import { useState } from 'react';
import { ProSidebar, Menu, MenuItem } from 'react-pro-sidebar';
import { Box, IconButton, Typography, useTheme } from '#mui/material';
import { Link } from 'react-router-dom';
import 'react-pro-sidebar/dist/css/styles.css';
import { tokens } from '../../theme';
const Sidebar = () => {
const theme = useTheme();
const colors = tokens(theme.palette.mode);
const [isCollapsed, setIsCollapsed] = useState(false);
const [selected, setSelected] = useState('Dashboard');
return (
<Box
sx={{
'& .pro-sidebar-inner': {
background: `${colors.primary[400]} !important`,
},
'& .pro-icon-wrapper': {
backgroundColor: 'transparent !important',
},
'& .pro-inner-item': {
padding: '5px 35px 5px 20px !important',
},
'& .pro-inner-item:hover': {
color: '#868dfb !important',
},
'& .pro-menu-item.active': {
color: '#6870fa !important',
},
}}
>
<ProSidebar collapsed={isCollapsed}>
<Menu iconShape="square">
{/* LOGO AND MENU ICON */}
<MenuItem
onClick={() => setIsCollapsed(!isCollapsed)}
icon={isCollapsed ? <MenuOutlinedIcon /> : undefined}
style={{
margin: '10px 0 20px 0',
color: colors.grey[100],
}}
>
</MenuItem>
</Menu>
</ProSidebar>
</Box>
);
};
export default Sidebar;
This is Sidebar code
I will be able get the output as dark and light theme. and the sidebar , navbar and the profile user is of left side
but I am getting blank while page and I didn't get any error in the terminal.

How to apply multiple classes to components on React 5?

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

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

Material UI pagination - How Can I use custom style for number of pages?

I'm quite new to material-ui. I'm trying to build this component.
I was able to do the style for the next and previous buttons the same as in the picture.
The normal style shows the number of pages as a numbered group besides each other like this:
Are there any properties that I can pass for the pagination component, in which I can change the style?
Here is the code:
import Pagination from "#material-ui/lab/Pagination";
import useStyles from "./styles";
const ReviewsPagination = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<Pagination count={8} />
</div>
);
};
export default ReviewsPagination;
and the style file:
import { makeStyles } from "#material-ui/core/styles";
const useStyles = makeStyles({
root: {
"& .MuiPagination-ul": {
"& > li:first-child": {
"& button": {
borderRadius: "50%",
border: "1px solid black",
width: "48px",
height: "48px",
},
},
"& > li:last-child": {
"& button": {
borderRadius: "50%",
border: "1px solid black",
width: "48px",
height: "48px",
},
},
},
},
});
export default useStyles;
Thank you!
you can use the usePagination hook to customize the pagination component. Like below:
export default function UsePagination() {
const classes = useStyles();
const { items } = usePagination({
count: 10,
});
return (
<nav>
<ul className={classes.ul}>
{items.map(({ page, type, selected, ...item }, index) => {
let children = null;
if (type === 'next' || type === 'previous') {
children = (
<button type="button" {...item}>
{type}
</button>
);
}else if(selected){
children = <div>{`${page}/10`}</div>
}
return <li key={index}>{children}</li>;
})}
</ul>
</nav>
);
}

StopPropagation react google maps autocomplete

I would like to stopPropagation of https://react-google-maps-api-docs.netlify.app/#autocomplete.
The autocomplete works well but I need to use it in a popover and when I select a place the popover automatically closes.
If there is another library that works well with popovers and modals
const [autocomplete, setAutocomplete] = useState<any>(null);
const onLoad = (auto: any) => {
if (!autocomplete) {
setAutocomplete(auto);
}
};
const onPlaceChanged = () => {
if (autocomplete) {
console.log(autocomplete?.getPlace());
} else {
console.log('Autocomplete is not loaded yet!');
}
};
<Autocomplete
onLoad={onLoad}
onPlaceChanged={onPlaceChanged}
>
<chakra.input
type='text'
as={Input}
placeholder='Customized your placeholder'
style={{
boxSizing: `border-box`,
border: `1px solid transparent`,
width: `240px`,
height: `32px`,
padding: `0 12px`,
borderRadius: `3px`,
boxShadow: `0 2px 6px rgba(0, 0, 0, 0.3)`,
fontSize: `14px`,
outline: `none`,
textOverflow: `ellipses`,
position: 'absolute',
left: '50%',
marginLeft: '-120px',
animationName: 'none',
zIndex: 9999,
}}
/>
</Autocomplete>
I'm using chakraUI, I get help from discord(dodas user) here is the solution he make. Basically he catch the mousedown event(google block click event) and when the popover is open, select item or click trigger button just do nothing for others events just close popovers.
https://codesandbox.io/s/popovergoogle-autocomplete-6nvtb?file=/src/App.js:0-3329
import React, { useState, useEffect, useRef } from "react";
import {
Button,
Stack,
Popover,
PopoverTrigger,
PopoverContent,
PopoverHeader,
PopoverBody,
PopoverArrow,
PopoverCloseButton,
Box,
Portal,
PopoverFooter,
useRadioGroup,
useRadio
} from "#chakra-ui/core";
import { useEvent } from "react-use";
import {
GoogleMap,
useJsApiLoader,
Autocomplete
} from "#react-google-maps/api";
let autoComplete;
export function App() {
const { isLoaded, loadError } = useJsApiLoader({
googleMapsApiKey: "YOUR_KEY", // ,
libraries: ["places"]
// ...otherOptions
});
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const openPopover = () => setIsPopoverOpen(true);
const closePopover = () => setIsPopoverOpen(false);
const triggerRef = useRef(null);
const popoverContentRef = useRef(null);
const [autocomplete, setAutocomplete] = useState(null);
const onLoad = (autocompleteInstance) => {
console.log("autocomplete: ", autocomplete);
setAutocomplete(autocompleteInstance);
};
const onPlaceChanged = () => {
if (autocomplete) {
console.log(autocomplete.getPlace());
} else {
console.log("Autocomplete is not loaded yet!");
}
};
useEvent("mousedown", (ev) => {
if (!isPopoverOpen) {
return;
}
const clickedInsideTrigger = triggerRef.current.contains(ev.target);
const clickedInsidePopover = popoverContentRef.current.contains(ev.target);
const clickedInsideAutocomplete =
ev.target.closest(".pac-container") != null;
if (
clickedInsideTrigger ||
clickedInsidePopover ||
clickedInsideAutocomplete
) {
return;
}
closePopover();
});
return (
<>
<Box width="100vw" height="100vh">
<Popover
isOpen={isPopoverOpen}
onOpen={openPopover}
onClose={closePopover}
closeOnBlur={false}
isLazy
placement="bottom-end"
>
<PopoverTrigger>
<Button ref={triggerRef}>Trigger</Button>
</PopoverTrigger>
<PopoverContent ref={popoverContentRef}>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>Confirmation!</PopoverHeader>
<PopoverBody minH="20rem">
{isLoaded && (
<Autocomplete onLoad={onLoad} onPlaceChanged={onPlaceChanged}>
<input
type="text"
placeholder="Customized your placeholder"
style={{
boxSizing: `border-box`,
border: `1px solid transparent`,
width: `240px`,
height: `32px`,
padding: `0 12px`,
borderRadius: `3px`,
boxShadow: `0 2px 6px rgba(0, 0, 0, 0.3)`,
fontSize: `14px`,
outline: `none`,
textOverflow: `ellipses`,
position: "absolute",
left: "50%",
marginLeft: "-120px"
}}
/>
</Autocomplete>
)}
</PopoverBody>
</PopoverContent>
</Popover>
</Box>
</>
);
}

Resources