@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
385 lines (384 loc) • 13.1 kB
JavaScript
"use client";
import _pushInstanceProperty from "core-js-pure/stable/instance/push.js";
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import useMountEffect from "../../shared/helpers/useMountEffect.js";
import clsx from 'clsx';
import { SuffixContext } from "../../shared/helpers/Suffix.js";
import Context from "../../shared/Context.js";
import useId from "../../shared/helpers/useId.js";
import { warn, extendPropsWithContext, removeUndefinedProps, processChildren, dispatchCustomElementEvent } from "../../shared/component-helper.js";
import { applySpacing } from "../space/SpacingUtils.js";
import HelpButtonInstance from "../help-button/HelpButtonInstance.js";
import { getListOfModalRoots, getModalRoot } from "./helpers.js";
import ModalInner from "./parts/ModalInner.js";
import ModalHeader from "./parts/ModalHeader.js";
import ModalHeaderBar from "./parts/ModalHeaderBar.js";
import CloseButton from "./parts/CloseButton.js";
import ModalRoot from "./ModalRoot.js";
import { ParagraphContext } from "../../elements/typography/P.js";
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
export const ANIMATION_DURATION = 300;
const modalDefaultProps = {
spacing: true,
dialogTitle: 'Vindu',
closeTitle: 'Lukk',
hideCloseButton: false,
preventClose: false,
preventCoreStyle: false,
animationDuration: ANIMATION_DURATION,
noAnimation: false,
noAnimationOnMobile: false,
fullscreen: 'auto',
containerPlacement: null,
alignContent: 'left',
directDomReturn: false,
omitTriggerButton: false
};
function getContent(props) {
if (typeof props.modalContent === 'string') {
return props.modalContent;
} else if (typeof props.modalContent === 'function') {
return props.modalContent(props);
}
return processChildren(props);
}
function ModalComponent(ownProps) {
var _props$animationDurat, _props$noAnimation;
const context = useContext(Context);
const suffixContext = useContext(SuffixContext);
const visualTestsPropsOverride = typeof window !== 'undefined' && window['IS_TEST'] ? {
animationDuration: 0,
noAnimation: true
} : {};
const props = extendPropsWithContext({
...modalDefaultProps,
...removeUndefinedProps({
...ownProps
})
}, modalDefaultProps, context.getTranslation(ownProps).Modal, context.Modal, visualTestsPropsOverride);
const {
contentId = null,
disabled = null,
labelledBy = null,
focusSelector = null,
headerContent = null,
barContent = null,
bypassInvalidationSelectors = null,
verticalAlignment = 'center',
id: idProp,
openDelay,
omitTriggerButton = false,
trigger = null,
triggerAttributes = null,
ref: _ref,
...rest
} = props;
const {
open,
openModal,
closeModal,
preventClose = false
} = props;
const fallbackId = useId(idProp);
const _id = useRef(fallbackId);
const triggerRef = useRef(null);
const modalContentCloseRef = useRef(null);
const onUnmountRef = useRef([]);
const activeElementRef = useRef(null);
const isInTransitionRef = useRef(false);
const openTimeoutRef = useRef(null);
const closeTimeoutRef = useRef(null);
const pendingSideEffectsRef = useRef(null);
const [hide, setHide] = useState(false);
const [modalActive, setModalActive] = useState(false);
const [preventAutoFocus, setPreventAutoFocus] = useState(true);
const animationDuration = typeof window !== 'undefined' && window['IS_TEST'] ? 0 : (_props$animationDurat = props.animationDuration) !== null && _props$animationDurat !== void 0 ? _props$animationDurat : ANIMATION_DURATION;
const noAnimation = typeof window !== 'undefined' && window['IS_TEST'] ? true : (_props$noAnimation = props.noAnimation) !== null && _props$noAnimation !== void 0 ? _props$noAnimation : false;
const stateRef = useRef({
hide,
modalActive,
preventAutoFocus
});
stateRef.current = {
hide,
modalActive,
preventAutoFocus
};
const propsRef = useRef(props);
propsRef.current = props;
const removeActiveState = useCallback(() => {
const last = getModalRoot(-1);
if (last !== null && last !== void 0 && last._id && last._id !== _id.current) {
return setActiveState(last._id);
}
try {
document.documentElement.removeAttribute('data-dnb-modal-active');
} catch (e) {
warn('Modal: Error on remove "data-dnb-modal-active"', e);
}
}, []);
const setActiveState = useCallback(modalId => {
if (!modalId) {
warn('Modal: A valid modalId is required');
}
if (typeof document !== 'undefined') {
try {
document.documentElement.setAttribute('data-dnb-modal-active', modalId);
} catch (e) {
warn('Modal: Error on set "data-dnb-modal-active"', e);
}
}
}, []);
const handleSideEffects = useCallback((isModalActive, currentPreventAutoFocus) => {
if (isModalActive) {
if (typeof closeModal === 'function') {
const fn = closeModal(() => {
toggleOpenCloseRef.current(null, false);
}, {
_id: _id.current,
props: propsRef.current
});
if (fn) {
var _context;
_pushInstanceProperty(_context = onUnmountRef.current).call(_context, fn);
}
}
setActiveState(_id.current);
} else if (isModalActive === false && !currentPreventAutoFocus) {
const focus = elem => {
elem.setAttribute('data-autofocus', 'true');
elem.focus({
preventScroll: true
});
return new Promise(resolve => {
setTimeout(() => {
elem === null || elem === void 0 || elem.removeAttribute('data-autofocus');
resolve();
}, parseFloat(String(animationDuration)) / 3);
});
};
if (triggerRef !== null && triggerRef !== void 0 && triggerRef.current) {
focus(triggerRef.current);
}
if (open === true && activeElementRef.current instanceof HTMLElement) {
try {
focus(activeElementRef.current).then(() => {
activeElementRef.current = null;
});
} catch (e) {}
}
removeActiveState();
}
if (currentPreventAutoFocus) {
setPreventAutoFocus(false);
}
}, [closeModal, open, animationDuration, removeActiveState, setActiveState]);
const toggleOpenCloseRef = useRef(null);
const toggleOpenClose = useCallback((event = null, showModal = null) => {
if (event && 'preventDefault' in event) {
event.preventDefault();
}
const toggleNow = () => {
const timeoutDuration = typeof animationDuration === 'string' ? parseFloat(animationDuration) : animationDuration;
const newModalActive = typeof showModal === 'boolean' ? showModal : !stateRef.current.modalActive;
isInTransitionRef.current = true;
const doItNow = () => {
setHide(false);
setModalActive(newModalActive);
isInTransitionRef.current = false;
pendingSideEffectsRef.current = {
isModalActive: newModalActive,
preventAutoFocus: stateRef.current.preventAutoFocus
};
};
if (newModalActive === false && !noAnimation) {
setHide(true);
closeTimeoutRef.current = setTimeout(doItNow, timeoutDuration);
} else {
doItNow();
}
};
const waitBeforeOpen = () => {
const delay = typeof openDelay === 'string' ? parseFloat(openDelay) : openDelay;
if (delay > 0 && !noAnimation) {
openTimeoutRef.current = setTimeout(toggleNow, delay);
} else {
toggleNow();
}
};
clearTimeout(closeTimeoutRef.current);
clearTimeout(openTimeoutRef.current);
if (typeof openModal === 'function') {
const fn = openModal(waitBeforeOpen, {
_id: _id.current,
props: propsRef.current
});
if (fn) {
var _context2;
_pushInstanceProperty(_context2 = onUnmountRef.current).call(_context2, fn);
}
} else {
waitBeforeOpen();
}
}, [animationDuration, noAnimation, openDelay, openModal, handleSideEffects]);
toggleOpenCloseRef.current = toggleOpenClose;
const closeHandler = useCallback((event, {
ifIsLatest,
triggeredBy = 'handler'
} = {
ifIsLatest: true
}) => {
var _modalContentCloseRef;
(_modalContentCloseRef = modalContentCloseRef.current) === null || _modalContentCloseRef === void 0 || _modalContentCloseRef.call(modalContentCloseRef, event, {
triggeredBy
});
if (preventClose) {
const id = _id.current;
dispatchCustomElementEvent(propsRef.current, 'onClosePrevent', {
id,
event,
triggeredBy,
close: e => {
toggleOpenCloseRef.current(e, false);
}
});
} else {
if (ifIsLatest) {
const list = getListOfModalRoots();
if (list.length > 1) {
const last = getModalRoot(-1);
if (last !== modalStackIdentityRef.current) {
return;
}
}
}
toggleOpenCloseRef.current(event, false);
}
}, [preventClose]);
const modalStackIdentityRef = useRef({
_id: _id.current
});
const prevOpenRef = useRef(undefined);
if (open !== prevOpenRef.current) {
if (open === true) {
setHide(false);
if (noAnimation) {
setModalActive(true);
}
} else if (open === false) {
setHide(true);
if (noAnimation) {
setModalActive(false);
}
}
prevOpenRef.current = open;
}
const prevEffectOpenRef = useRef(undefined);
const prevOwnPropsRef = useRef(undefined);
useEffect(() => {
if (!activeElementRef.current && typeof document !== 'undefined') {
activeElementRef.current = document.activeElement;
}
const isNewProps = prevOwnPropsRef.current !== undefined && prevOwnPropsRef.current !== ownProps;
prevOwnPropsRef.current = ownProps;
const openChanged = prevEffectOpenRef.current !== open;
prevEffectOpenRef.current = open;
if (!openChanged && !isNewProps) {
return;
}
if (isInTransitionRef.current) {
return;
}
if (!hide && open === true) {
toggleOpenClose(null, true);
} else if (hide && open === false) {
toggleOpenClose(null, false);
}
});
useEffect(() => {
if (pendingSideEffectsRef.current) {
const {
isModalActive,
preventAutoFocus
} = pendingSideEffectsRef.current;
pendingSideEffectsRef.current = null;
handleSideEffects(isModalActive, preventAutoFocus);
}
});
useMountEffect(() => {
return () => {
clearTimeout(openTimeoutRef.current);
clearTimeout(closeTimeoutRef.current);
removeActiveState();
onUnmountRef.current.forEach(fn => {
if (typeof fn === 'function') {
fn();
}
});
};
});
const modalContent = getContent(typeof ownProps.children === 'function' ? Object.freeze({
...ownProps,
close: closeHandler
}) : ownProps);
const usedTriggerAttributes = {
hidden: false,
variant: 'secondary',
iconPosition: 'left',
...triggerAttributes
};
if (disabled) {
usedTriggerAttributes.disabled = true;
}
if (usedTriggerAttributes.id) {
_id.current = usedTriggerAttributes.id;
}
let fallbackTitle;
if (usedTriggerAttributes.title) {
fallbackTitle = usedTriggerAttributes.title;
} else if (suffixContext) {
fallbackTitle = context.translation.HelpButton.title;
}
const headerTitle = rest.title || fallbackTitle;
const title = !(usedTriggerAttributes !== null && usedTriggerAttributes !== void 0 && usedTriggerAttributes.text) && headerTitle ? headerTitle || fallbackTitle : null;
const TriggerButton = trigger ? trigger : HelpButtonInstance;
return _jsxs(_Fragment, {
children: [TriggerButton && !omitTriggerButton && _jsx(TriggerButton, {
...usedTriggerAttributes,
...applySpacing(rest, {
id: _id.current,
title,
onClick: event => toggleOpenClose(event.nativeEvent),
ref: triggerRef,
className: clsx('dnb-modal__trigger', usedTriggerAttributes.className)
})
}), modalActive && modalContent && _jsx(ParagraphContext, {
value: {
isNested: false
},
children: _jsx(ModalRoot, {
...rest,
id: _id.current,
contentId: contentId || `dnb-modal-${_id.current}`,
labelledBy: labelledBy,
focusSelector: focusSelector,
modalContent: modalContent,
headerContent: headerContent,
verticalAlignment: verticalAlignment,
barContent: barContent,
bypassInvalidationSelectors: bypassInvalidationSelectors,
close: closeHandler,
hide: hide,
title: headerTitle,
modalContentCloseRef: modalContentCloseRef
})
})]
});
}
const Modal = React.memo(ModalComponent);
Modal.Bar = ModalHeaderBar;
Modal.Header = ModalHeader;
Modal.Content = ModalInner;
export { CloseButton, Modal as OriginalComponent };
export default Modal;
//# sourceMappingURL=Modal.js.map