I have a modal. This modal opens after a user uploads a CSV. They can then make changes to the entries or delete them. The user can Confirm & Continue (go to next page, modal closes) or Upload New File (modal closes, remain on same page). However, if they make any changes, a new button will appear after these two: Download Updated CSV
Currently on my modal I am implementing Focus Trapping for accessibility purposes. My focus trap will prevent user from tabbing outside of the modal - if they hit tab on the last element, it focuses the first, and shift tab on the first will focus the last. Here is where my problem was first noticed.
In my functional component for the modal, I have a useEffect. This useEffect will detect when changes are made and cause a re-render (using a custom Hook) and then will grab a ref of all interactable elements. This is then passed to a function called focusTrap.
Custom hook useIsMount (used for checking if DOM is on first or second render, in case elements are not ready to be referenced):
export const useIsMount = () => {
const isMountRef = useRef(true);
useEffect(() => {
isMountRef.current = false;
}, []);
return isMountRef.current;
};
useEffect that (might be) causing the issue:
const isMount = useIsMount();
useEffect(() => {
if (!isMount) {
const buttonsElement = buttonsRef?.current;
buttonsElement && focusTrap(buttonsElement, onClose);
}
});
!isMount is used here to make sure all content has been rendered before grabbing elements. I'm not using a dependency array so that it will always run the useEffect on re-render.
My focusTrap function that hasn't failed me until now:
export const focusTrap = (modalElement, onClose) => {
const focusableElements = modalElement.querySelectorAll(
'button, [href], input, checkbox, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const handleTabKeyPress = (event) => {
if (event.key === "Tab") {
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
};
const handleEscapeKeyPress = (event) => {
if (event.key === "Escape") {
onClose();
}
};
modalElement.addEventListener("keydown", handleTabKeyPress, true);
modalElement.addEventListener("keydown", handleEscapeKeyPress);
return () => {
modalElement.removeEventListener("keydown", handleTabKeyPress, true);
modalElement.removeEventListener("keydown", handleEscapeKeyPress);
};
};
When the modal renders, all buttons are found just fine and no issues. However when the user makes a change, the useEffect runs again, and the focusTrap function is called again. Each additional change will cause focusTrap to run an additional instance, but with a different list of focusableElements each instance. When the first edit is made, the Download Updated CSV button is rendered conditionally. It shows in the console.logs for focusableElements but only in the second run of focusTrap. This causes the first run to take priority, ignoring the new button and focusing to the first again.
I've tried:
Adding empty dependency array (doesn't catch content change re-renders, doesn't detect Download button)
Adding dependency array for
isMountandtotalEdits/totalDeletes(keep track of edit/delete amounts to render Download button) but I still callfocusTrapas I need an if statement to determine if there's changesif (totalEdits > 0 || totalDeletes > 0) { buttonsElement && focusTrap(buttonsElement, onClose); }A bunch of other things that I honestly can't even recall at this point. Either the focus trap would fail and escape the modal (but detect the new button!) or the button still isn't focusable
It may be possible that a new addEventListener is being added each time focusTrap runs, because whenever I hit Tab the handleTabKeyPress will run one additional time for each change made (i.e. 3 changes/deletions made to CSV table - handleTabKeyPress runs 4 times at once and I log something to the console)
Would it be somehow possible to kill a function that is running before it runs again?
Lastly if I conditionally render this button somewhere that it isn't the last element, it's focused fine (like before the Confirm button). However the focusTrap still runs multiple times and that's not good.
