Reputation: 5464
In a React application, I have a grid of <Card />
components. Each <Card />
renders several <Button />
components.
Due to constraints outside my control, Button
is an inefficient component. It is expensive to render, and we need to render hundreds of <Card />
components, so it becomes a performance concern.
To account for this, I only want <Button />
to mount/render when the user is actively interacting with the <Card />
:
<Card />
, mount <Button />
<Card />
, mount and focus <Button />
.<Card />
componentsThe hover portion is fairly straightforward, and my implementation can be seen below.
I am having trouble with the keyboard portion. Since <Button />
is not mounted yet, how can the user keyboard navigate to it?
CodeSandbox link of what I've tried
This mostly works, except in two important ways:
<Button />
should receive focus as soon as the <Card/>
is tabbed into (not requiring a second tab press)<Button>
to be focused. It should focus the "last" button, and allow the user to navigate through all the buttons and then out to the previous <Card>
Question: What modifications must I make to the below code to support the above two points?
import { useState, Fragment, useEffect, useRef, MutableRefObject } from "react";
import Button from "./Button";
export default function Card() {
const [isHovered, setIsHovered] = useState<boolean>(false);
const [showButtons, setShowButtons] = useState<boolean>(false);
const containerRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
useEffect(() => {
setShowButtons(isHovered);
}, [isHovered]);
return (
<div
onMouseOver={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onFocus={(event) => {
event.persist();
setIsHovered(true);
}}
onBlur={(event) => {
event.persist();
window.requestAnimationFrame(() => {
if (containerRef.current) {
setIsHovered(
containerRef.current.contains(document.activeElement) === true
);
}
});
}}
tabIndex={0}
>
<div ref={containerRef}>
<h2>Card</h2>
{showButtons && (
<Fragment>
<Button>Expensive Button One</Button>
<Button>Expensive Button Two</Button>
</Fragment>
)}
</div>
</div>
);
}
Upvotes: 3
Views: 1066
Reputation: 78
You can reach the desired outcome with simple CSS (the opacity
property) and a state change on focus.
Be aware that making a non-interactive Card container component interactive with tabIndex={0}
is an anti-pattern.
//styles.css
.invisible-button {
opacity:0;
}
//Button.tsx
import React, { useState, FC } from "react";
export const Button: FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [hasBeenFocused, setHasBeenFocused] = useState(false);
const handleFocus = () => setHasBeenFocused(true);
const handleBlur = () => setHasBeenFocused(false);
return (
<button
className={hasBeenFocused ? "" : "invisible-button"}
onFocus={handleFocus}
onBlur={handleBlur}
onMouseEnter={handleFocus}
onMouseLeave={handleBlur}
>
{children}
</button>
);
};
export default Button;
Have a look at the complete sandbox: https://codesandbox.io/p/sandbox/intelligent-matsumoto-frc9kk?file=%2Fsrc%2FApp.js%3A14%2C1
If you wish to have just the buttons receive focus you do not need to make your card component interactive with the tabIndex property.
I hope my solution helps :-)
Upvotes: 0