@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
408 lines (404 loc) • 13.5 kB
JavaScript
/**
* MSKCC 2021, 2024
*/
import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
import PropTypes from 'prop-types';
import React__default, { useRef, useEffect } from 'react';
import cx from 'classnames';
import { Close } from '@carbon/icons-react';
import toggleClass from '../../tools/toggleClass.js';
import Button from '../Button/Button.js';
import '../Button/Button.Skeleton.js';
import ButtonSet from '../ButtonSet/ButtonSet.js';
import requiredIfGivenPropIsTruthy from '../../prop-types/requiredIfGivenPropIsTruthy.js';
import wrapFocus, { elementOrParentIsFloatingMenu } from '../../internal/wrapFocus.js';
import setupGetInstanceId from '../../tools/setupGetInstanceId.js';
import { usePrefix } from '../../internal/usePrefix.js';
import { match } from '../../internal/keyboard/match.js';
import { Escape, Enter } from '../../internal/keyboard/keys.js';
const getInstanceId = setupGetInstanceId();
const Modal = /*#__PURE__*/React__default.forwardRef(function Modal(_ref, ref) {
let {
'aria-label': ariaLabelProp,
children,
className,
modalHeading = '',
modalLabel = '',
modalAriaLabel,
passiveModal = false,
secondaryButtonText,
primaryButtonText,
open,
onRequestClose = () => {},
onRequestSubmit = () => {},
onSecondarySubmit,
primaryButtonDisabled = false,
danger,
alert,
secondaryButtons,
selectorPrimaryFocus = '[data-modal-primary-focus]',
// eslint-disable-line
selectorsFloatingMenus,
// eslint-disable-line
shouldSubmitOnEnter,
// eslint-disable-line
size,
hasScrollingContent = false,
closeButtonLabel,
preventCloseOnClickOutside = false,
// eslint-disable-line
isFullWidth,
primaryButtonKind = 'primary',
secondaryButtonKind = 'tertiary',
primaryButtonClassName,
secondaryButtonClassName,
...rest
} = _ref;
const prefix = usePrefix();
const button = useRef();
const secondaryButton = useRef();
const innerModal = useRef();
const startTrap = useRef();
const endTrap = useRef();
const modalInstanceId = `modal-${getInstanceId()}`;
const modalLabelId = `${prefix}--modal-header__label--${modalInstanceId}`;
const modalHeadingId = `${prefix}--modal-header__heading--${modalInstanceId}`;
const modalBodyId = `${prefix}--modal-body--${modalInstanceId}`;
const modalCloseButtonClass = `${prefix}--modal-close`;
function isCloseButton(element) {
return !onSecondarySubmit && element === secondaryButton.current || element.classList.contains(modalCloseButtonClass);
}
function handleKeyDown(evt) {
if (open) {
if (match(evt, Escape)) {
onRequestClose(evt);
}
if (match(evt, Enter) && shouldSubmitOnEnter && !isCloseButton(evt.target)) {
onRequestSubmit(evt);
}
}
}
function handleMousedown(evt) {
if (innerModal.current && !innerModal.current.contains(evt.target) && !elementOrParentIsFloatingMenu(evt.target, selectorsFloatingMenus) && !preventCloseOnClickOutside) {
onRequestClose(evt);
}
}
function handleBlur(_ref2) {
let {
target: oldActiveNode,
relatedTarget: currentActiveNode
} = _ref2;
if (open && currentActiveNode && oldActiveNode) {
const {
current: bodyNode
} = innerModal;
const {
current: startTrapNode
} = startTrap;
const {
current: endTrapNode
} = endTrap;
wrapFocus({
bodyNode,
startTrapNode,
endTrapNode,
currentActiveNode,
oldActiveNode,
selectorsFloatingMenus
});
}
}
const onSecondaryButtonClick = onSecondarySubmit ? onSecondarySubmit : onRequestClose;
const modalClasses = cx(`${prefix}--modal`, {
[`${prefix}--modal-tall`]: !passiveModal,
'is-visible': open,
[`${prefix}--modal--danger`]: danger,
[className]: className
});
const containerClasses = cx(`${prefix}--modal-container`, {
[`${prefix}--modal-container--${size}`]: size,
[`${prefix}--modal-container--full-width`]: isFullWidth
});
const contentClasses = cx(`${prefix}--modal-content`, {
[`${prefix}--modal-scroll-content`]: hasScrollingContent
});
const footerClasses = cx(`${prefix}--modal-footer`, {
[`${prefix}--modal-footer--three-button`]: Array.isArray(secondaryButtons) && secondaryButtons.length === 2
});
const ariaLabel = modalLabel || ariaLabelProp || modalAriaLabel || modalHeading;
const getAriaLabelledBy = modalLabel ? modalLabelId : modalHeadingId;
const hasScrollingContentProps = hasScrollingContent ? {
tabIndex: 0,
role: 'region',
'aria-label': ariaLabel,
'aria-labelledby': getAriaLabelledBy
} : {};
const alertDialogProps = {};
if (alert && passiveModal) {
alertDialogProps.role = 'alert';
}
if (alert && !passiveModal) {
alertDialogProps.role = 'alertdialog';
alertDialogProps['aria-describedby'] = modalBodyId;
}
useEffect(() => {
return () => {
toggleClass(document.body, `${prefix}--body--with-modal-open`, false);
};
}, [prefix]);
useEffect(() => {
toggleClass(document.body, `${prefix}--body--with-modal-open`, open);
}, [open, prefix]);
useEffect(() => {
const initialFocus = focusContainerElement => {
const containerElement = focusContainerElement || innerModal.current;
const primaryFocusElement = containerElement ? containerElement.querySelector(danger ? `.${prefix}--btn--secondary` : selectorPrimaryFocus) : null;
if (primaryFocusElement) {
return primaryFocusElement;
}
return button && button.current;
};
const focusButton = focusContainerElement => {
const target = initialFocus(focusContainerElement);
if (target) {
target.focus();
}
};
if (open) {
focusButton(innerModal.current);
}
}, [open, selectorPrimaryFocus, danger, prefix]);
const modalButton = /*#__PURE__*/React__default.createElement("button", {
className: modalCloseButtonClass,
type: "button",
onClick: onRequestClose,
title: ariaLabel,
"aria-label": closeButtonLabel ? closeButtonLabel : 'close',
ref: button
}, /*#__PURE__*/React__default.createElement(Close, {
size: 20,
"aria-hidden": "true",
tabIndex: "-1",
className: `${modalCloseButtonClass}__icon`
}));
const modalBody = /*#__PURE__*/React__default.createElement("div", _extends({
ref: innerModal,
role: "dialog"
}, alertDialogProps, {
className: containerClasses,
"aria-label": ariaLabel,
"aria-modal": "true",
tabIndex: "-1"
}), /*#__PURE__*/React__default.createElement("div", {
className: `${prefix}--modal-header`
}, passiveModal && modalButton, modalLabel && /*#__PURE__*/React__default.createElement("h2", {
id: modalLabelId,
className: `${prefix}--modal-header__label`
}, modalLabel), /*#__PURE__*/React__default.createElement("h3", {
id: modalHeadingId,
className: `${prefix}--modal-header__heading`
}, modalHeading), !passiveModal && modalButton), /*#__PURE__*/React__default.createElement("div", _extends({
id: modalBodyId,
className: contentClasses
}, hasScrollingContentProps), children), hasScrollingContent && /*#__PURE__*/React__default.createElement("div", {
className: `${prefix}--modal-content--overflow-indicator`
}), !passiveModal && /*#__PURE__*/React__default.createElement(ButtonSet, {
className: footerClasses
}, Array.isArray(secondaryButtons) && secondaryButtons.length <= 2 ? secondaryButtons.map((_ref3, i) => {
let {
buttonText,
onClick: onButtonClick,
buttonKind: extraButtonKind = 'tertiary',
buttonClassName: extraClassName
} = _ref3;
return /*#__PURE__*/React__default.createElement(Button, {
key: `${buttonText}-${i}`,
kind: extraButtonKind,
onClick: onButtonClick,
className: extraClassName
}, buttonText);
}) : secondaryButtonText && /*#__PURE__*/React__default.createElement(Button, {
kind: secondaryButtonKind,
onClick: onSecondaryButtonClick,
ref: secondaryButton,
className: secondaryButtonClassName
}, secondaryButtonText), /*#__PURE__*/React__default.createElement(Button, {
className: primaryButtonClassName,
kind: danger ? 'danger' : primaryButtonKind,
disabled: primaryButtonDisabled,
onClick: onRequestSubmit,
ref: button
}, primaryButtonText)));
return /*#__PURE__*/React__default.createElement("div", _extends({}, rest, {
onKeyDown: handleKeyDown,
onMouseDown: handleMousedown,
onBlur: handleBlur,
className: modalClasses,
role: "presentation",
ref: ref
}), /*#__PURE__*/React__default.createElement("span", {
ref: startTrap,
tabIndex: "0",
role: "link",
className: `${prefix}--visually-hidden`
}, "Focus sentinel"), modalBody, /*#__PURE__*/React__default.createElement("span", {
ref: endTrap,
tabIndex: "0",
role: "link",
className: `${prefix}--visually-hidden`
}, "Focus sentinel"));
});
Modal.propTypes = {
/**
* Specify whether the Modal is displaying an alert, error or warning
* Should go hand in hand with the danger prop.
*/
alert: PropTypes.bool,
/**
* Required props for the accessibility label of the header
*/
['aria-label']: requiredIfGivenPropIsTruthy('hasScrollingContent', PropTypes.string),
/**
* Provide the contents of your Modal
*/
children: PropTypes.node,
/**
* Specify an optional className to be applied to the modal root node
*/
className: PropTypes.string,
/**
* Specify an label for the close button of the modal; defaults to close
*/
closeButtonLabel: PropTypes.string,
/**
* Specify whether the Modal is for dangerous actions
*/
danger: PropTypes.bool,
/**
* Specify whether the modal contains scrolling content
*/
hasScrollingContent: PropTypes.bool,
/**
* Specify the DOM element ID of the top-level node.
*/
id: PropTypes.string,
/**
* Specify whether or not the Modal content should have any inner padding.
*/
isFullWidth: PropTypes.bool,
/**
* Specify a label to be read by screen readers on the modal root node
*/
modalAriaLabel: PropTypes.string,
/**
* Specify the content of the modal header title.
*/
modalHeading: PropTypes.node,
/**
* Specify the content of the modal header label.
*/
modalLabel: PropTypes.node,
/**
* Specify a handler for keypresses.
*/
onKeyDown: PropTypes.func,
/**
* Specify a handler for closing modal.
* The handler should care of closing modal, e.g. changing `open` prop.
*/
onRequestClose: PropTypes.func,
/**
* Specify a handler for "submitting" modal.
* The handler should care of closing modal, e.g. changing `open` prop, if necessary.
*/
onRequestSubmit: PropTypes.func,
/**
* Specify a handler for the secondary button.
* Useful if separate handler from `onRequestClose` is desirable
*/
onSecondarySubmit: PropTypes.func,
/**
* Specify whether the Modal is currently open
*/
open: PropTypes.bool,
/**
* Specify whether the modal should be button-less
*/
passiveModal: PropTypes.bool,
/**
* Prevent closing on click outside of modal
*/
preventCloseOnClickOutside: PropTypes.bool,
/**
* Specify whether the Button should be disabled, or not
*/
primaryButtonDisabled: PropTypes.bool,
/**
* Specify the text for the primary button
*/
primaryButtonText: PropTypes.node,
/**
* Specify kind of the primary button
*/
primaryButtonKind: PropTypes.string,
/**
* Specify an optional className to be applied to the primary button
*/
primaryButtonClassName: PropTypes.string,
/**
* Specify the text for the secondary button
*/
secondaryButtonText: PropTypes.node,
/**
* Specify kind of the secondary button
*/
secondaryButtonKind: PropTypes.string,
/**
* Specify an optional className to be applied to the secondary button
*/
secondaryButtonClassName: PropTypes.string,
/**
* Specify an array of config objects for secondary buttons
* (`Array<{
* buttonText: string,
* onClick: function,
* buttonKind: string
* buttonClassName: string
* }>`).
*/
secondaryButtons: (props, propName, componentName) => {
if (props.secondaryButtons) {
if (!Array.isArray(props.secondaryButtons) || props.secondaryButtons.length !== 2) {
return new Error(`${propName} needs to be an array of two button config objects`);
}
const shape = {
buttonText: PropTypes.node,
onClick: PropTypes.func
};
props[propName].forEach(secondaryButton => {
PropTypes.checkPropTypes(shape, secondaryButton, propName, componentName);
});
}
return null;
},
/**
* Specify a CSS selector that matches the DOM element that should
* be focused when the Modal opens
*/
selectorPrimaryFocus: PropTypes.string,
/**
* Specify CSS selectors that match DOM elements working as floating menus.
* Focusing on those elements won't trigger "focus-wrap" behavior
*/
selectorsFloatingMenus: PropTypes.arrayOf(PropTypes.string),
/**
* Specify if Enter key should be used as "submit" action
*/
shouldSubmitOnEnter: PropTypes.bool,
/**
* Specify the size variant.
*/
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg'])
};
var Modal$1 = Modal;
export { Modal$1 as default };