@mapbox/mr-ui
Version:
UI components for Mapbox projects
236 lines (234 loc) • 9.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = Modal;
var _react = _interopRequireDefault(require("react"));
var _classnames = _interopRequireDefault(require("classnames"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var DialogPrimitive = _interopRequireWildcard(require("@radix-ui/react-dialog"));
var VisuallyHidden = _interopRequireWildcard(require("@radix-ui/react-visually-hidden"));
var _tooltip = _interopRequireDefault(require("../tooltip"));
var _icon = _interopRequireDefault(require("../icon"));
var _modalActions = _interopRequireDefault(require("./modal-actions"));
var _eventTrap = _interopRequireDefault(require("./event-trap"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
/**
* An accessible modal dialog.
*
* To get a standard button arrangement at the bottom of the modal, use the
* `primaryAction`, `secondaryAction`, and `tertiaryAction` props.
*
* This modal *traps focus within it*. You should be aware of that, because it
* can sometimes introduce a hurdle when integrating the modal with other
* things, especially third-party libraries. But it's an essential UX feature.
*/
function Modal(_ref) {
let {
children,
accessibleTitle,
size = 'large',
padding = 'large',
margin = 'default',
zIndex = 'auto',
allowEventBubbling = false,
exitOnUnderlayClicked = true,
initialFocus,
primaryAction,
secondaryAction,
tertiaryAction,
themeOverlay = '',
themeContent = '',
onExit
} = _ref;
const renderActions = () => {
if (!primaryAction) {
return null;
}
return /*#__PURE__*/_react.default.createElement("div", {
className: "mt24"
}, /*#__PURE__*/_react.default.createElement(_modalActions.default, {
primaryAction: primaryAction,
secondaryAction: secondaryAction,
tertiaryAction: tertiaryAction
}));
};
let widthClass = 'wmax-full';
if (size === 'small') {
widthClass = 'wmax360 w-11/12';
} else if (size === 'large') {
widthClass = 'wmax600 w-11/12';
}
let marginClass = 'my12 my60-mm';
if (margin === 'large') {
marginClass = 'my120';
}
const overlayProps = {
className: `fixed top bottom left right bg-darken50 ${themeOverlay}`,
style: {
display: 'grid',
overflowY: 'auto',
placeItems: 'start center',
zIndex
},
'data-testid': 'modal-overlay'
};
const rootProps = {
defaultOpen: true
};
if (onExit && exitOnUnderlayClicked) {
rootProps.onOpenChange = onExit;
}
const contentProps = {
className: (0, _classnames.default)(`relative ${marginClass} ${widthClass} bg-white round ${themeContent}`, {
'px36 py36': padding === 'large'
})
};
if (initialFocus) {
contentProps.onOpenAutoFocus = e => {
const el = document.querySelector(initialFocus);
if (el !== null) {
e.preventDefault();
el.focus();
}
};
}
const modal = /*#__PURE__*/_react.default.createElement(DialogPrimitive.Root, _extends({}, rootProps, {
open: true
}), /*#__PURE__*/_react.default.createElement(DialogPrimitive.Portal, null, /*#__PURE__*/_react.default.createElement(DialogPrimitive.Overlay, overlayProps, /*#__PURE__*/_react.default.createElement(DialogPrimitive.Content, contentProps, /*#__PURE__*/_react.default.createElement(VisuallyHidden.Root, null, /*#__PURE__*/_react.default.createElement(DialogPrimitive.Title, null, accessibleTitle)), children, renderActions(), onExit && /*#__PURE__*/_react.default.createElement(_tooltip.default, {
content: "Close"
}, /*#__PURE__*/_react.default.createElement("button", {
onClick: onExit,
type: "button",
"data-testid": "modal-close",
"aria-label": "Close",
className: "btn btn--transparent unround-t unround-br color-gray py12 px12 absolute top right"
}, /*#__PURE__*/_react.default.createElement(_icon.default, {
name: "close"
})))))));
if (!allowEventBubbling) {
return /*#__PURE__*/_react.default.createElement(_eventTrap.default, null, modal);
}
return modal;
}
Modal.propTypes = {
/**
* A screen-reader-friendly modal title. Required for accessibility.
*
* This **will not be displayed.** It's only for screen readers.
* You can visually display your own header text however you'd like.
*/
accessibleTitle: _propTypes.default.string.isRequired,
/**
* Invoked when the modal should close. When this callback is provided,
* a close button will be in the top right corner, and a click on the underlay
* or the Escape key will close the modal.
*
* If this prop is not provided, the close button will not be present and
* a click on the underlay or Escape will not close the modal. The reason
* you might not provide this function is that you want to force the user
* to do something, instead of allowing them to sneak out of the modal.
*/
onExit: _propTypes.default.func,
/**
* If `onExit` is provided but this prop is set as false, a click on the underlay will
* not close the modal. The only way of closing the modal is clicking on the close button.
*/
exitOnUnderlayClicked: _propTypes.default.bool,
/**
* Modal container size. Options are `small`, `large`, or `auto`. If `auto`
* is provided, a width is not specified.
*/
size: _propTypes.default.oneOf(['small', 'large', 'auto']),
/**
* Selector for a specific element that should receive initial focus. The
* value will be passed to `querySelector`.
*/
initialFocus: _propTypes.default.string,
/**
* The content of the modal.
*/
children: _propTypes.default.node.isRequired,
/**
* `'large'` or `'none'`.
*/
padding: _propTypes.default.oneOf(['large', 'none']),
/**
* `'large'` to compensate for a fixed top header or `'default'` to be closer to the top of the viewport.
*/
margin: _propTypes.default.oneOf(['large', 'default']),
/**
* z-index of the modal
*/
zIndex: _propTypes.default.number,
/**
* Additional CSS classes to apply to the overlay,
* along with existing classes `fixed top bottom left right bg-darken50`
*/
themeOverlay: _propTypes.default.string,
/**
* Additional CSS classes to apply to the content,
* along with existing classes `relative bg-white round`
*/
themeContent: _propTypes.default.string,
/**
* The modal's primary action. If this is provided, an encouraging
* button will be rendered at the bottom of the modal.
*
* Provide this and other action props if you want a standard button
* arrangement at the bottom of the modal. If you need a more custom
* arrangement, leave them out and insert your buttons into the content.
*
* The value is an object with the following properties:
* - `text`: **(required)** The text of the button.
* - `callback`: **(required)** Invoked when the button is clicked.
* - `destructive`: If `true`, the [Button](#button) will be primed for
* desctruction.
*/
primaryAction: _propTypes.default.shape({
text: _propTypes.default.string.isRequired,
callback: _propTypes.default.func.isRequired,
destructive: _propTypes.default.bool
}),
/**
* The modal's secondary action. If this is provided, a discouraging button
* will be rendered at the bottom of the modal. See the description of
* `primaryAction`.
*
* **Can only be used in combination with `primaryAction`.**
*
* The value is an object with the following properties:
* - `text`: **(required)** The text of the button.
* - `callback`: **(required)** Invoked when the button is clicked.
*/
secondaryAction: _propTypes.default.shape({
text: _propTypes.default.string.isRequired,
callback: _propTypes.default.func.isRequired
}),
/**
* The modal's tertiary action. **You should rarely if ever need this.**
* If this is provided, a *very* discouraging button
* will be rendered at the bottom of the modal. See the description of
* `primaryAction`.
*
* **Can only be used in combination with `primaryAction` and
* `secondaryAction`.**
*
* The value is an object with the following properties:
* - `text`: **(required)** The text of the button.
* - `callback`: **(required)** Invoked when the button is clicked.
*/
tertiaryAction: _propTypes.default.shape({
text: _propTypes.default.string.isRequired,
callback: _propTypes.default.func.isRequired
}),
/**
* We stop propagation on clicks by default to enable more intuitive
* operation with React Portals. If you need click events to bubble up
* to parent components, set this prop to true
*/
allowEventBubbling: _propTypes.default.bool
};