@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
411 lines (410 loc) • 12.5 kB
JavaScript
"use client";
import _extends from "@babel/runtime/helpers/esm/extends";
import React from 'react';
import classnames from 'classnames';
import { SuffixContext } from "../../shared/helpers/Suffix.js";
import Context from "../../shared/Context.js";
import { warn, isTrue, makeUniqueId, extendPropsWithContextInClassComponent, processChildren, dispatchCustomElementEvent } from "../../shared/component-helper.js";
import { createSpacingClasses } from "../space/SpacingHelper.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 { classWithCamelCaseProps } from "../../shared/helpers/withCamelCaseProps.js";
export const ANIMATION_DURATION = 300;
class Modal extends React.PureComponent {
static contextType = Context;
static Bar = ModalHeaderBar;
static Header = ModalHeader;
static Content = ModalInner;
static getContent(props) {
if (typeof props.modal_content === 'string') {
return props.modal_content;
} else if (typeof props.modal_content === 'function') {
return props.modal_content(props);
}
return processChildren(props);
}
state = {
hide: false,
modalActive: false,
preventAutoFocus: true,
animation_duration: ANIMATION_DURATION,
no_animation: false
};
static defaultProps = {
id: null,
focus_selector: null,
labelled_by: null,
title: null,
disabled: null,
spacing: true,
open_delay: null,
content_id: null,
dialog_title: 'Vindu',
close_title: 'Lukk',
hide_close_button: false,
close_button_attributes: null,
prevent_close: false,
prevent_core_style: false,
animation_duration: ANIMATION_DURATION,
no_animation: false,
no_animation_on_mobile: false,
fullscreen: 'auto',
min_width: null,
max_width: null,
align_content: 'left',
container_placement: null,
vertical_alignment: null,
open_state: null,
direct_dom_return: false,
root_id: 'root',
omit_trigger_button: false,
className: null,
children: null,
on_open: null,
on_close: null,
on_close_prevent: null,
open_modal: null,
close_modal: null,
trigger: null,
trigger_attributes: null,
overlay_class: null,
content_class: null,
modal_content: null,
header_content: null,
bar_content: null
};
static getDerivedStateFromProps(props, state) {
if (typeof window !== 'undefined' && window['IS_TEST']) {
state.animation_duration = 0;
state.no_animation = true;
} else {
state.animation_duration = props.animation_duration;
state.no_animation = props.no_animation;
}
if (props.open_state !== state._open_state) {
switch (props.open_state) {
case 'opened':
case true:
state.hide = false;
if (isTrue(state.no_animation)) {
state.modalActive = true;
}
break;
case 'closed':
case false:
state.hide = true;
if (isTrue(state.no_animation)) {
state.modalActive = false;
}
break;
}
}
state._open_state = props.open_state;
return state;
}
constructor(props) {
super(props);
this._id = props.id || makeUniqueId('modal-');
this._triggerRef = React.createRef();
this.modalContentCloseRef = React.createRef();
this._onUnmount = [];
}
componentDidMount() {
this.openBasedOnStateUpdate();
}
componentWillUnmount() {
clearTimeout(this._openTimeout);
clearTimeout(this._closeTimeout);
this.removeActiveState();
this._onUnmount.forEach(fn => {
if (typeof fn === 'function') {
fn();
}
});
}
componentDidUpdate(prevProps) {
if (this.isInTransition) {
return;
}
if (prevProps !== this.props) {
this.openBasedOnStateUpdate();
}
}
openBasedOnStateUpdate() {
const {
hide
} = this.state;
const {
open_state
} = this.props;
if (!this.activeElement && typeof document !== 'undefined') {
this.activeElement = document.activeElement;
}
if (!hide && (open_state === 'opened' || open_state === true)) {
this.toggleOpenClose(null, true);
} else if (hide && (open_state === 'closed' || open_state === false)) {
this.toggleOpenClose(null, false);
}
}
toggleOpenClose = (event = null, showModal = null) => {
if (event && event.preventDefault) {
event.preventDefault();
}
const toggleNow = () => {
const {
animation_duration = ANIMATION_DURATION,
no_animation = false
} = this.state;
const timeoutDuration = typeof animation_duration === 'string' ? parseFloat(animation_duration) : animation_duration;
const modalActive = typeof showModal === 'boolean' ? showModal : !this.state.modalActive;
this.isInTransition = true;
const doItNow = () => {
this.setState({
hide: false,
modalActive
}, () => {
this.isInTransition = false;
this.handleSideEffects();
});
};
if (modalActive === false && !isTrue(no_animation)) {
this.setState({
hide: true
});
this._closeTimeout = setTimeout(doItNow, timeoutDuration);
} else {
doItNow();
}
};
const waitBeforeOpen = () => {
const {
open_delay
} = this.props;
const {
no_animation
} = this.state;
const delay = typeof open_delay === 'string' ? parseFloat(open_delay) : open_delay;
if (delay > 0 && !isTrue(no_animation)) {
this._openTimeout = setTimeout(toggleNow, delay);
} else {
toggleNow();
}
};
clearTimeout(this._closeTimeout);
clearTimeout(this._openTimeout);
const {
open_modal
} = this.props;
if (typeof open_modal === 'function') {
const fn = open_modal(waitBeforeOpen, this);
if (fn) {
this._onUnmount.push(fn);
}
} else {
waitBeforeOpen();
}
};
handleSideEffects = () => {
const {
modalActive,
preventAutoFocus,
animation_duration
} = this.state;
const {
close_modal,
open_state
} = this.props;
if (modalActive) {
if (typeof close_modal === 'function') {
const fn = close_modal(() => {
this.toggleOpenClose(null, false);
}, this);
if (fn) {
this._onUnmount.push(fn);
}
}
this.setActiveState(this._id);
} else if (modalActive === false && !preventAutoFocus) {
const focus = elem => {
elem.setAttribute('data-autofocus', 'true');
elem.focus({
preventScroll: true
});
return new Promise(resolve => {
setTimeout(() => {
elem?.removeAttribute('data-autofocus');
resolve();
}, parseFloat(String(animation_duration)) / 3);
});
};
if (this._triggerRef?.current) {
focus(this._triggerRef.current);
}
if ((open_state === 'opened' || open_state === true) && this.activeElement instanceof HTMLElement) {
try {
focus(this.activeElement).then(() => {
this.activeElement = null;
});
} catch (e) {}
}
this.removeActiveState();
}
if (preventAutoFocus) {
this.setState({
preventAutoFocus: false
});
}
};
open = e => {
this.toggleOpenClose(e, true);
};
close = (event, {
ifIsLatest,
triggeredBy = 'handler'
} = {
ifIsLatest: true
}) => {
this.modalContentCloseRef.current?.(event, {
triggeredBy
});
const {
prevent_close = false
} = this.props;
if (isTrue(prevent_close)) {
const id = this._id;
dispatchCustomElementEvent(this, 'on_close_prevent', {
id,
event,
triggeredBy,
close: e => {
this.toggleOpenClose(e, false);
}
});
} else {
if (ifIsLatest) {
const list = getListOfModalRoots();
if (list.length > 1) {
const last = getModalRoot(-1);
if (last !== this) {
return;
}
}
}
this.toggleOpenClose(event, false);
}
};
removeActiveState() {
const last = getModalRoot(-1);
if (last?._id && last._id !== this._id) {
return this.setActiveState(last._id);
}
try {
document.documentElement.removeAttribute('data-dnb-modal-active');
} catch (e) {
warn('Modal: Error on remove "data-dnb-modal-active"', e);
}
}
setActiveState(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);
}
}
}
render() {
const visualTestsPropsOverride = typeof window !== 'undefined' && window['IS_TEST'] ? {
animation_duration: 0,
no_animation: true
} : {};
const props = extendPropsWithContextInClassComponent(this.props, Modal.defaultProps, this.context.getTranslation(this.props).Modal, this.context.Modal, visualTestsPropsOverride);
const {
root_id = 'root',
content_id = null,
disabled = null,
labelled_by = null,
focus_selector = null,
header_content = null,
bar_content = null,
bypass_invalidation_selectors = null,
vertical_alignment = 'center',
id,
open_delay,
omit_trigger_button = false,
trigger = null,
trigger_attributes = null,
...rest
} = props;
const {
hide,
modalActive
} = this.state;
const modal_content = Modal.getContent(typeof this.props.children === 'function' ? Object.freeze({
...this.props,
close: this.close
}) : this.props);
const render = suffixProps => {
const triggerAttributes = {
hidden: false,
variant: 'secondary',
icon_position: 'left',
...trigger_attributes
};
if (isTrue(disabled)) {
triggerAttributes.disabled = true;
}
if (triggerAttributes.id) {
this._id = triggerAttributes.id;
}
let fallbackTitle;
if (triggerAttributes.title) {
fallbackTitle = triggerAttributes.title;
} else if (suffixProps) {
fallbackTitle = this.context.translation.HelpButton.title;
}
const headerTitle = rest.title || fallbackTitle;
const TriggerButton = trigger ? trigger : HelpButtonInstance;
const title = !triggerAttributes.text && headerTitle ? headerTitle || fallbackTitle : null;
return React.createElement(React.Fragment, null, TriggerButton && !isTrue(omit_trigger_button) && React.createElement(TriggerButton, _extends({}, triggerAttributes, {
id: this._id,
title: title,
onClick: this.toggleOpenClose,
innerRef: this._triggerRef,
className: classnames('dnb-modal__trigger', createSpacingClasses(rest), triggerAttributes.className, triggerAttributes.class)
})), modalActive && modal_content && React.createElement(ParagraphContext.Provider, {
value: {
isNested: false
}
}, React.createElement(ModalRoot, _extends({}, rest, {
id: this._id,
root_id: root_id,
content_id: content_id || `dnb-modal-${this._id}`,
labelled_by: labelled_by,
focus_selector: focus_selector,
modal_content: modal_content,
header_content: header_content,
vertical_alignment: vertical_alignment,
bar_content: bar_content,
bypass_invalidation_selectors: bypass_invalidation_selectors,
close: this.close,
hide: hide,
title: headerTitle,
modalContentCloseRef: this.modalContentCloseRef
}))));
};
return React.createElement(SuffixContext.Consumer, null, render);
}
}
export { CloseButton, Modal as OriginalComponent };
export default classWithCamelCaseProps(Modal);
//# sourceMappingURL=Modal.js.map