@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
295 lines (284 loc) • 10.4 kB
JavaScript
/**
* MSKCC 2021, 2024
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js');
var React = require('react');
var reactIs = require('react-is');
var PropTypes = require('prop-types');
var ModalHeader = require('./ModalHeader.js');
var ModalFooter = require('./ModalFooter.js');
var cx = require('classnames');
var toggleClass = require('../../tools/toggleClass.js');
var requiredIfGivenPropIsTruthy = require('../../prop-types/requiredIfGivenPropIsTruthy.js');
var wrapFocus = require('../../internal/wrapFocus.js');
var usePrefix = require('../../internal/usePrefix.js');
var match = require('../../internal/keyboard/match.js');
var keys = require('../../internal/keyboard/keys.js');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
var cx__default = /*#__PURE__*/_interopDefaultLegacy(cx);
const ModalBody = /*#__PURE__*/React__default["default"].forwardRef(function ModalBody(_ref, ref) {
let {
className: customClassName,
children,
hasForm,
hasScrollingContent,
...rest
} = _ref;
const prefix = usePrefix.usePrefix();
const contentClass = cx__default["default"](`${prefix}--modal-content`, hasForm && `${prefix}--modal-content--with-form`, hasScrollingContent && `${prefix}--modal-scroll-content`, customClassName);
const hasScrollingContentProps = hasScrollingContent ? {
tabIndex: 0,
role: 'region'
} : {};
return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement("div", _rollupPluginBabelHelpers["extends"]({
className: contentClass
}, hasScrollingContentProps, rest, {
ref: ref
}), children), hasScrollingContent && /*#__PURE__*/React__default["default"].createElement("div", {
className: `${prefix}--modal-content--overflow-indicator`
}));
});
ModalBody.propTypes = {
/**
* Required props for the accessibility label of the header
*/
// @ts-expect-error: Built-in prop-types > TS logic doesn't jive well with custom validators
['aria-label']: requiredIfGivenPropIsTruthy["default"]('hasScrollingContent', PropTypes__default["default"].string),
/**
* Specify the content to be placed in the ModalBody
*/
children: PropTypes__default["default"].node,
/**
* Specify an optional className to be added to the Modal Body node
*/
className: PropTypes__default["default"].string,
/**
* Provide whether the modal content has a form element.
* If `true` is used here, non-form child content should have `bx--modal-content__regular-content` class.
*/
hasForm: PropTypes__default["default"].bool,
/**
* Specify whether the modal contains scrolling content
*/
hasScrollingContent: PropTypes__default["default"].bool
};
const ComposedModal = /*#__PURE__*/React__default["default"].forwardRef(function ComposedModal(_ref2, ref) {
let {
['aria-labelledby']: ariaLabelledBy,
['aria-label']: ariaLabel,
children,
className: customClassName,
containerClassName,
danger,
isFullWidth,
onClose,
onKeyDown,
open,
preventCloseOnClickOutside,
selectorPrimaryFocus,
selectorsFloatingMenus,
size,
...rest
} = _ref2;
const prefix = usePrefix.usePrefix();
const [isOpen, setIsOpen] = React.useState(!!open);
const [wasOpen, setWasOpen] = React.useState(!!open);
const innerModal = React.useRef(null);
const button = React.useRef(null);
const startSentinel = React.useRef(null);
const endSentinel = React.useRef(null);
// Kepp track of modal open/close state
// and propagate it to the document.body
React.useEffect(() => {
if (open !== wasOpen) {
setIsOpen(!!open);
setWasOpen(!!open);
toggleClass["default"](document.body, `${prefix}--body--with-modal-open`, !!open);
}
}, [open, wasOpen, prefix]);
// Remove the document.body className on unmount
React.useEffect(() => {
return () => {
toggleClass["default"](document.body, `${prefix}--body--with-modal-open`, false);
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
function handleKeyDown(evt) {
if (match.match(evt, keys.Escape)) {
closeModal(evt);
}
onKeyDown?.(evt);
}
function handleMousedown(evt) {
const isInside = innerModal.current?.contains(evt.target);
if (!isInside && !preventCloseOnClickOutside) {
closeModal(evt);
}
}
function handleBlur(_ref3) {
let {
target: oldActiveNode,
relatedTarget: currentActiveNode
} = _ref3;
if (open && currentActiveNode && oldActiveNode && innerModal.current) {
const {
current: bodyNode
} = innerModal;
const {
current: startSentinelNode
} = startSentinel;
const {
current: endSentinelNode
} = endSentinel;
wrapFocus["default"]({
bodyNode,
startTrapNode: startSentinelNode,
endTrapNode: endSentinelNode,
currentActiveNode,
oldActiveNode,
selectorsFloatingMenus: selectorsFloatingMenus?.filter(Boolean)
});
}
}
function closeModal(evt) {
if (!onClose || onClose(evt) !== false) {
setIsOpen(false);
}
}
const modalClass = cx__default["default"](`${prefix}--modal`, isOpen && 'is-visible', danger && `${prefix}--modal--danger`, customClassName);
const containerClass = cx__default["default"](`${prefix}--modal-container`, size && `${prefix}--modal-container--${size}`, isFullWidth && `${prefix}--modal-container--full-width`, containerClassName);
// Generate aria-label based on Modal Header label if one is not provided (L253)
let generatedAriaLabel;
const childrenWithProps = React__default["default"].Children.toArray(children).map(child => {
switch (true) {
case reactIs.isElement(child) && child.type === React__default["default"].createElement(ModalHeader.ModalHeader).type:
{
const el = child;
generatedAriaLabel = el.props.label;
return /*#__PURE__*/React__default["default"].cloneElement(el, {
closeModal
});
}
case reactIs.isElement(child) && child.type === React__default["default"].createElement(ModalFooter.ModalFooter).type:
{
const el = child;
return /*#__PURE__*/React__default["default"].cloneElement(el, {
closeModal,
inputref: button
});
}
default:
return child;
}
});
React.useEffect(() => {
const focusButton = focusContainerElement => {
if (focusContainerElement) {
const primaryFocusElement = focusContainerElement.querySelector(selectorPrimaryFocus);
if (primaryFocusElement) {
primaryFocusElement.focus();
return;
}
if (button.current) {
button.current.focus();
}
}
};
if (!open) {
return;
}
if (innerModal.current) {
focusButton(innerModal.current);
}
}, [open, selectorPrimaryFocus]);
return /*#__PURE__*/React__default["default"].createElement("div", _rollupPluginBabelHelpers["extends"]({}, rest, {
role: "presentation",
ref: ref,
"aria-hidden": !open,
onBlur: handleBlur,
onMouseDown: handleMousedown,
onKeyDown: handleKeyDown,
className: modalClass
}), /*#__PURE__*/React__default["default"].createElement("div", {
className: containerClass,
role: "dialog",
"aria-modal": "true",
"aria-label": ariaLabel ? ariaLabel : generatedAriaLabel,
"aria-labelledby": ariaLabelledBy
}, /*#__PURE__*/React__default["default"].createElement("button", {
type: "button",
ref: startSentinel,
className: `${prefix}--visually-hidden`
}, "Focus sentinel"), /*#__PURE__*/React__default["default"].createElement("div", {
ref: innerModal,
className: `${prefix}--modal-container-body`
}, childrenWithProps), /*#__PURE__*/React__default["default"].createElement("button", {
type: "button",
ref: endSentinel,
className: `${prefix}--visually-hidden`
}, "Focus sentinel")));
});
ComposedModal.propTypes = {
/**
* Specify the aria-label for bx--modal-container
*/
['aria-label']: PropTypes__default["default"].string,
/**
* Specify the aria-labelledby for bx--modal-container
*/
['aria-labelledby']: PropTypes__default["default"].string,
/**
* Specify the content to be placed in the ComposedModal
*/
children: PropTypes__default["default"].node,
/**
* Specify an optional className to be applied to the modal root node
*/
className: PropTypes__default["default"].string,
/**
* Specify an optional className to be applied to the modal node
*/
containerClassName: PropTypes__default["default"].string,
/**
* Specify whether the primary button should be replaced with danger button.
* Note that this prop is not applied if you render primary/danger button by yourself
*/
danger: PropTypes__default["default"].bool,
/**
* Specify whether the Modal content should have any inner padding.
*/
isFullWidth: PropTypes__default["default"].bool,
/**
* Specify an optional handler for closing modal.
* Returning `false` here prevents closing modal.
*/
onClose: PropTypes__default["default"].func,
/**
* Specify an optional handler for the `onKeyDown` event. Called for all
* `onKeyDown` events that do not close the modal
*/
onKeyDown: PropTypes__default["default"].func,
/**
* Specify whether the Modal is currently open
*/
open: PropTypes__default["default"].bool,
preventCloseOnClickOutside: PropTypes__default["default"].bool,
/**
* Specify a CSS selector that matches the DOM element that should be
* focused when the Modal opens
*/
selectorPrimaryFocus: PropTypes__default["default"].string,
/**
* Specify the CSS selectors that match the floating menus
*/
selectorsFloatingMenus: PropTypes__default["default"].arrayOf(PropTypes__default["default"].string),
/**
* Specify the size variant.
*/
size: PropTypes__default["default"].oneOf(['xs', 'sm', 'md', 'lg'])
};
exports.ModalBody = ModalBody;
exports["default"] = ComposedModal;