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 />
:
- When the user hovers on
<Card />
, mount<Button />
- When the user keyboard-navigates into
<Card />
, mount and focus<Button />
. - Allow keyboard users to fully cycle forwards/backwards between
<Card />
components
The 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:
- the first
<Button />
should receive focus as soon as the<Card/>
is tabbed into (not requiring a second tab press) - reverse keyboard navigation (ie shift+tab) does not allow any
<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>
);
}