UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

358 lines (357 loc) 12 kB
"use client"; import _extends from "@babel/runtime/helpers/esm/extends"; import React from 'react'; import classnames from 'classnames'; import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from "./bodyScrollLock.js"; import { warn, isTrue, makeUniqueId, InteractionInvalidation, combineLabelledBy, combineDescribedBy, dispatchCustomElementEvent, keycode } 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"; export default class ModalContent extends React.PureComponent { state = { color: null }; _mounted = 0; _lastFocusTime = 0; static contextType = Context; constructor(props) { super(props); this._contentRef = this.props.content_ref || React.createRef(); this._scrollRef = this.props.scroll_ref || React.createRef(); this._overlayClickRef = React.createRef(); if (this.props.modalContentCloseRef) { this.props.modalContentCloseRef.current = this.setModalContentState; } this._id = props.id; } componentDidUpdate(prevProps) { if (prevProps.children !== this.props.children) { this.setFocus(); } } componentDidMount() { const { id = null, no_animation = false, animation_duration = null } = this.props; const timeoutDuration = typeof animation_duration === 'string' ? parseFloat(animation_duration) : animation_duration; addToIndex(this); this.removeScrollPossibility(); this.setFocus(); this.setAndroidFocusHelper(); dispatchCustomElementEvent(this, 'on_open', { id }); if (isTrue(no_animation) || process.env.NODE_ENV === 'test') { this.lockBody(); } else { this._lockTimeout = setTimeout(this.lockBody, timeoutDuration * 1.2); } this._mounted = Date.now(); } componentWillUnmount() { clearTimeout(this._focusTimeout); clearTimeout(this._lockTimeout); this.removeLocks(); this._mounted = 0; } wasOpenedManually() { if (this._triggeredBy) { return true; } const { open_state } = this.props; if (typeof open_state === 'boolean' || typeof open_state === 'string') { if (process.env.NODE_ENV !== 'test') { const delay = Date.now() - this._mounted; return delay > 30; } return true; } return false; } lockBody = () => { const modalRoots = getListOfModalRoots(); const firstLevel = modalRoots[0]; if (firstLevel === this) { const contentElement = this._contentRef.current || document.querySelector(`#${this.props.content_id}`); const parentElements = getParents(contentElement); this._ii = new InteractionInvalidation(); this._ii.setBypassElements(parentElements); this._ii.setBypassSelector(['#eufemia-portal-root', '#eufemia-portal-root *', `#${this.props.content_id}`, `#${this.props.content_id} *`, '.dnb-modal--bypass_invalidation', '.dnb-modal--bypass_invalidation_deep *', ...(this.props?.bypass_invalidation_selectors || [])].filter(Boolean)); this._ii.activate(); } else { modalRoots.forEach(modal => { if (modal !== this && 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', this.onKeyDownHandler); } }; removeLocks() { const modalRoots = getListOfModalRoots(); const firstLevel = modalRoots[0]; removeFromIndex(this); if (firstLevel === this) { this._ii?.revert(); this.revertScrollPossibility(); } else { try { const modal = modalRoots[modalRoots.length - 2]; if (modal !== this && modal._iiLocal) { modal._iiLocal.revert(); delete modal._iiLocal; } } catch (e) { warn(e); } } this.removeAndroidFocusHelper(); if (this.wasOpenedManually()) { const id = this.props.id; dispatchCustomElementEvent(this, 'on_close', { id, event: this._triggeredByEvent, triggeredBy: this._triggeredBy || 'unmount' }); } if (typeof document !== 'undefined') { document.removeEventListener('keydown', this.onKeyDownHandler); } } setAndroidFocusHelper() { if (typeof window !== 'undefined' && isAndroid()) { window.addEventListener('resize', this._androidFocusHelper); } } removeAndroidFocusHelper() { window.removeEventListener('resize', this._androidFocusHelper); clearTimeout(this._androidFocusTimeout); } _androidFocusHelper = () => { const { animation_duration = null } = this.props; const timeoutDuration = typeof animation_duration === 'string' ? parseFloat(animation_duration) : animation_duration; clearTimeout(this._androidFocusTimeout); this._androidFocusTimeout = setTimeout(() => { try { if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') { document.activeElement.scrollIntoView(); } } catch (e) {} }, timeoutDuration / 2); }; setFocus() { const { focus_selector = null, no_animation = null, animation_duration = null } = this.props; const elem = this._contentRef.current; const timeoutDuration = typeof animation_duration === 'string' ? parseFloat(animation_duration) : animation_duration; if (elem) { if (this._lastFocusTime && Date.now() - this._lastFocusTime > 2000) { return; } this._lastFocusTime = Date.now(); clearTimeout(this._focusTimeout); this._focusTimeout = setTimeout(() => { try { let focusElement = elem; const headerElem = elem.querySelector('.dnb-drawer__header, .dnb-dialog__header'); const firstHeading = 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 focus_selector === 'string') { focusElement = elem.querySelector(focus_selector); } if (focusElement !== document.activeElement) { focusElement?.focus({ preventScroll: true }); } } catch (e) { warn(e); } }, isTrue(no_animation) ? 0 : timeoutDuration || 0); } } removeScrollPossibility() { if (this._scrollRef.current) { disableBodyScroll(this._scrollRef.current); } } revertScrollPossibility() { enableBodyScroll(this._scrollRef.current); clearAllBodyScrollLocks(); } preventClick = event => { if (event) { event.stopPropagation(); } }; onCloseClickHandler = event => { this.closeModalContent(event, { triggeredBy: 'button' }); }; onContentMouseDownHandler = event => { this._overlayClickRef.current = event.target === event.currentTarget ? event.target : null; }; onContentClickHandler = event => { if (this._overlayClickRef.current !== event.target) { return; } this._overlayClickRef.current = null; const { prevent_overlay_close } = this.props; if (!isTrue(prevent_overlay_close)) { this.closeModalContent(event, { triggeredBy: 'overlay', ifIsLatest: false }); } }; onKeyDownHandler = event => { switch (keycode(event)) { case 'escape': case 'esc': { const mostCurrent = getModalRoot(-1); if (mostCurrent === this) { event.preventDefault(); this.closeModalContent(event, { triggeredBy: 'keyboard' }); } break; } } }; setModalContentState = (event, { triggeredBy }) => { this._triggeredBy = triggeredBy; this._triggeredByEvent = event; }; closeModalContent(event, { triggeredBy, ...params }) { event?.persist?.(); this.props.close(event, { triggeredBy, ...params }); } setBackgroundColor = color => { this.setState({ color }); }; render() { const { hide, title, labelled_by, id: _id, close_title = 'Lukk', dialog_title = 'Vindu', hide_close_button = false, close_button_attributes, no_animation = false, no_animation_on_mobile = false, fullscreen = 'auto', container_placement = 'right', vertical_alignment = 'center', close, content_class, overlay_class, content_id, children, dialog_role = null, ...rest } = this.props; const { color } = this.state; const contentId = content_id || makeUniqueId('modal-'); const useDialogRole = !(IS_MAC || IS_SAFARI || IS_IOS); let role = dialog_role || 'dialog'; if (!useDialogRole && role === 'dialog') { role = 'region'; } const contentParams = { role, 'aria-modal': useDialogRole ? true : undefined, 'aria-labelledby': combineLabelledBy(this.props, title ? contentId + '-title' : null, labelled_by), 'aria-describedby': combineDescribedBy(this.props, contentId + '-content'), 'aria-label': !title && !labelled_by ? dialog_title : undefined, className: classnames(`dnb-modal__content dnb-modal__vertical-alignment--${vertical_alignment}`, isTrue(fullscreen) ? 'dnb-modal__content--fullscreen' : fullscreen === 'auto' && 'dnb-modal__content--auto-fullscreen', getThemeClasses(this.context?.theme), content_class, container_placement && `dnb-modal__content--${container_placement || 'right'}`), onMouseDown: this.onContentMouseDownHandler, onClick: this.onContentClickHandler }; const content = typeof children === 'function' ? children({ ...rest, close }) : children; return React.createElement(ModalContext.Provider, { value: { id: this.props.id, title, hide_close_button, close_button_attributes, close_title, hide, setBackgroundColor: this.setBackgroundColor, onCloseClickHandler: this.onCloseClickHandler, preventClick: this.preventClick, onKeyDownHandler: this.onKeyDownHandler, contentRef: this._contentRef, scrollRef: this._scrollRef, contentId, close } }, React.createElement("div", _extends({ id: contentId, style: color ? { '--modal-background-color': `var(--color-${color})` } : null }, contentParams), content), React.createElement("span", { className: classnames('dnb-modal__overlay', overlay_class, hide && 'dnb-modal__overlay--hide', isTrue(no_animation) && 'dnb-modal__overlay--no-animation', isTrue(no_animation_on_mobile) && '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) { parents.push(current); current = current.parentElement; } return parents; } //# sourceMappingURL=ModalContent.js.map