Testing UI with conditional rendering based on screen size - reactjs

I'm using react-testing-library with jest to test storybook stories. I have a component that, based on it's scrollWidth and the clientWidth it conditionally renders a button to page through the component:
function Collection({
children,
scrollOffset = DEFAULT_SCROLL_X_OFFSET,
onScroll,
...rest
}: HorizontalChipCollectionProps) {
const [scrollX, setScrollX] = useState(0); // For detecting start scroll postion
const [showRightPager, setShowRightPager] = useState(false); // For detecting end of scrolling
const scrollableAreaRef = useRef();
const scrollableAreaCallback = React.useCallback((node) => {
scrollableAreaRef.current = node;
/** Check to see if the scrollWidth is less than the full width, if so hide the right arrow */
if (node?.scrollWidth <= node?.clientWidth) {
setShowRightPager(true);
}
}, [setShowRightPager]);
const leftPageOnClick = () => pagerOnClick(PAGER_DIRECTION.LEFT);
const rightPageOnClick = () => pagerOnClick(PAGER_DIRECTION.RIGHT);
return (
<Box>
<ScrollableArea
py={{
base: 'xsmall',
medium: 'small',
}}
display="flex"
ref={scrollableAreaCallback}
onScroll={scrollCurrCheck}
overflowX="auto"
css={css`
> *:not(:last-child) {
margin-right: var(--space-small);
}
`}
{...rest}
>
{children}
</ScrollableArea>
{!showRightPager ? <Pager data-testid="right-pager" variant="right" onClick={rightPageOnClick} /> : null}
</Box>
);
}
I'm trying to test the rightPageOnClick to confirm it works but I'm unable to find the element in my test. Here is my test:
test('should show pager buttons', () => {
window.innerWidth = 1440;
window.innerHeight = 900;
window.dispatchEvent(new Event('resize'));
render(<Basic />);
const rightPager = screen.getByTestId('right-pager');
expect(rightPager).toBeInTheDocument();
});
Let me know if you need any more info from me!

Related

Reactjs virtualization infinit scroll

I'm trying to implement a virtualisation component with reactjs.
In order to fill the empty top and bottom gap I used an empty div with dynamic height which depends on the scroll position.
<div style={{ height: topPlaceholderHeight }} />
{visibleItems.map(renderItem)}
<div style={{ height: bottomPlaceholderHeight }} />
When the user scrolls > a scroll event is triggered > placeholderHeight is updated > triggers an other scroll event => infinite loop that leads to an auto scroll to the bottom
const [start, setStart] = useState(0);
const [end, setEnd] = useState(0);
const [scrollTop, setScrollTop] = useState(0);
const parentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const listElement = parentRef.current;
if (!listElement) return;
const itemsPerPage = Math.ceil(listElement.offsetHeight / itemHeight) + 2;
const newStart = Math.max(0, Math.floor(scrollTop / itemHeight) - 1);
const newEnd = Math.min(items.length, newStart + itemsPerPage);
setStart(newStart);
setEnd(newEnd);
}, [items, itemHeight, scrollTop]);
useEffect(() => {
const listElement = parentRef.current;
if (!listElement) return;
const scrollHandler = () => {
setScrollTop(listElement.scrollTop);
};
listElement.addEventListener("scroll", scrollHandler);
return () => listElement.removeEventListener("scroll", scrollHandler);
}, [parentRef]);
Code sandbox
Any suggestions for solving this problem ?

why the change of variant is not captured in framer motion?

I am trying to perform an animation fadein and fadeout to the images in a gallery with framer motions variants. The problem is I didn't manage to trigger the second animation.
So far, I have created a holder for the animation to be displayed till the image load, which I call DotLottieWrapper. I trigger the animation of this holder in the first render and when the loading state changes but the second looks like is not happening. My code is bellow:
const [loading,setLoading] = useState(1);
const loadedDone = () => {
const animationTime = 6000
async function wait() {
await new Promise(() => {
setTimeout(() => {
setLoading(0)
}, animationTime);
})
};
wait();
}
const StyledWrapper = wrapperStyle ?
styled(wrapperStyle)`position:relative;` :
styled(imageStyle)`
${cssImageStyle(src)}
`;
const animationVariants = {
0:{
backgroundColor:'#300080',
opacity:0,
transition:{duration:1.5}
},
1:{
opacity:1,
backgroundColor:'#702090',
transition:{duration:1.5}
}
}
return (
<StyledWrapper
as={NormalDiv}
onClick={handleClick || function () { }}
>
{(loading < 2) && (
<DotLottieWrapper
initial='0'
animate={loading===1?'1':'0'}
variants={animationVariants}
key={IDGenerator()}
>
</DotLottieWrapper>
)}
{(loading < 2) && (
<img
key={IDGenerator()}
onLoad={() => {
loadedDone();
}}
src={src}
/>
)}
</StyledWrapper>
)
Have I done something wrong?

React Link framer motion animation with AnimatePresence

I have a Navigation component in which the Menu Items float in separately on load and float out on click.
When I added Router and changed the items to Links, the exit animation didn't work because it loaded the new Route component right away.
I want to keep the items individual animation with Link functionality.
Here is the link:
https://codesandbox.io/s/elastic-leaf-fxsswo?file=/src/components/Navigation.js
Code:
export const Navigation = () => {
const navRef = useRef(null);
const onResize = () => {
setIsColumn(window.innerWidth <= 715);
};
const [clickOnMenu, setClick] = useState(false);
const [itemtransition, setTransition] = useState(
Array(menuItems.length).fill(0)
);
const [isColumn, setIsColumn] = useState(window.innerWidth <= 715);
const click = (e) => {
const copy = [...itemtransition];
const index = e.target.id;
setTransition(copy.map((e, i) => (Math.abs(index - i) + 1) / 10));
setTimeout(() => setClick(true), 50);
};
useEffect(() => {
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return (
<AnimatePresence exitBeforeEnter>
{!clickOnMenu && (
<Nav ref={navRef}>
{menuItems.map((e, i) => {
const text = Object.keys(e)[0];
const value = Object.values(e)[0];
return (
<Item
id={i}
key={value}
animate={{
x: 0,
y: 0,
opacity: 1,
transition: { delay: (i + 1) / 10 }
}}
initial={{
x: isColumn ? 1000 : 0,
y: isColumn ? 0 : 1000,
opacity: 0
}}
exit={{
x: isColumn ? -1000 : 0,
y: isColumn ? 0 : -1000,
opacity: 0,
transition: { delay: itemtransition[i] }
}}
onClick={click}
>
{/*<Link to={`/${value}`}>{text}</Link>*/}
{text}
</Item>
);
})}
</Nav>
)}
</AnimatePresence>
);
};
In the sandbox in Navigation.js 69-70. row:
This is the desired animation.
69. {/*<Link to={`/${value}`}>{text}</Link>*/}
70. {text}
But when I use Link there is no exit animation
69. <Link to={`/${value}`}>{text}</Link>
70. {/*text*/}
Is there a workaround or I should forget router-dom.
Thank you in forward!
This may be a bit hackish, but with routing and transitions sometimes that is the nature. I suggest rendering the Link so the semantic HTML is correct and add an onClick handler to prevent the default navigation action from occurring. This allows any transitions/animations to go through. Then update the click handler of the Item component to consume the link target and issue an imperative navigation action on a timeout to allow transitions/animations to complete.
I used a 750ms timeout but you may need to tune this value to better suit your needs.
Example:
...
import { Link, useNavigate } from "react-router-dom";
...
export const Navigation = () => {
const navRef = useRef(null);
const navigate = useNavigate(); // <-- access navigate function
...
const click = target => (e) => { // <-- consume target
const copy = [...itemtransition];
const index = e.target.id;
setTransition(copy.map((e, i) => (Math.abs(index - i) + 1) / 10));
setTimeout(() => {
setClick(true);
}, 50);
setTimeout(() => {
navigate(target); // <-- navigate after some delta
}, 750);
};
...
return (
<AnimatePresence exitBeforeEnter>
{!clickOnMenu && (
<Nav ref={navRef}>
{menuItems.map((e, i) => {
const text = Object.keys(e)[0];
const value = Object.values(e)[0];
return (
<Item
...
onClick={click(`/${value}`)} // <-- pass target to handler
>
<Link
to={`/${value}`}
onClick={e => e.preventDefault()} // <-- prevent link click
>
{text}
</Link>
</Item>
);
})}
</Nav>
)}
</AnimatePresence>
);
};
...

Pigeon Maps Popup get rendered infinitely onMouseOver

When I hover over a geopoint marker the popover appears and then disappears infinitely and until I move mouse off of marker. My HandleMouseOver function should only show the popup once, I am unsure why it is looping through multiple times:
const handleMarkerMouseOver = (args: any) => {
console.log("arges", args)
setPopoverVisible(true)
setDoc(args.payload.doc_fields)
setAnchor(args.anchor)
setPopoverContent(args.payload.doc_fields['#service_address'])
}
All of code:
export function NewMap(props: any): ReactElement {
const [anchor, setAnchor] = useState(undefined)
const [popoverVisible, setPopoverVisible] = useState(false)
const [popoverContent, setPopoverContent] = useState(undefined)
const [doc, setDoc] = useState()
const [showVisualDrawer, setShowVisualDrawer] = useState(false)
const pageSizeInRedux = useAppSelector(selectPageSize)
const mapFormData = props
const mapData = useWaveWatcherEventsMapDataQuery({
index: props.index,
pageSize: pageSizeInRedux,
body: props.body
})
let dataLength = 1
if(mapData.data !== undefined) {
dataLength = mapData?.data?.buckets?.map((t: { doc_count: any; }) => t.doc_count).reduce((a: any, b: any) => a + b, 0);
}
let waveContent: ReactElement = <></>
if (mapData.isSuccess && dataLength > 1) {
const handleMarkerClick = (args: any) => {
setPopoverVisible(true)
setDoc(args.payload.doc_fields)
setAnchor(args.anchor)
setPopoverContent(args.payload.doc_fields['#service_address']);
}
const handleMarkerMouseOut = (args: any) => {
setPopoverVisible(false)
}
const handleMarkerMouseOver = (args: any) => {
console.log("arges", args)
setPopoverVisible(true)
setDoc(args.payload.doc_fields)
setAnchor(args.anchor)
setPopoverContent(args.payload.doc_fields['#service_address'])
}
let center: any = [0, 0]
const points = mapData.data.buckets.map((e: any, index: number) => {
center = e.location.split(',').map((c: string) => (parseFloat(c))) || utilityCoordinates
return (
<Marker key={index}
width={20}
color={Color.darkblue}
anchor={center}
payload={e}
onClick={handleMarkerClick}
onMouseOut={handleMarkerMouseOut}
onMouseOver={handleMarkerMouseOver}
/>
)
}
)
return (
waveContent = <>
<MapStats data={mapData} mapFormData={mapFormData}/>
<div className={styles.mapWrapper}>
<Map height={443} defaultCenter={center} defaultZoom={13}>
{points}
<ZoomControl/>
<Overlay anchor={anchor} offset={[0, 0]}>
<Popover
visible={popoverVisible}
content={popContent}
title={'Marker Details'}
>
</Popover>
</Overlay>
</Map>
</div>
This is the popover that appears and disappears infinitely:
This may or may not be helpful, but it seems related to a problem that I also just encountered today. My Popover would appear then disappear over and over. The culprit was the backdrop that loads (similar to how a modal backdrop loads in the background) so then my component would think I'm not moused over it anymore, then it would disappear, then it would think I'm on it again, reappear, etc... I eventually just used a MUI Popper instead of the Popover. Hope this helps.

How to add or remove a className when screen size change in react

I want the className should be added or removed automatically when the screen size changes.
I tried using media queries, but that didn't really work for me. In my stylesheet I have predefined classes for example:
.bold { font-weight: bold;}
and this classes cannot be added or removed using media queries.
Attach a resize event handler to window, then you can check the screen size and use that to update state. Then in render, you should check state to tell whether or not the new class should be added or removed:
componentDidMount() {
window.addEventListener('resize', () => {
this.setState({
isMobile: window.innerWidth < 1200
});
}, false);
}
render() {
const className = this.state.isMobile ? 'mobile' : '';
return (
<Component className={className} />
)
}
If you're looking to do this with hooks instead of classes, here is a simple example for this case:
const [isMobile, setIsMobile] = useState(window.innerWidth < 1200);
{/* Performs similarly to componentDidMount in classes */}
useEffect(() => {
window.addEventListener("resize", () => {
const ismobile = window.innerWidth < 1200;
if (ismobile !== isMobile) setIsMobile(ismobile);
}, false);
}, [isMobile]);
{/* There is no need for a render function with Hooks */}
return (
<p className={`${isMobile ? "mobile-class" : "non-mobile-class"}`}>Your text here</p>
);
For a more in-depth explanation of the useEffect hook, check out the official React documentation here. Please note that you must be using React 16.8 or higher to take advantage of hooks.
the solution above is firing the event every time you resize, perhaps something like this would be a better option:
const mediaQuery: string = "(min-width: 768px)";
const mediaQueryMatch = window.matchMedia(mediaQuery);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const handleClassByMediaQuery = (event: { matches: any }) => {
const isMobile = event.matches;
return setIsMobile(isMobile);
};
mediaQueryMatch.addEventListener("change", handleClassByMediaQuery);
return () => {
mediaQueryMatch.removeEventListener("change", handleClassByMediaQuery);
};
}, [isMobile, mediaQueryMatch]);

Resources