From 04bdeea87d74c3ab1fd47a7f272504a843129aad Mon Sep 17 00:00:00 2001 From: Michael Webber Date: Thu, 23 Jul 2026 13:26:46 -0700 Subject: [PATCH 1/4] feat: modal accessibility and escape key features for unity react and bootstrap --- .../unity-bootstrap-theme/src/js/modals.js | 6 ++ .../src/components/Modal/Modal.tsx | 62 ++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/unity-bootstrap-theme/src/js/modals.js b/packages/unity-bootstrap-theme/src/js/modals.js index 50b9b6c816..324335d3bb 100644 --- a/packages/unity-bootstrap-theme/src/js/modals.js +++ b/packages/unity-bootstrap-theme/src/js/modals.js @@ -5,6 +5,7 @@ function initModals() { .getElementById("openModalButton") ?.addEventListener("click", function () { document.getElementById("uds-modal").classList.add("open"); + document.getElementById("closeModalButton").focus(); }); document @@ -12,6 +13,11 @@ function initModals() { ?.addEventListener("click", function () { document.getElementById("uds-modal").classList.remove("open"); }); + + document?.addEventListener("keydown", function (event) { + event.key === "Escape" && + document.getElementById("uds-modal").classList.remove("open"); + }); } EventHandler.on(window, "load.uds.modals", initModals); diff --git a/packages/unity-react-core/src/components/Modal/Modal.tsx b/packages/unity-react-core/src/components/Modal/Modal.tsx index cf043a551d..7fb0d0ffd7 100644 --- a/packages/unity-react-core/src/components/Modal/Modal.tsx +++ b/packages/unity-react-core/src/components/Modal/Modal.tsx @@ -36,21 +36,74 @@ export const Modal: React.FC = ({ open, gaData }) => { const { isReact, isBootstrap } = useBaseSpecificFramework(); const [openState, setOpen] = React.useState(open); + const focusOnModalInput = () => { + //source: https://stackoverflow.com/questions/4195616/how-to-set-the-focus-on-a-javascript-modal-window + const focusableElements = + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; + const modal = document.getElementsByClassName("uds-modal-container")[0]; + const firstFocusableElement = modal?.querySelectorAll(focusableElements)[0]; + const focusableContent = modal?.querySelectorAll(focusableElements); + const lastFocusableElement = focusableContent + ? focusableContent[focusableContent?.length - 1] + : undefined; + + if (lastFocusableElement && firstFocusableElement) { + document.addEventListener("keydown", function (e) { + let isTabPressed = e.key === "Tab" || e.keyCode === 9; + + if (!isTabPressed) { + return; + } + + if (e.shiftKey) { + // if shift key pressed for shift + tab combination + if (document.activeElement === firstFocusableElement) { + (lastFocusableElement as HTMLElement)?.focus(); // add focus for the last focusable element + e.preventDefault(); + } + } else { + // if tab key is pressed + if (document.activeElement === lastFocusableElement) { + // if focused has reached to last focusable element then focus first focusable element after pressing tab + (firstFocusableElement as HTMLElement)?.focus(); // add focus for the first focusable element + e.preventDefault(); + } + } + }); + (firstFocusableElement as HTMLElement)?.focus(); + } + }; + const handleOpen = () => { setOpen(true); + focusOnModalInput(); }; const handleClose = () => { setOpen(false); }; + useEffect(() => { + const handleKeyDown = (event: any) => { + if (event.key === "Escape") setOpen(false); // Close on Esc key + }; + + if (openState) { + document.addEventListener("keydown", handleKeyDown); + } + + return () => document.removeEventListener("keydown", handleKeyDown); + }, [openState, setOpen]); + + const modalTitle = "Content"; + return (