UNPKG

@mskcc/carbon-react

Version:

Carbon react components for the MSKCC DSM

298 lines (287 loc) 10.5 kB
/** * 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']) }; ComposedModal.defaultProps = { selectorPrimaryFocus: '[data-modal-primary-focus]' }; exports.ModalBody = ModalBody; exports["default"] = ComposedModal;