diff --git a/packages/unity-bootstrap-theme/src/js/modals.js b/packages/unity-bootstrap-theme/src/js/modals.js
index 622eb6d204..a6048ff60a 100644
--- a/packages/unity-bootstrap-theme/src/js/modals.js
+++ b/packages/unity-bootstrap-theme/src/js/modals.js
@@ -1,23 +1,61 @@
import { EventHandler } from "./bootstrap-helper";
function initModals() {
- document
- .getElementById("openModalButton")
- ?.addEventListener("click", function () {
- document.getElementById("uds-modal")?.classList.add("open");
- document.getElementById("closeModalButton")?.focus();
- });
+ const modal = document.getElementById("uds-modal");
+ if (!modal) return null;
+ const modalBackdrop = document.getElementById("uds-modal-backdrop");
+ if (!modalBackdrop) return null;
+ const openModalButton = document.getElementById("openModalButton");
+ if (!openModalButton) return null;
+ const closeModalButton = document.getElementById("closeModalButton");
+ if (!closeModalButton) return null;
+ const firstFocusable = modal.querySelector(
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+ );
- document
- .getElementById("closeModalButton")
- ?.addEventListener("click", function () {
- document.getElementById("uds-modal").classList.remove("open");
- });
+ let previousFocus = null;
+ function focusTrap(e) {
+ // If relatedTarget is outside, move focus back inside
+ if (!modal.contains(e.relatedTarget)) {
+ firstFocusable?.focus();
+ }
+ }
- document?.addEventListener("keydown", function (event) {
- event.key === "Escape" &&
- document.getElementById("uds-modal")?.classList.remove("open");
- });
+ function openModal() {
+ // When opening: save current focus and move into modal
+ previousFocus = document.activeElement;
+ modal.classList.add("open");
+ modalBackdrop.classList.add("open");
+ // attach event listeners to trap focus and close modal
+ modalBackdrop.addEventListener("focusout", focusTrap);
+ modalBackdrop.addEventListener("click", closeModal, true);
+ document.addEventListener("keydown", closeModal);
+
+ // Focus the first interactive element inside (or body if none)
+ setTimeout(() => {
+ firstFocusable?.focus();
+ }, 200);
+ }
+
+ function closeModal({ type, target, key } = {}) {
+ if (
+ // escape key pressed
+ (type === "keydown" && key === "Escape") ||
+ // click on close button
+ (type === "click" && target === closeModalButton) ||
+ // click on backdrop
+ (type === "click" && target === modalBackdrop)
+ ) {
+ modal.classList.remove("open");
+ modalBackdrop.classList.remove("open");
+ modalBackdrop.removeEventListener("focusout", focusTrap);
+ modalBackdrop.removeEventListener("click", closeModal, true);
+ document.removeEventListener("keydown", closeModal);
+ // When closing: restore original focus
+ previousFocus?.focus();
+ }
+ }
+ openModalButton.addEventListener("click", openModal);
}
EventHandler.on(window, "load.uds.modals", initModals);
diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss b/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss
index 6bcace67f7..d949ca3b6a 100644
--- a/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss
+++ b/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss
@@ -7,7 +7,7 @@
left: 0;
opacity: 0;
padding: 4rem 2rem;
- position: absolute;
+ position: fixed;
right: 0;
top: 0;
z-index: 1030;
@@ -84,3 +84,12 @@
}
}
}
+
+.uds-modal-main {
+ background-color: #0000;
+ pointer-events: none;
+}
+
+.uds-modal-container {
+ pointer-events: all;
+}
diff --git a/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx b/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx
index 86ed0abd8e..6a2a904eae 100644
--- a/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx
+++ b/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx
@@ -23,6 +23,7 @@ const gaDefaultObject = {
*/
export const ButtonIconOnly = ({
color = "gray",
+ autoFocus = undefined,
icon = undefined,
innerRef = undefined,
onClick = undefined,
@@ -46,6 +47,7 @@ export const ButtonIconOnly = ({
}}
>
+ // ),
+ openModalButtonClassName: "btn-dark",
+ openModalButtonText: "Show modal",
+ children: (
+ <>
+
Content
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
+ eiusmod incididuntåç ut labore et dolore magna aliqua eiusmod tempo.
+
+
+ >
+ ),
},
};
+//@ts-ignore
const modalTemplate = args => ;
export const Overview = {
diff --git a/packages/unity-react-core/src/components/Modal/Modal.tsx b/packages/unity-react-core/src/components/Modal/Modal.tsx
index 8c945d703d..3da6a77d1f 100644
--- a/packages/unity-react-core/src/components/Modal/Modal.tsx
+++ b/packages/unity-react-core/src/components/Modal/Modal.tsx
@@ -4,10 +4,6 @@ import { ButtonIconOnly } from "../ButtonIconOnly/ButtonIconOnly";
import { GaEventWrapper } from "../GaEventWrapper/GaEventWrapper";
import { useBaseSpecificFramework } from "../GaEventWrapper/useBaseSpecificFramework";
import classNames from "classnames";
-/**
- *
- * TODO: Should we be using bootstrap's built in modal functionality?
- */
const defaultGaData = {
name: "onclick",
@@ -20,7 +16,14 @@ const defaultGaData = {
};
export interface ModalProps {
+ /**
+ * Modal open/closed state
+ */
open?: boolean;
+ /**
+ * React useState custom setter
+ */
+ setOpen?: React.Dispatch> | undefined;
gaData?: {
name: string;
event: string;
@@ -30,36 +33,85 @@ export interface ModalProps {
section: string;
ga: string;
};
+ /**
+ * Custom JSX to replace the default open modal button
+ */
+ openModalInput?: JSX.Element;
+ /**
+ * Style class for the default open modal button
+ */
+ openModalButtonClassName?: string;
+ /**
+ * Display text for the default open modal button
+ */
+ openModalButtonText?: string;
+ /**
+ * JSX for the content displayed within the modal
+ */
+ children?: JSX.Element;
}
-export const Modal: React.FC = ({ open, gaData }) => {
+export const Modal: React.FC = ({
+ children,
+ open,
+ setOpen,
+ openModalInput,
+ openModalButtonClassName,
+ openModalButtonText,
+ gaData,
+}) => {
const { isReact, isBootstrap } = useBaseSpecificFramework();
- const [openState, setOpen] = React.useState(open);
+ const [defaultOpenState, defaultSetOpen] = React.useState(open ?? false);
+
+ const handleSetOpen = (e: boolean) => {
+ if (setOpen) {
+ setOpen(e); // custom set open prop
+ } else {
+ defaultSetOpen(e); // default set open function
+ }
+ };
+
+ const getOpenState = () => {
+ if (setOpen) {
+ return open; // custom open state value
+ } else {
+ return defaultOpenState; // default open state value
+ }
+ };
const handleOpen = () => {
- setOpen(true);
+ handleSetOpen(true);
};
const handleClose = () => {
- setOpen(false);
+ handleSetOpen(false);
};
useEffect(() => {
const handleKeyDown = (event: any) => {
- if (event.key === "Escape") setOpen(false); // Close on Esc key
+ if (event.key === "Escape") handleSetOpen(false); // Close on Esc key
};
- if (openState) {
+ if (getOpenState()) {
document.addEventListener("keydown", handleKeyDown);
}
return () => document.removeEventListener("keydown", handleKeyDown);
- }, [openState, setOpen]);
+ }, [getOpenState(), handleSetOpen]);
useEffect(() => {
- if (!openState) return;
-
- //source: https://stackoverflow.com/questions/4195616/how-to-set-the-focus-on-a-javascript-modal-window
+ if (!getOpenState()) {
+ let openModalButton = document.getElementById("openModalButtonR");
+ setTimeout(() => {
+ if (openModalButton) {
+ // Wait for dom to update before setting focus
+ openModalButton?.focus();
+ }
+ }, 200);
+ return;
+ }
+ // Disable navigation to everything accept for the modal content
+ // 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];
@@ -94,57 +146,135 @@ export const Modal: React.FC = ({ open, gaData }) => {
if (lastFocusableElement && firstFocusableElement) {
document.addEventListener("keydown", handleTabKey);
- (firstFocusableElement as HTMLElement)?.focus();
+ setTimeout(() => {
+ if (firstFocusableElement) {
+ // Wait for dom to update before setting focus
+ (firstFocusableElement as HTMLElement)?.focus();
+ }
+ }, 200);
return () => document.removeEventListener("keydown", handleTabKey);
}
- }, [openState]);
-
- const modalTitle = "Content";
-
- return (
-
-
-
- {(openState || isBootstrap) && (
+ }, [getOpenState()]);
+
+ let modalHeaderText = "Modal"; // default aria-label value
+
+ if (children && children.props && children.props.children) {
+ for (let i = 0; i < children.props.children.length; i++) {
+ if (children.props.children[i].type === "h1") {
+ if (
+ children.props.children[i].props &&
+ children.props.children[i].props.children &&
+ typeof children.props.children[i].props.children === "string"
+ ) {
+ modalHeaderText = children.props.children[i].props.children;
+ }
+ }
+ }
+ }
+
+ if (isBootstrap) {
+ return (
+
+ {/* Disable main content on modal open */}
+
+ {openModalInput ? (
+ openModalInput
+ ) : (
+
+ )}
+
+
-
{modalTitle}
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
- eiusmod incididuntåç ut labore et dolore magna aliqua eiusmod
- tempo.
-
-
+ {children}
- )}
-
- );
+
+
+ );
+ } else {
+ return (
+
+ {/* Disable main content on modal open */}
+
+ {openModalInput ? (
+ openModalInput
+ ) : (
+
+ )}
+
+
+ {getOpenState() && (
+ <>
+
+
+ >
+ )}
+
+ );
+ }
};
diff --git a/packages/unity-react-core/src/core/types/shared-types.js b/packages/unity-react-core/src/core/types/shared-types.js
index 8a31446f6a..b9c02e27e4 100644
--- a/packages/unity-react-core/src/core/types/shared-types.js
+++ b/packages/unity-react-core/src/core/types/shared-types.js
@@ -24,6 +24,7 @@
* @typedef {Object} ButtonIconOnlyProps
* @property {Array.} icon
* @property {string} [color]
+ * @property {boolean} [autoFocus]
* @property {React.RefObject} [innerRef]
* @property {function():void} [onClick]
* @property {"large"|"small"} [size]