UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

385 lines (384 loc) 13.1 kB
"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