@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
363 lines (362 loc) • 13.8 kB
JavaScript
"use client";
import _pushInstanceProperty from "core-js-pure/stable/instance/push.js";
import React, { useCallback, useContext, useEffect, useRef } from 'react';
import useMountEffect from "../../shared/helpers/useMountEffect.js";
import clsx from 'clsx';
import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from "./bodyScrollLock.js";
import useId from "../../shared/helpers/useId.js";
import { warn, InteractionInvalidation, combineLabelledBy, combineDescribedBy, dispatchCustomElementEvent } from "../../shared/component-helper.js";
import ModalContext from "./ModalContext.js";
import { IS_IOS, IS_SAFARI, IS_MAC, isAndroid } from "../../shared/helpers.js";
import { getListOfModalRoots, getModalRoot, addToIndex, removeFromIndex } from "./helpers.js";
import { getThemeClasses } from "../../shared/Theme.js";
import { Context } from "../../shared/index.js";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export default function ModalContent(props) {
const {
hide,
title,
labelledBy,
id: idProp,
closeTitle = 'Lukk',
dialogTitle = 'Vindu',
hideCloseButton = false,
closeButtonAttributes,
noAnimation = false,
noAnimationOnMobile = false,
fullscreen = 'auto',
containerPlacement = 'right',
verticalAlignment = 'center',
close,
contentClass,
overlayClass,
contentId: contentIdProp,
children,
dialogRole = null,
focusSelector = null,
animationDuration = null,
preventOverlayClose,
open,
contentRef: contentRefProp,
scrollRef: scrollRefProp,
modalContentCloseRef,
bypassInvalidationSelectors,
...rest
} = props;
const context = useContext(Context);
const internalContentRef = useRef(null);
const contentRef = contentRefProp || internalContentRef;
const internalScrollRef = useRef(null);
const scrollRef = scrollRefProp || internalScrollRef;
const overlayClickRef = useRef(null);
const lockTimeoutRef = useRef(null);
const focusTimeoutRef = useRef(null);
const androidFocusTimeoutRef = useRef(null);
const iiRef = useRef(null);
const mountedRef = useRef(0);
const lastFocusTimeRef = useRef(0);
const triggeredByRef = useRef(undefined);
const triggeredByEventRef = useRef(undefined);
const selfRef = useRef(null);
if (!selfRef.current) {
selfRef.current = {
_id: idProp,
_scrollRef: scrollRef,
_contentRef: contentRef,
_iiLocal: undefined
};
}
selfRef.current._id = idProp;
selfRef.current._scrollRef = scrollRef;
selfRef.current._contentRef = contentRef;
const setModalContentState = useCallback((event, {
triggeredBy
}) => {
triggeredByRef.current = triggeredBy;
triggeredByEventRef.current = event;
}, []);
useEffect(() => {
if (modalContentCloseRef) {
const mutableRef = modalContentCloseRef;
mutableRef.current = setModalContentState;
}
}, [modalContentCloseRef, setModalContentState]);
const usedContentId = useId(contentIdProp);
const wasOpenedManually = useCallback(() => {
if (triggeredByRef.current) {
return true;
}
if (typeof open === 'boolean') {
if (process.env.NODE_ENV !== 'test') {
const delay = Date.now() - mountedRef.current;
return delay > 30;
}
return true;
}
return false;
}, [open]);
const removeScrollPossibility = useCallback(() => {
if (scrollRef.current) {
disableBodyScroll(scrollRef.current);
}
}, [scrollRef]);
const revertScrollPossibility = useCallback(() => {
enableBodyScroll(scrollRef.current);
clearAllBodyScrollLocks();
}, [scrollRef]);
const setFocus = useCallback(() => {
const elem = contentRef.current;
const timeoutDuration = typeof animationDuration === 'string' ? parseFloat(animationDuration) : animationDuration;
if (elem) {
if (lastFocusTimeRef.current && Date.now() - lastFocusTimeRef.current > 2000) {
return;
}
lastFocusTimeRef.current = Date.now();
clearTimeout(focusTimeoutRef.current);
focusTimeoutRef.current = setTimeout(() => {
try {
let focusElement = elem;
const headerElem = elem.querySelector('.dnb-drawer__header, .dnb-dialog__header');
const firstHeading = (headerElem === null || headerElem === void 0 ? void 0 : headerElem.querySelector('h1, h2, h3')) || elem.querySelector('h1, h2, h3');
if (firstHeading) {
if (firstHeading.tagName !== 'H1') {
warn('A Dialog or Drawer needs a h1 as its first element!');
}
firstHeading.setAttribute('tabIndex', '-1');
firstHeading.classList.add('dnb-no-focus');
focusElement = firstHeading;
} else {
const focusHelper = elem.querySelector('.dnb-modal__close-button, .dnb-modal__focus-helper');
focusElement = focusHelper;
}
if (typeof focusSelector === 'string') {
focusElement = elem.querySelector(focusSelector);
}
if (focusElement !== document.activeElement) {
var _focusElement;
(_focusElement = focusElement) === null || _focusElement === void 0 || _focusElement.focus({
preventScroll: true
});
}
} catch (e) {
warn(e);
}
}, noAnimation ? 0 : timeoutDuration || 0);
}
}, [contentRef, animationDuration, focusSelector, noAnimation]);
const androidFocusHelperRef = useRef(null);
androidFocusHelperRef.current = () => {
const timeoutDuration = typeof animationDuration === 'string' ? parseFloat(animationDuration) : animationDuration;
clearTimeout(androidFocusTimeoutRef.current);
androidFocusTimeoutRef.current = setTimeout(() => {
try {
const elem = contentRef.current;
if ((elem === null || elem === void 0 ? void 0 : elem.tagName) === 'INPUT' || (elem === null || elem === void 0 ? void 0 : elem.tagName) === 'TEXTAREA') {
elem.scrollIntoView();
}
} catch (e) {}
}, timeoutDuration / 2);
};
const stableAndroidFocusHelper = useCallback(() => {
var _androidFocusHelperRe;
(_androidFocusHelperRe = androidFocusHelperRef.current) === null || _androidFocusHelperRe === void 0 || _androidFocusHelperRe.call(androidFocusHelperRef);
}, []);
const closeModalContent = useCallback((event, {
triggeredBy,
...params
}) => {
close(event, {
triggeredBy,
...params
});
}, [close]);
const onCloseClickHandler = useCallback(event => {
closeModalContent(event, {
triggeredBy: 'button'
});
}, [closeModalContent]);
const onContentMouseDownHandler = useCallback(event => {
overlayClickRef.current = event.target === event.currentTarget ? event.target : null;
}, []);
const onContentClickHandler = useCallback(event => {
if (overlayClickRef.current !== event.target) {
return;
}
overlayClickRef.current = null;
if (!preventOverlayClose) {
closeModalContent(event, {
triggeredBy: 'overlay',
ifIsLatest: false
});
}
}, [preventOverlayClose, closeModalContent]);
const onKeyDownHandlerRef = useRef(null);
onKeyDownHandlerRef.current = event => {
if (event.key === 'Escape') {
const mostCurrent = getModalRoot(-1);
if (mostCurrent === selfRef.current) {
event.preventDefault();
closeModalContent(event, {
triggeredBy: 'keyboard'
});
}
}
};
const stableOnKeyDownHandler = useCallback(event => {
var _onKeyDownHandlerRef$;
(_onKeyDownHandlerRef$ = onKeyDownHandlerRef.current) === null || _onKeyDownHandlerRef$ === void 0 || _onKeyDownHandlerRef$.call(onKeyDownHandlerRef, event);
}, []);
const preventClick = useCallback(event => {
if (event) {
event.stopPropagation();
}
}, []);
const lockBody = useCallback(() => {
const modalRoots = getListOfModalRoots();
const firstLevel = modalRoots[0];
if (firstLevel === selfRef.current) {
const contentElement = contentRef.current || document.querySelector(`#${usedContentId}`);
const parentElements = getParents(contentElement);
const ii = new InteractionInvalidation();
ii.setBypassElements(parentElements);
ii.setBypassSelector(['#eufemia-portal-root', '#eufemia-portal-root *', `#${usedContentId}`, `#${usedContentId} *`, '.dnb-modal--bypass-invalidation', '.dnb-modal--bypass-invalidation-deep *', ...(bypassInvalidationSelectors || [])].filter(Boolean));
ii.activate();
iiRef.current = ii;
} else {
modalRoots.forEach(modal => {
if (modal !== selfRef.current && typeof modal._iiLocal === 'undefined' && typeof modal._scrollRef !== 'undefined') {
modal._iiLocal = new InteractionInvalidation();
modal._iiLocal.activate(modal._scrollRef.current);
}
});
}
if (typeof document !== 'undefined') {
document.addEventListener('keydown', stableOnKeyDownHandler);
}
}, [contentRef, usedContentId, bypassInvalidationSelectors, stableOnKeyDownHandler]);
const removeLocks = useCallback(() => {
const modalRoots = getListOfModalRoots();
const firstLevel = modalRoots[0];
removeFromIndex(selfRef.current);
if (firstLevel === selfRef.current) {
var _iiRef$current;
(_iiRef$current = iiRef.current) === null || _iiRef$current === void 0 || _iiRef$current.revert();
revertScrollPossibility();
} else {
try {
const modal = modalRoots[modalRoots.length - 2];
if (modal !== selfRef.current && modal._iiLocal) {
modal._iiLocal.revert();
delete modal._iiLocal;
}
} catch (e) {
warn(e);
}
}
window.removeEventListener('resize', stableAndroidFocusHelper);
clearTimeout(androidFocusTimeoutRef.current);
if (wasOpenedManually()) {
dispatchCustomElementEvent(props, 'onClose', {
id: idProp,
event: triggeredByEventRef.current,
triggeredBy: triggeredByRef.current || 'unmount'
});
}
if (typeof document !== 'undefined') {
document.removeEventListener('keydown', stableOnKeyDownHandler);
}
}, [revertScrollPossibility, stableAndroidFocusHelper, wasOpenedManually, props, idProp, stableOnKeyDownHandler]);
const removeLocksRef = useRef(removeLocks);
removeLocksRef.current = removeLocks;
useMountEffect(() => {
const timeoutDuration = typeof animationDuration === 'string' ? parseFloat(animationDuration) : animationDuration;
addToIndex(selfRef.current);
removeScrollPossibility();
setFocus();
if (typeof window !== 'undefined' && isAndroid()) {
window.addEventListener('resize', stableAndroidFocusHelper);
}
dispatchCustomElementEvent(props, 'onOpen', {
id: idProp
});
if (noAnimation || process.env.NODE_ENV === 'test') {
lockBody();
} else {
lockTimeoutRef.current = setTimeout(lockBody, timeoutDuration * 1.2);
}
mountedRef.current = Date.now();
return () => {
clearTimeout(focusTimeoutRef.current);
clearTimeout(lockTimeoutRef.current);
removeLocksRef.current();
mountedRef.current = 0;
};
});
const prevChildrenRef = useRef(children);
useEffect(() => {
if (prevChildrenRef.current !== children) {
prevChildrenRef.current = children;
setFocus();
}
}, [children, setFocus]);
const useDialogRole = !(IS_MAC || IS_SAFARI || IS_IOS);
let role = dialogRole || 'dialog';
if (!useDialogRole && role === 'dialog') {
role = 'region';
}
const contentParams = {
role,
'aria-modal': useDialogRole ? true : undefined,
'aria-labelledby': combineLabelledBy(props, title ? usedContentId + '-title' : null, labelledBy),
'aria-describedby': combineDescribedBy(props, usedContentId + '-content'),
'aria-label': !title && !labelledBy ? dialogTitle : undefined,
className: clsx(`dnb-modal__content dnb-modal__vertical-alignment--${verticalAlignment}`, fullscreen === true ? 'dnb-modal__content--fullscreen' : fullscreen === 'auto' && 'dnb-modal__content--auto-fullscreen', getThemeClasses(context === null || context === void 0 ? void 0 : context.theme), contentClass, containerPlacement && `dnb-modal__content--${containerPlacement || 'right'}`),
onMouseDown: onContentMouseDownHandler,
onClick: onContentClickHandler
};
const content = typeof children === 'function' ? children({
...rest,
close
}) : children;
const {
colorScheme
} = (context === null || context === void 0 ? void 0 : context.theme) || {};
return _jsxs(ModalContext, {
value: {
id: idProp,
title,
hideCloseButton,
closeButtonAttributes,
closeTitle,
hide,
onCloseClickHandler,
preventClick,
onKeyDownHandler: stableOnKeyDownHandler,
contentRef,
scrollRef,
contentId: usedContentId,
close
},
children: [_jsx("div", {
id: usedContentId,
...contentParams,
children: content
}), _jsx("span", {
className: clsx('dnb-modal__overlay', overlayClass, colorScheme && `dnb-modal__color-scheme--${colorScheme}`, hide && 'dnb-modal__overlay--hide', noAnimation && 'dnb-modal__overlay--no-animation', noAnimationOnMobile && 'dnb-modal__overlay--no-animation-on-mobile'),
"aria-hidden": true
})]
});
}
function getParents(elem) {
if (!elem || typeof document === 'undefined') {
return [];
}
const parents = [];
let current = elem.parentElement;
while (current && current !== document.body) {
_pushInstanceProperty(parents).call(parents, current);
current = current.parentElement;
}
return parents;
}
//# sourceMappingURL=ModalContent.js.map