@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
358 lines (357 loc) • 12 kB
JavaScript
"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