UNPKG

@reach/dialog

Version:

Accessible React Modal Dialog.

349 lines (297 loc) 12.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var React = require('react'); var portal = require('@reach/portal'); var utils = require('@reach/utils'); var FocusLock = require('react-focus-lock'); var reactRemoveScroll = require('react-remove-scroll'); var PropTypes = require('prop-types'); function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; } var FocusLock__default = /*#__PURE__*/_interopDefault(FocusLock); var PropTypes__default = /*#__PURE__*/_interopDefault(PropTypes); function _extends() { _extends = Object.assign || 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); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } var overlayPropTypes = { allowPinchZoom: PropTypes__default['default'].bool, dangerouslyBypassFocusLock: PropTypes__default['default'].bool, dangerouslyBypassScrollLock: PropTypes__default['default'].bool, // TODO: initialFocusRef: function initialFocusRef() { return null; }, onDismiss: PropTypes__default['default'].func }; //////////////////////////////////////////////////////////////////////////////// /** * DialogOverlay * * Low-level component if you need more control over the styles or rendering of * the dialog overlay. * * Note: You must render a `DialogContent` inside. * * @see Docs https://reach.tech/dialog#dialogoverlay */ var DialogOverlay = /*#__PURE__*/utils.forwardRefWithAs(function DialogOverlay(_ref, forwardedRef) { var _ref$as = _ref.as, Comp = _ref$as === void 0 ? "div" : _ref$as, _ref$isOpen = _ref.isOpen, isOpen = _ref$isOpen === void 0 ? true : _ref$isOpen, props = _objectWithoutPropertiesLoose(_ref, ["as", "isOpen"]); utils.useCheckStyles("dialog"); // We want to ignore the immediate focus of a tooltip so it doesn't pop // up again when the menu closes, only pops up when focus returns again // to the tooltip (like native OS tooltips). React.useEffect(function () { if (isOpen) { // @ts-ignore window.__REACH_DISABLE_TOOLTIPS = true; } else { window.requestAnimationFrame(function () { // Wait a frame so that this doesn't fire before tooltip does // @ts-ignore window.__REACH_DISABLE_TOOLTIPS = false; }); } }, [isOpen]); return isOpen ? /*#__PURE__*/React.createElement(portal.Portal, { "data-reach-dialog-wrapper": "" }, /*#__PURE__*/React.createElement(DialogInner, _extends({ ref: forwardedRef, as: Comp }, props))) : null; }); if (process.env.NODE_ENV !== "production") { DialogOverlay.displayName = "DialogOverlay"; DialogOverlay.propTypes = /*#__PURE__*/_extends({}, overlayPropTypes, { isOpen: PropTypes__default['default'].bool }); } //////////////////////////////////////////////////////////////////////////////// /** * DialogInner */ var DialogInner = /*#__PURE__*/utils.forwardRefWithAs(function DialogInner(_ref2, forwardedRef) { var allowPinchZoom = _ref2.allowPinchZoom, _ref2$as = _ref2.as, Comp = _ref2$as === void 0 ? "div" : _ref2$as, _ref2$dangerouslyBypa = _ref2.dangerouslyBypassFocusLock, dangerouslyBypassFocusLock = _ref2$dangerouslyBypa === void 0 ? false : _ref2$dangerouslyBypa, _ref2$dangerouslyBypa2 = _ref2.dangerouslyBypassScrollLock, dangerouslyBypassScrollLock = _ref2$dangerouslyBypa2 === void 0 ? false : _ref2$dangerouslyBypa2, initialFocusRef = _ref2.initialFocusRef, onClick = _ref2.onClick, _ref2$onDismiss = _ref2.onDismiss, onDismiss = _ref2$onDismiss === void 0 ? utils.noop : _ref2$onDismiss, onKeyDown = _ref2.onKeyDown, onMouseDown = _ref2.onMouseDown, _ref2$unstable_lockFo = _ref2.unstable_lockFocusAcrossFrames, unstable_lockFocusAcrossFrames = _ref2$unstable_lockFo === void 0 ? true : _ref2$unstable_lockFo, props = _objectWithoutPropertiesLoose(_ref2, ["allowPinchZoom", "as", "dangerouslyBypassFocusLock", "dangerouslyBypassScrollLock", "initialFocusRef", "onClick", "onDismiss", "onKeyDown", "onMouseDown", "unstable_lockFocusAcrossFrames"]); var mouseDownTarget = React.useRef(null); var overlayNode = React.useRef(null); var ref = utils.useForkedRef(overlayNode, forwardedRef); var activateFocusLock = React.useCallback(function () { if (initialFocusRef && initialFocusRef.current) { initialFocusRef.current.focus(); } }, [initialFocusRef]); function handleClick(event) { if (mouseDownTarget.current === event.target) { event.stopPropagation(); onDismiss(event); } } function handleKeyDown(event) { if (event.key === "Escape") { event.stopPropagation(); onDismiss(event); } } function handleMouseDown(event) { mouseDownTarget.current = event.target; } React.useEffect(function () { return overlayNode.current ? createAriaHider(overlayNode.current) : void null; }, []); return /*#__PURE__*/React.createElement(FocusLock__default['default'], { autoFocus: true, returnFocus: true, onActivation: activateFocusLock, disabled: dangerouslyBypassFocusLock, crossFrame: unstable_lockFocusAcrossFrames }, /*#__PURE__*/React.createElement(reactRemoveScroll.RemoveScroll, { allowPinchZoom: allowPinchZoom, enabled: !dangerouslyBypassScrollLock }, /*#__PURE__*/React.createElement(Comp, _extends({}, props, { ref: ref, "data-reach-dialog-overlay": "" /* * We can ignore the `no-static-element-interactions` warning here * because our overlay is only designed to capture any outside * clicks, not to serve as a clickable element itself. */ , onClick: utils.wrapEvent(onClick, handleClick), onKeyDown: utils.wrapEvent(onKeyDown, handleKeyDown), onMouseDown: utils.wrapEvent(onMouseDown, handleMouseDown) })))); }); if (process.env.NODE_ENV !== "production") { DialogOverlay.displayName = "DialogOverlay"; DialogOverlay.propTypes = /*#__PURE__*/_extends({}, overlayPropTypes); } //////////////////////////////////////////////////////////////////////////////// /** * DialogContent * * Low-level component if you need more control over the styles or rendering of * the dialog content. * * Note: Must be a child of `DialogOverlay`. * * Note: You only need to use this when you are also styling `DialogOverlay`, * otherwise you can use the high-level `Dialog` component and pass the props * to it. Any props passed to `Dialog` component (besides `isOpen` and * `onDismiss`) will be spread onto `DialogContent`. * * @see Docs https://reach.tech/dialog#dialogcontent */ var DialogContent = /*#__PURE__*/utils.forwardRefWithAs(function DialogContent(_ref3, forwardedRef) { var _ref3$as = _ref3.as, Comp = _ref3$as === void 0 ? "div" : _ref3$as, onClick = _ref3.onClick; _ref3.onKeyDown; var props = _objectWithoutPropertiesLoose(_ref3, ["as", "onClick", "onKeyDown"]); return /*#__PURE__*/React.createElement(Comp, _extends({ "aria-modal": "true", role: "dialog", tabIndex: -1 }, props, { ref: forwardedRef, "data-reach-dialog-content": "", onClick: utils.wrapEvent(onClick, function (event) { event.stopPropagation(); }) })); }); /** * @see Docs https://reach.tech/dialog#dialogcontent-props */ if (process.env.NODE_ENV !== "production") { DialogContent.displayName = "DialogContent"; DialogContent.propTypes = { "aria-label": ariaLabelType, "aria-labelledby": ariaLabelType }; } //////////////////////////////////////////////////////////////////////////////// /** * Dialog * * High-level component to render a modal dialog window over the top of the page * (or another dialog). * * @see Docs https://reach.tech/dialog#dialog */ var Dialog = /*#__PURE__*/utils.forwardRefWithAs(function Dialog(_ref4, forwardedRef) { var _ref4$allowPinchZoom = _ref4.allowPinchZoom, allowPinchZoom = _ref4$allowPinchZoom === void 0 ? false : _ref4$allowPinchZoom, initialFocusRef = _ref4.initialFocusRef, isOpen = _ref4.isOpen, _ref4$onDismiss = _ref4.onDismiss, onDismiss = _ref4$onDismiss === void 0 ? utils.noop : _ref4$onDismiss, props = _objectWithoutPropertiesLoose(_ref4, ["allowPinchZoom", "initialFocusRef", "isOpen", "onDismiss"]); return /*#__PURE__*/React.createElement(DialogOverlay, { allowPinchZoom: allowPinchZoom, initialFocusRef: initialFocusRef, isOpen: isOpen, onDismiss: onDismiss }, /*#__PURE__*/React.createElement(DialogContent, _extends({ ref: forwardedRef }, props))); }); /** * @see Docs https://reach.tech/dialog#dialog-props */ if (process.env.NODE_ENV !== "production") { Dialog.displayName = "Dialog"; Dialog.propTypes = { isOpen: PropTypes__default['default'].bool, onDismiss: PropTypes__default['default'].func, "aria-label": ariaLabelType, "aria-labelledby": ariaLabelType }; } //////////////////////////////////////////////////////////////////////////////// function createAriaHider(dialogNode) { var originalValues = []; var rootNodes = []; var ownerDocument = utils.getOwnerDocument(dialogNode); if (!dialogNode) { if (process.env.NODE_ENV !== "production") { console.warn("A ref has not yet been attached to a dialog node when attempting to call `createAriaHider`."); } return utils.noop; } Array.prototype.forEach.call(ownerDocument.querySelectorAll("body > *"), function (node) { var _dialogNode$parentNod, _dialogNode$parentNod2; var portalNode = (_dialogNode$parentNod = dialogNode.parentNode) == null ? void 0 : (_dialogNode$parentNod2 = _dialogNode$parentNod.parentNode) == null ? void 0 : _dialogNode$parentNod2.parentNode; if (node === portalNode) { return; } var attr = node.getAttribute("aria-hidden"); var alreadyHidden = attr !== null && attr !== "false"; if (alreadyHidden) { return; } originalValues.push(attr); rootNodes.push(node); node.setAttribute("aria-hidden", "true"); }); return function () { rootNodes.forEach(function (node, index) { var originalValue = originalValues[index]; if (originalValue === null) { node.removeAttribute("aria-hidden"); } else { node.setAttribute("aria-hidden", originalValue); } }); }; } function ariaLabelType(props, propName, compName, location, propFullName) { var details = "\nSee https://www.w3.org/TR/wai-aria/#aria-label for details."; if (!props["aria-label"] && !props["aria-labelledby"]) { return new Error("A <" + compName + "> must have either an `aria-label` or `aria-labelledby` prop.\n " + details); } if (props["aria-label"] && props["aria-labelledby"]) { return new Error("You provided both `aria-label` and `aria-labelledby` props to a <" + compName + ">. If the a label for this component is visible on the screen, that label's component should be given a unique ID prop, and that ID should be passed as the `aria-labelledby` prop into <" + compName + ">. If the label cannot be determined programmatically from the content of the element, an alternative label should be provided as the `aria-label` prop, which will be used as an `aria-label` on the HTML tag." + details); } else if (props[propName] != null && !utils.isString(props[propName])) { return new Error("Invalid prop `" + propName + "` supplied to `" + compName + "`. Expected `string`, received `" + (Array.isArray(propFullName) ? "array" : typeof propFullName) + "`."); } return null; } //////////////////////////////////////////////////////////////////////////////// exports.Dialog = Dialog; exports.DialogContent = DialogContent; exports.DialogOverlay = DialogOverlay; exports.default = Dialog;