@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
485 lines (484 loc) • 17.3 kB
JavaScript
"use client";
import _extends from "@babel/runtime-corejs3/helpers/esm/extends";
import _defineProperty from "@babel/runtime-corejs3/helpers/esm/defineProperty";
var _Hr;
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Context from "../../shared/Context.js";
import { warn, isTrue, makeUniqueId, validateDOMAttributes, dispatchCustomElementEvent, extendPropsWithContextInClassComponent, keycode } from "../../shared/component-helper.js";
import HeightAnimation from "../height-animation/HeightAnimation.js";
import { skeletonDOMAttributes, createSkeletonClass } from "../skeleton/SkeletonHelper.js";
import { spacingPropTypes, createSpacingClasses } from "../space/SpacingHelper.js";
import Hr from "../../elements/hr/Hr.js";
import GlobalStatusController, { GlobalStatusInterceptor } from "./GlobalStatusController.js";
import GlobalStatusProvider from "./GlobalStatusProvider.js";
import Icon from "../icon/Icon.js";
import { InfoIcon, ErrorIcon, WarnIcon } from "../form-status/FormStatus.js";
import Section from "../section/Section.js";
import Button from "../button/Button.js";
export default class GlobalStatus extends React.PureComponent {
static getIcon({
state,
icon,
icon_size
}) {
if (typeof icon === 'string') {
let IconToLoad = icon;
switch (state) {
case 'info':
case 'information':
case 'success':
IconToLoad = InfoIcon;
break;
case 'warning':
case 'warn':
IconToLoad = WarnIcon;
break;
case 'error':
default:
IconToLoad = ErrorIcon;
}
icon = React.createElement(Icon, {
icon: React.createElement(IconToLoad, {
state: state
}),
size: icon_size,
inheritColor: false
});
}
return icon;
}
static getDerivedStateFromProps(props, state) {
if (state._items !== props.items) {
state.globalStatus = GlobalStatusProvider.combineMessages([state.globalStatus, props]);
}
state._items = props.items;
return state;
}
constructor(props) {
super(props);
_defineProperty(this, "state", {
globalStatus: null,
isActive: false
});
_defineProperty(this, "isPassive", () => {
return this.props.show !== 'auto' && isTrue(this.props.show) === false;
});
_defineProperty(this, "setVisible", () => {
if (this.isPassive()) {
return;
}
this.setState({
isActive: true
});
});
_defineProperty(this, "setHidden", () => {
this.setState({
isActive: false
});
});
_defineProperty(this, "onKeyDownHandler", e => {
switch (keycode(e)) {
case 'escape':
case 'esc':
e.preventDefault();
this.closeHandler();
break;
}
});
_defineProperty(this, "closeHandler", () => {
this.provider.add({
status_id: 'internal-close',
show: false
});
if (this.initialActiveElement) {
try {
this.initialActiveElement.focus();
this.initialActiveElement = null;
} catch (e) {
warn(e);
}
}
dispatchCustomElementEvent(this._globalStatus, 'on_hide', this._globalStatus);
});
_defineProperty(this, "gotoItem", (event, item) => {
event.persist();
const keyCode = keycode(event);
if (item.item_id && typeof document !== 'undefined' && typeof window !== 'undefined' && keyCode === 'space' || keyCode === 'enter' || typeof keyCode === 'undefined') {
event.preventDefault();
try {
const element = document.getElementById(item.item_id);
if (!element) {
return;
}
isElementVisible(element, elem => {
try {
elem.addEventListener('blur', e => {
if (e.target.classList) {
e.target.removeAttribute('tabindex');
}
});
elem.classList.add('dnb-no-focus');
elem.setAttribute('tabindex', '-1');
elem.focus({
preventScroll: true
});
} catch (e) {
warn(e);
}
});
if (typeof element.scrollIntoView === 'function') {
element.scrollIntoView({
block: 'center',
behavior: 'smooth'
});
}
} catch (e) {
warn(e);
}
}
});
_defineProperty(this, "itemsRenderHandler", ({
status_anchor_text,
lang
}) => (item, i) => {
const text = item !== null && item !== void 0 && item.text ? item.text : typeof item === 'string' ? item : null;
if (!text) {
return null;
}
const id = item.id || item.item_id ? `${item.item_id}-${i}` : makeUniqueId();
let anchorText = status_anchor_text;
if (React.isValidElement(item.status_anchor_label)) {
anchorText = React.createElement(React.Fragment, null, typeof status_anchor_text === 'string' ? status_anchor_text.replace('%s', '').trim() : status_anchor_text, ' ', item.status_anchor_label);
} else {
anchorText = String(item.status_anchor_text || status_anchor_text).replace('%s', item.status_anchor_label || '').replace(/[: ]$/g, '');
}
const useAutolink = item.item_id && isTrue(item.status_anchor_url);
return React.createElement("li", {
key: i
}, React.createElement("p", {
id: id,
className: "dnb-p"
}, text), item && (useAutolink || item.status_anchor_url) && React.createElement("a", {
className: "dnb-anchor",
"aria-describedby": id,
lang: lang,
href: useAutolink ? `#${item.item_id}` : item.status_anchor_url,
onClick: e => this.gotoItem(e, item),
onKeyDown: e => this.gotoItem(e, item)
}, anchorText));
});
_defineProperty(this, "onAnimationStart", state => {
this.setState({
isAnimating: true
});
switch (state) {
case 'opening':
this.scrollToStatus();
}
});
_defineProperty(this, "onAnimationEnd", state => {
switch (state) {
case 'opened':
this.setFocus();
dispatchCustomElementEvent(this._globalStatus, 'on_open', this._globalStatus);
break;
case 'adjusted':
if (!isTrue(this.props.omit_set_focus_on_update)) {
this.setFocus();
}
dispatchCustomElementEvent(this._globalStatus, 'on_adjust', this._globalStatus);
break;
case 'closed':
dispatchCustomElementEvent(this._globalStatus, 'on_close', this._globalStatus);
break;
}
});
_defineProperty(this, "onOpen", isOpened => {
if (isOpened) {
dispatchCustomElementEvent(this._globalStatus, 'on_show', this._globalStatus);
}
});
this._wrapperRef = React.createRef();
this.provider = GlobalStatusProvider.create(props.id);
this.state.globalStatus = this._globalStatus = this.provider.init(props);
if (isTrue(props.show)) {
this.state.isActive = true;
}
this.provider.onUpdate(globalStatus => {
if (globalStatus.on_close) {
this._globalStatus = globalStatus;
}
this.setState({
globalStatus
});
if (isTrue(this.props.autoclose) && this._hadContent && !this.hasContent(globalStatus) && !isTrue(this.props.show) || typeof globalStatus.show !== 'undefined' && !isTrue(globalStatus.show)) {
this.setHidden();
} else if (isTrue(this.props.show) || typeof globalStatus.show !== 'undefined' && isTrue(globalStatus.show)) {
this._hadContent = this.hasContent(globalStatus);
this.setVisible();
}
});
this.initialActiveElement = null;
}
componentWillUnmount() {
clearTimeout(this._scrollToStatusTimeout);
this.provider.empty();
}
componentDidUpdate(prevProps) {
if (prevProps !== this.props) {
const globalStatus = extendPropsWithContextInClassComponent(this.props, GlobalStatus.defaultProps, this.context.globalStatus);
this.setState({
globalStatus
});
}
if (prevProps.show !== this.props.show) {
if (isTrue(this.props.show)) {
this.setVisible();
} else {
this.setHidden();
}
}
}
hasContent(globalStatus) {
var _globalStatus$items;
return Boolean(((_globalStatus$items = globalStatus.items) === null || _globalStatus$items === void 0 ? void 0 : _globalStatus$items.length) > 0 || globalStatus.text);
}
correctStatus(state) {
switch (state) {
case 'information':
state = 'info';
break;
}
return state;
}
setFocus() {
if (typeof document !== 'undefined' && document.activeElement !== this._wrapperRef.current) {
this.initialActiveElement = document.activeElement;
}
if (this._wrapperRef.current && !isTrue(this.props.omit_set_focus)) {
this._wrapperRef.current.focus({
preventScroll: true
});
}
}
async scrollToStatus(isDone = null) {
if (typeof window === 'undefined' || isTrue(this.state.globalStatus.autoscroll) === false) {
return;
}
try {
const element = this._wrapperRef.current;
this._scrollToStatusTimeout = isElementVisible(element, isDone);
if (element && typeof element.scrollIntoView === 'function') {
await wait(1);
element.scrollIntoView({
block: 'center',
behavior: 'smooth'
});
} else {
const top = element.offsetTop;
if (window.scrollTo) {
window.scrollTo({
top,
behavior: 'smooth'
});
} else {
window.scrollTop = top;
}
}
} catch (e) {
warn('GlobalStatus: Could not scroll into view!', e);
}
}
render() {
var _this$context;
const {
isActive
} = this.state;
const fallbackProps = extendPropsWithContextInClassComponent(this.props, GlobalStatus.defaultProps, this.context.getTranslation(this.props).GlobalStatus);
const props = extendPropsWithContextInClassComponent(GlobalStatusProvider.combineMessages([this.context.globalStatus, this.state.globalStatus]), GlobalStatus.defaultProps, fallbackProps);
const lang = this.context.locale;
const {
title,
default_title,
state: rawState,
className,
no_animation,
hide_close_button,
close_text,
status_anchor_text,
skeleton,
id,
item,
items,
autoclose,
show,
delay,
autoscroll,
text,
icon,
icon_size,
children,
...attributes
} = props;
const wrapperParams = {
id,
key: 'global-status',
className: classnames("dnb-global-status__wrapper dnb-no-focus", createSkeletonClass('font', skeleton, this.context), createSpacingClasses(props), className),
'aria-live': isActive ? 'assertive' : 'off',
onKeyDown: this.onKeyDownHandler,
tabIndex: '-1'
};
const state = this.correctStatus(rawState);
const iconToRender = GlobalStatus.getIcon({
state,
icon: icon || fallbackProps.icon,
icon_size: icon_size || fallbackProps.icon_size,
theme: ((_this$context = this.context) === null || _this$context === void 0 || (_this$context = _this$context.theme) === null || _this$context === void 0 ? void 0 : _this$context.name) || 'ui'
});
const titleToRender = title || fallbackProps.title || fallbackProps.default_title;
const noAnimation = isTrue(no_animation);
const itemsToRender = props.items || [];
const contentToRender = props.text || props.children;
const params = {
className: `dnb-global-status dnb-global-status--${state}`,
...attributes
};
skeletonDOMAttributes(params, skeleton, this.context);
validateDOMAttributes(this.props, params);
const renderedItems = itemsToRender.length > 0 && React.createElement("ul", {
className: "dnb-ul"
}, itemsToRender.map(this.itemsRenderHandler({
status_anchor_text,
lang
})));
const hasContent = renderedItems || contentToRender;
const renderedContent = React.createElement(React.Fragment, null, title !== false && React.createElement(React.Fragment, null, React.createElement("div", {
className: "dnb-global-status__title",
role: titleToRender !== null && titleToRender !== void 0 && titleToRender.type ? undefined : 'paragraph',
lang: lang
}, React.createElement("span", {
className: "dnb-global-status__icon"
}, iconToRender), titleToRender, !isTrue(hide_close_button) && React.createElement(Button, {
text: close_text,
title: close_text,
variant: state === 'success' ? 'secondary' : 'tertiary',
className: "dnb-global-status__close-button",
icon: "close",
on_click: this.closeHandler,
size: "medium",
icon_position: "left"
})), hasContent && React.createElement("div", {
className: "dnb-global-status__message"
}, React.createElement("div", {
className: 'dnb-global-status__message__content' + (!renderedItems ? " dnb-space__bottom--small" : "")
}, typeof contentToRender === 'string' ? React.createElement("p", {
className: "dnb-p"
}, contentToRender) : contentToRender, renderedItems)), _Hr || (_Hr = React.createElement(Hr, {
breakout: true
}))));
return React.createElement("div", _extends({}, wrapperParams, {
ref: this._wrapperRef
}), React.createElement("section", params, React.createElement(HeightAnimation, {
className: "dnb-global-status__shell",
duration: 800,
delay: delay,
open: isActive,
animate: !noAnimation,
onAnimationEnd: this.onAnimationEnd,
onAnimationStart: this.onAnimationStart,
onOpen: this.onOpen
}, React.createElement(Section, {
element: "div",
variant: state,
className: "dnb-global-status__content"
}, renderedContent))));
}
}
_defineProperty(GlobalStatus, "contextType", Context);
_defineProperty(GlobalStatus, "defaultProps", {
id: 'main',
status_id: 'status-main',
title: null,
default_title: null,
text: null,
items: [],
icon: 'error',
icon_size: 'medium',
state: 'error',
show: 'auto',
autoscroll: true,
autoclose: true,
no_animation: false,
close_text: 'Lukk',
hide_close_button: false,
omit_set_focus: false,
omit_set_focus_on_update: true,
delay: null,
status_anchor_text: null,
skeleton: null,
className: null,
children: null,
on_adjust: null,
on_open: null,
on_show: null,
on_close: null,
on_hide: null
});
process.env.NODE_ENV !== "production" ? GlobalStatus.propTypes = {
id: PropTypes.string,
status_id: PropTypes.string,
title: PropTypes.oneOfType([PropTypes.node, PropTypes.bool]),
default_title: PropTypes.string,
text: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
items: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.array]),
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
icon_size: PropTypes.string,
state: PropTypes.oneOf(['error', 'info', 'warning', 'success']),
show: PropTypes.oneOf(['auto', true, false, 'true', 'false']),
autoscroll: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
autoclose: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
no_animation: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
delay: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
close_text: PropTypes.node,
hide_close_button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
omit_set_focus: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
omit_set_focus_on_update: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
status_anchor_text: PropTypes.node,
skeleton: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
...spacingPropTypes,
className: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
on_adjust: PropTypes.func,
on_open: PropTypes.func,
on_show: PropTypes.func,
on_close: PropTypes.func,
on_hide: PropTypes.func
} : void 0;
GlobalStatus.create = (...args) => new GlobalStatusInterceptor(...args);
GlobalStatus.Update = GlobalStatus.create;
GlobalStatus.Add = GlobalStatusController;
GlobalStatus.Remove = GlobalStatusController.Remove;
const isElementVisible = (elem, callback, delayFallback = 1e3) => {
if (typeof IntersectionObserver !== 'undefined') {
const intersectionObserver = new IntersectionObserver(entries => {
const [entry] = entries;
if (entry.isIntersecting) {
intersectionObserver.unobserve(elem);
if (typeof callback === 'function') {
callback(elem);
}
}
});
intersectionObserver.observe(elem);
} else {
if (typeof callback === 'function') {
return setTimeout(() => callback(elem), delayFallback);
}
}
return null;
};
const wait = duration => new Promise(r => setTimeout(r, duration));
GlobalStatus._supportsSpacingProps = true;
//# sourceMappingURL=GlobalStatus.js.map