@carbon/react
Version:
React components for the Carbon Design System
679 lines (667 loc) • 26.4 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
;
Object.defineProperty(exports, '__esModule', { value: true });
var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js');
var PropTypes = require('prop-types');
var React = require('react');
var cx = require('classnames');
var iconsReact = require('@carbon/icons-react');
var toggleClass = require('../../tools/toggleClass.js');
var Button = require('../Button/Button.js');
require('../Button/Button.Skeleton.js');
var ButtonSet = require('../ButtonSet/ButtonSet.js');
var InlineLoading = require('../InlineLoading/InlineLoading.js');
var index$3 = require('../Layer/index.js');
var requiredIfGivenPropIsTruthy = require('../../prop-types/requiredIfGivenPropIsTruthy.js');
var wrapFocus = require('../../internal/wrapFocus.js');
var useResizeObserver = require('../../internal/useResizeObserver.js');
var useId = require('../../internal/useId.js');
var useMergedRefs = require('../../internal/useMergedRefs.js');
var usePrefix = require('../../internal/usePrefix.js');
var usePreviousValue = require('../../internal/usePreviousValue.js');
var keys = require('../../internal/keyboard/keys.js');
var match = require('../../internal/keyboard/match.js');
var index$2 = require('../IconButton/index.js');
var noopFn = require('../../internal/noopFn.js');
var Text = require('../Text/Text.js');
require('../Text/TextDirection.js');
var index = require('../FeatureFlags/index.js');
var events = require('../../tools/events.js');
var deprecate = require('../../prop-types/deprecate.js');
var Dialog = require('../Dialog/Dialog.js');
var index$1 = require('../AILabel/index.js');
var utils = require('../../internal/utils.js');
var warning = require('../../internal/warning.js');
var ModalPresence = require('./ModalPresence.js');
const ModalSizes = ['xs', 'sm', 'md', 'lg'];
const invalidOutsideClickMessage = '`<Modal>` prop `preventCloseOnClickOutside` should not be `false` when ' + '`passiveModal` is `false`. Transactional, non-passive Modals should ' + 'not be dissmissable by clicking outside. ' + 'See: https://carbondesignsystem.com/components/modal/usage/#transactional-modal';
const Modal = /*#__PURE__*/React.forwardRef(function Modal({
open,
...props
}, ref) {
const id = useId.useId();
const enablePresence = index.useFeatureFlag('enable-presence');
const hasPresenceContext = Boolean(React.useContext(ModalPresence.ModalPresenceContext));
const hasPresenceOptIn = enablePresence || hasPresenceContext;
const exclusivePresenceContext = ModalPresence.useExclusiveModalPresenceContext(id);
// if opt in and not exclusive to a presence context, wrap with presence
if (hasPresenceOptIn && !exclusivePresenceContext) {
return /*#__PURE__*/React.createElement(ModalPresence.ModalPresence, {
open: open ?? false,
_presenceId: id
// do not auto enable styles for opt-in by feature flag
,
_autoEnablePresence: hasPresenceContext
}, /*#__PURE__*/React.createElement(ModalDialog, _rollupPluginBabelHelpers.extends({
open: true,
ref: ref
}, props)));
}
return /*#__PURE__*/React.createElement(ModalDialog, _rollupPluginBabelHelpers.extends({
ref: ref,
open: open
}, props));
});
const ModalDialog = /*#__PURE__*/React.forwardRef(function ModalDialog({
'aria-label': ariaLabelProp,
children,
className,
decorator,
modalHeading = '',
modalLabel = '',
modalAriaLabel,
passiveModal = false,
secondaryButtonText,
primaryButtonText,
open: externalOpen,
onRequestClose = noopFn.noopFn,
onRequestSubmit = noopFn.noopFn,
onSecondarySubmit,
primaryButtonDisabled = false,
danger,
alert,
secondaryButtons,
selectorPrimaryFocus = '[data-modal-primary-focus]',
selectorsFloatingMenus,
shouldSubmitOnEnter,
size,
hasScrollingContent = false,
closeButtonLabel = 'Close',
preventCloseOnClickOutside,
isFullWidth,
launcherButtonRef,
loadingStatus = 'inactive',
loadingDescription,
loadingIconDescription,
onLoadingSuccess = noopFn.noopFn,
slug,
...rest
}, ref) {
const prefix = usePrefix.usePrefix();
const button = React.useRef(null);
const secondaryButton = React.useRef(null);
const contentRef = React.useRef(null);
const innerModal = React.useRef(null);
const startTrap = React.useRef(null);
const endTrap = React.useRef(null);
const wrapFocusTimeout = React.useRef(null);
const modalInstanceId = `modal-${useId.useId()}`;
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`;
const primaryButtonClass = cx({
[`${prefix}--btn--loading`]: loadingStatus !== 'inactive'
});
const loadingActive = loadingStatus !== 'inactive';
const presenceContext = React.useContext(ModalPresence.ModalPresenceContext);
const mergedRefs = useMergedRefs.useMergedRefs([ref, presenceContext?.presenceRef]);
const enablePresence = index.useFeatureFlag('enable-presence') || presenceContext?.autoEnablePresence;
// always mark as open when mounted with presence
const open = externalOpen || enablePresence;
const prevOpen = usePreviousValue.usePreviousValue(open);
const deprecatedFlag = index.useFeatureFlag('enable-experimental-focus-wrap-without-sentinels');
const focusTrapWithoutSentinelsFlag = index.useFeatureFlag('enable-focus-wrap-without-sentinels');
const focusTrapWithoutSentinels = focusTrapWithoutSentinelsFlag || deprecatedFlag;
const enableDialogElement = index.useFeatureFlag('enable-dialog-element');
process.env.NODE_ENV !== "production" ? warning.warning(!(focusTrapWithoutSentinels && enableDialogElement), '`<Modal>` detected both `focusTrapWithoutSentinels` and ' + '`enableDialogElement` feature flags are enabled. The native dialog ' + 'element handles focus, so `enableDialogElement` must be off for ' + '`focusTrapWithoutSentinels` to have any effect.') : void 0;
process.env.NODE_ENV !== "production" ? warning.warning(!(!passiveModal && preventCloseOnClickOutside === false), invalidOutsideClickMessage) : void 0;
function isCloseButton(element) {
return !onSecondarySubmit && element === secondaryButton.current || element.classList.contains(modalCloseButtonClass);
}
function handleKeyDown(evt) {
const {
target
} = evt;
evt.stopPropagation();
if (open && target instanceof HTMLElement) {
if (match.match(evt, keys.Enter) && shouldSubmitOnEnter && !isCloseButton(target) && document.activeElement !== button.current) {
onRequestSubmit(evt);
}
if (focusTrapWithoutSentinels && !enableDialogElement && match.match(evt, keys.Tab) && innerModal.current) {
wrapFocus.wrapFocusWithoutSentinels({
containerNode: innerModal.current,
currentActiveNode: target,
event: evt
});
}
}
}
function handleOnClick(evt) {
const {
target
} = evt;
evt.stopPropagation();
const shouldCloseOnOutsideClick =
// Passive modals can close on clicks outside the modal when
// preventCloseOnClickOutside is undefined or explicitly set to false.
passiveModal && !preventCloseOnClickOutside ||
// Non-passive modals have to explicitly opt-in for close on outside
// behavior by explicitly setting preventCloseOnClickOutside to false,
// rather than just leaving it undefined.
!passiveModal && preventCloseOnClickOutside === false;
if (shouldCloseOnOutsideClick && target instanceof Node && !wrapFocus.elementOrParentIsFloatingMenu(target, selectorsFloatingMenus, prefix) && innerModal.current && !innerModal.current.contains(target)) {
onRequestClose(evt);
}
}
function handleBlur({
target: oldActiveNode,
relatedTarget: currentActiveNode
}) {
if (!enableDialogElement && open && oldActiveNode instanceof HTMLElement && currentActiveNode instanceof HTMLElement) {
const {
current: bodyNode
} = innerModal;
const {
current: startTrapNode
} = startTrap;
const {
current: endTrapNode
} = endTrap;
// use setTimeout to ensure focus is set after all browser default focus behavior. Fixes issue of
// focus not wrapping in Firefox
wrapFocusTimeout.current = setTimeout(() => {
wrapFocus.wrapFocus({
bodyNode,
startTrapNode,
endTrapNode,
currentActiveNode,
oldActiveNode,
selectorsFloatingMenus,
prefix
});
if (wrapFocusTimeout.current) {
clearTimeout(wrapFocusTimeout.current);
}
});
}
// Adjust scroll if needed so that element with focus is not obscured by gradient
const modalContent = document.querySelector(`.${prefix}--modal-content`);
if (!modalContent || !modalContent.classList.contains(`${prefix}--modal-scroll-content`) || !currentActiveNode || !modalContent.contains(currentActiveNode)) {
return;
}
currentActiveNode.scrollIntoView({
block: 'center'
});
}
const onSecondaryButtonClick = onSecondarySubmit ? onSecondarySubmit : onRequestClose;
const {
height
} = useResizeObserver.useResizeObserver({
ref: contentRef
});
const modalClasses = cx(`${prefix}--modal`, {
[`${prefix}--modal-tall`]: !passiveModal,
'is-visible': enablePresence || open,
[`${prefix}--modal--enable-presence`]: presenceContext?.autoEnablePresence,
[`${prefix}--modal--danger`]: danger,
[`${prefix}--modal--slug`]: slug,
[`${prefix}--modal--decorator`]: decorator
}, className);
const containerClasses = cx(`${prefix}--modal-container`, {
[`${prefix}--modal-container--${size}`]: size,
[`${prefix}--modal-container--full-width`]: isFullWidth
});
/**
* isScrollable is implicitly dependent on height, when height gets updated
* via `useResizeObserver`, clientHeight and scrollHeight get updated too
*/
const isScrollable = !!contentRef.current && contentRef?.current?.scrollHeight > contentRef?.current?.clientHeight;
const contentClasses = cx(`${prefix}--modal-content`, {
[`${prefix}--modal-scroll-content`]: hasScrollingContent || isScrollable,
[`${prefix}--modal-scroll-content--no-fade`]: height <= 300
});
const footerClasses = cx(`${prefix}--modal-footer`, {
[`${prefix}--modal-footer--three-button`]: Array.isArray(secondaryButtons) && secondaryButtons.length === 2
});
const asStringOrUndefined = node => {
return typeof node === 'string' ? node : undefined;
};
const modalLabelStr = asStringOrUndefined(modalLabel);
const modalHeadingStr = asStringOrUndefined(modalHeading);
const ariaLabel = modalLabelStr || ariaLabelProp || modalAriaLabel || modalHeadingStr;
const getAriaLabelledBy = modalLabel ? modalLabelId : modalHeadingId;
const hasScrollingContentProps = hasScrollingContent || isScrollable ? {
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;
}
React.useEffect(() => {
if (!open) return;
const handleEscapeKey = event => {
if (match.match(event, keys.Escape)) {
event.preventDefault();
event.stopPropagation();
onRequestClose(event);
}
};
document.addEventListener('keydown', handleEscapeKey, true);
return () => {
document.removeEventListener('keydown', handleEscapeKey, true);
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- https://github.com/carbon-design-system/carbon/issues/20452
}, [open]);
React.useEffect(() => {
return () => {
if (!enableDialogElement) {
toggleClass.toggleClass(document.body, `${prefix}--body--with-modal-open`, false);
}
};
}, [prefix, enableDialogElement]);
React.useEffect(() => {
if (!enableDialogElement) {
toggleClass.toggleClass(document.body, `${prefix}--body--with-modal-open`, open ?? false);
}
}, [open, prefix, enableDialogElement]);
React.useEffect(() => {
if (!enableDialogElement && !enablePresence && prevOpen && !open && launcherButtonRef) {
setTimeout(() => {
if ('current' in launcherButtonRef) {
launcherButtonRef.current?.focus();
}
});
}
}, [open, prevOpen, launcherButtonRef, enableDialogElement, enablePresence]);
// Focus launcherButtonRef on unmount
React.useEffect(() => {
const launcherButton = launcherButtonRef?.current;
return () => {
if (enablePresence && launcherButton) {
setTimeout(() => {
launcherButton.focus();
});
}
};
}, [enablePresence, launcherButtonRef]);
React.useEffect(() => {
if (!enableDialogElement) {
const initialFocus = focusContainerElement => {
const containerElement = focusContainerElement || innerModal.current;
const primaryFocusElement = containerElement && (containerElement.querySelector(selectorPrimaryFocus) || danger && containerElement.querySelector(`.${prefix}--btn--secondary`));
if (primaryFocusElement) {
return primaryFocusElement;
}
return button && button.current;
};
const focusButton = focusContainerElement => {
const target = initialFocus(focusContainerElement);
if (target !== null) {
target.focus();
}
};
if (open) {
focusButton(innerModal.current);
}
}
}, [open, selectorPrimaryFocus, danger, prefix, enableDialogElement]);
// AILabel always size `sm`
const candidate = slug ?? decorator;
const candidateIsAILabel = utils.isComponentElement(candidate, index$1.AILabel);
const normalizedDecorator = candidateIsAILabel ? /*#__PURE__*/React.cloneElement(candidate, {
size: 'sm'
}) : candidate;
const modalButton = /*#__PURE__*/React.createElement("div", {
className: `${prefix}--modal-close-button`
}, /*#__PURE__*/React.createElement(index$2.IconButton, {
className: modalCloseButtonClass,
label: closeButtonLabel,
onClick: onRequestClose,
"aria-label": closeButtonLabel,
align: "left",
ref: button
}, /*#__PURE__*/React.createElement(iconsReact.Close, {
size: 20,
"aria-hidden": "true",
tabIndex: "-1",
className: `${modalCloseButtonClass}__icon`
})));
// alertdialog is the only permitted aria role for a native dialog element
// https://www.w3.org/TR/html-aria/#docconformance:~:text=Role%3A-,alertdialog,-.%20(dialog%20is
const isAlertDialog = alert && !passiveModal;
const modalBody = enableDialogElement ? /*#__PURE__*/React.createElement(Dialog.Dialog, {
open: open,
focusAfterCloseRef: launcherButtonRef,
modal: true,
ref: innerModal,
role: isAlertDialog ? 'alertdialog' : '',
"aria-describedby": isAlertDialog ? modalBodyId : '',
className: containerClasses,
"aria-label": ariaLabel,
"data-exiting": presenceContext?.isExiting || undefined
}, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--modal-header`
}, modalLabel && /*#__PURE__*/React.createElement(Text.Text, {
as: "h2",
id: modalLabelId,
className: `${prefix}--modal-header__label`
}, modalLabel), /*#__PURE__*/React.createElement(Text.Text, {
as: "h2",
id: modalHeadingId,
className: `${prefix}--modal-header__heading`
}, modalHeading), decorator ? /*#__PURE__*/React.createElement("div", {
className: `${prefix}--modal--inner__decorator`
}, normalizedDecorator) : '', /*#__PURE__*/React.createElement("div", {
className: `${prefix}--modal-close-button`
}, /*#__PURE__*/React.createElement(index$2.IconButton, {
className: modalCloseButtonClass,
label: closeButtonLabel,
onClick: onRequestClose,
"aria-label": closeButtonLabel,
align: "left",
ref: button
}, /*#__PURE__*/React.createElement(iconsReact.Close, {
size: 20,
"aria-hidden": "true",
tabIndex: "-1",
className: `${modalCloseButtonClass}__icon`
})))), /*#__PURE__*/React.createElement(index$3.Layer, _rollupPluginBabelHelpers.extends({
ref: contentRef,
id: modalBodyId,
className: contentClasses
}, hasScrollingContentProps), children), !passiveModal && /*#__PURE__*/React.createElement(ButtonSet.default, {
className: footerClasses,
"aria-busy": loadingActive
}, Array.isArray(secondaryButtons) && secondaryButtons.length <= 2 ? secondaryButtons.map(({
buttonText,
onClick: onButtonClick
}, i) => /*#__PURE__*/React.createElement(Button.default, {
key: `${buttonText}-${i}`,
kind: "secondary",
onClick: onButtonClick
}, buttonText)) : secondaryButtonText && /*#__PURE__*/React.createElement(Button.default, {
disabled: loadingActive,
kind: "secondary",
onClick: onSecondaryButtonClick,
ref: secondaryButton
}, secondaryButtonText), /*#__PURE__*/React.createElement(Button.default, {
className: primaryButtonClass,
kind: danger ? 'danger' : 'primary',
disabled: loadingActive || primaryButtonDisabled,
onClick: onRequestSubmit,
ref: button
}, loadingStatus === 'inactive' ? primaryButtonText : /*#__PURE__*/React.createElement(InlineLoading.default, {
status: loadingStatus,
description: loadingDescription,
iconDescription: loadingIconDescription,
className: `${prefix}--inline-loading--btn`,
onSuccess: onLoadingSuccess
})))) : /*#__PURE__*/React.createElement(React.Fragment, null, !enableDialogElement && !focusTrapWithoutSentinels && /*#__PURE__*/React.createElement("span", {
ref: startTrap,
tabIndex: 0,
role: "link",
className: `${prefix}--visually-hidden`
}, "Focus sentinel"), /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({
ref: innerModal,
role: "dialog"
}, alertDialogProps, {
className: containerClasses,
"aria-label": ariaLabel,
"aria-modal": "true",
tabIndex: -1
}), /*#__PURE__*/React.createElement("div", {
className: `${prefix}--modal-header`
}, passiveModal && modalButton, modalLabel && /*#__PURE__*/React.createElement(Text.Text, {
as: "h2",
id: modalLabelId,
className: `${prefix}--modal-header__label`
}, modalLabel), /*#__PURE__*/React.createElement(Text.Text, {
as: "h2",
id: modalHeadingId,
className: `${prefix}--modal-header__heading`
}, modalHeading), slug ? normalizedDecorator : decorator ? /*#__PURE__*/React.createElement("div", {
className: `${prefix}--modal--inner__decorator`
}, normalizedDecorator) : '', !passiveModal && modalButton), /*#__PURE__*/React.createElement(index$3.Layer, _rollupPluginBabelHelpers.extends({
ref: contentRef,
id: modalBodyId,
className: contentClasses
}, hasScrollingContentProps), children), !passiveModal && /*#__PURE__*/React.createElement(ButtonSet.default, {
className: footerClasses,
"aria-busy": loadingActive
}, Array.isArray(secondaryButtons) && secondaryButtons.length <= 2 ? secondaryButtons.map(({
buttonText,
onClick: onButtonClick
}, i) => /*#__PURE__*/React.createElement(Button.default, {
key: `${buttonText}-${i}`,
kind: "secondary",
onClick: onButtonClick
}, buttonText)) : secondaryButtonText && /*#__PURE__*/React.createElement(Button.default, {
disabled: loadingActive,
kind: "secondary",
onClick: onSecondaryButtonClick,
ref: secondaryButton
}, secondaryButtonText), /*#__PURE__*/React.createElement(Button.default, {
className: primaryButtonClass,
kind: danger ? 'danger' : 'primary',
disabled: loadingActive || primaryButtonDisabled,
onClick: onRequestSubmit,
ref: button
}, loadingStatus === 'inactive' ? primaryButtonText : /*#__PURE__*/React.createElement(InlineLoading.default, {
status: loadingStatus,
description: loadingDescription,
iconDescription: loadingIconDescription,
className: `${prefix}--inline-loading--btn`,
onSuccess: onLoadingSuccess
})))), !enableDialogElement && !focusTrapWithoutSentinels && /*#__PURE__*/React.createElement("span", {
ref: endTrap,
tabIndex: 0,
role: "link",
className: `${prefix}--visually-hidden`
}, "Focus sentinel"));
return /*#__PURE__*/React.createElement(index$3.Layer, _rollupPluginBabelHelpers.extends({}, rest, {
level: 0,
onKeyDown: handleKeyDown,
onClick: events.composeEventHandlers([rest?.onClick, handleOnClick]),
onBlur: handleBlur,
className: modalClasses,
role: "presentation",
ref: mergedRefs,
"data-exiting": presenceContext?.isExiting || undefined
}), modalBody);
});
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.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 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,
/**
* **Experimental**: Provide a decorator component to be rendered inside the `Modal` component
*/
decorator: PropTypes.node,
/**
* 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,
/**
* Provide a ref to return focus to once the modal is closed.
*/
launcherButtonRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({
current: PropTypes.oneOfType([
// `PropTypes.instanceOf(HTMLButtonElement)` alone won't work because
// `HTMLButtonElement` is not defined in the test environment even
// though `testEnvironment` is set to `jsdom`.
typeof HTMLButtonElement !== 'undefined' ? PropTypes.instanceOf(HTMLButtonElement) : PropTypes.any, PropTypes.oneOf([null])]).isRequired
})]),
/**
* Specify the description for the loading text
*/
loadingDescription: PropTypes.string,
/**
* Specify the description for the loading text
*/
loadingIconDescription: PropTypes.string,
/**
* loading status
*/
loadingStatus: PropTypes.oneOf(['inactive', 'active', 'finished', 'error']),
/**
* 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,
/**
* Provide an optional handler to be invoked when loading is
* successful
*/
onLoadingSuccess: 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: (props, propName) => {
if (!props.passiveModal && props[propName] === false) {
return new Error(invalidOutsideClickMessage);
}
return null;
},
/**
* Specify whether the Button should be disabled, or not
*/
primaryButtonDisabled: PropTypes.bool,
/**
* Specify the text for the primary button
*/
primaryButtonText: PropTypes.node,
/**
* Specify the text for the secondary button
*/
secondaryButtonText: PropTypes.node,
/**
* Specify an array of config objects for secondary buttons
* (`Array<{
* buttonText: string,
* onClick: function,
* }>`).
*/
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.isRequired),
/**
* Specify if Enter key should be used as "submit" action
*/
shouldSubmitOnEnter: PropTypes.bool,
/**
* Specify the size variant.
*/
size: PropTypes.oneOf(ModalSizes),
slug: deprecate.deprecate(PropTypes.node, 'The `slug` prop has been deprecated and will be removed in the next major version. Use the decorator prop instead.')
};
exports.ModalSizes = ModalSizes;
exports.default = Modal;