UNPKG

13.2 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5var React = require('react');
6var portal = require('@reach/portal');
7var ownerDocument = require('@reach/utils/owner-document');
8var typeCheck = require('@reach/utils/type-check');
9var noop = require('@reach/utils/noop');
10var devUtils = require('@reach/utils/dev-utils');
11var composeRefs = require('@reach/utils/compose-refs');
12var composeEventHandlers = require('@reach/utils/compose-event-handlers');
13var FocusLock = require('react-focus-lock');
14var reactRemoveScroll = require('react-remove-scroll');
15var PropTypes = require('prop-types');
16
17function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
18
19var FocusLock__default = /*#__PURE__*/_interopDefault(FocusLock);
20var PropTypes__default = /*#__PURE__*/_interopDefault(PropTypes);
21
22function _extends() {
23 _extends = Object.assign || function (target) {
24 for (var i = 1; i < arguments.length; i++) {
25 var source = arguments[i];
26
27 for (var key in source) {
28 if (Object.prototype.hasOwnProperty.call(source, key)) {
29 target[key] = source[key];
30 }
31 }
32 }
33
34 return target;
35 };
36
37 return _extends.apply(this, arguments);
38}
39
40function _objectWithoutPropertiesLoose(source, excluded) {
41 if (source == null) return {};
42 var target = {};
43 var sourceKeys = Object.keys(source);
44 var key, i;
45
46 for (i = 0; i < sourceKeys.length; i++) {
47 key = sourceKeys[i];
48 if (excluded.indexOf(key) >= 0) continue;
49 target[key] = source[key];
50 }
51
52 return target;
53}
54
55var _excluded = ["as", "isOpen"],
56 _excluded2 = ["allowPinchZoom", "as", "dangerouslyBypassFocusLock", "dangerouslyBypassScrollLock", "initialFocusRef", "onClick", "onDismiss", "onKeyDown", "onMouseDown", "unstable_lockFocusAcrossFrames"],
57 _excluded3 = ["as", "onClick", "onKeyDown"],
58 _excluded4 = ["allowPinchZoom", "initialFocusRef", "isOpen", "onDismiss"];
59var overlayPropTypes = {
60 allowPinchZoom: PropTypes__default['default'].bool,
61 dangerouslyBypassFocusLock: PropTypes__default['default'].bool,
62 dangerouslyBypassScrollLock: PropTypes__default['default'].bool,
63 // TODO:
64 initialFocusRef: function initialFocusRef() {
65 return null;
66 },
67 onDismiss: PropTypes__default['default'].func
68}; ////////////////////////////////////////////////////////////////////////////////
69
70/**
71 * DialogOverlay
72 *
73 * Low-level component if you need more control over the styles or rendering of
74 * the dialog overlay.
75 *
76 * Note: You must render a `DialogContent` inside.
77 *
78 * @see Docs https://reach.tech/dialog#dialogoverlay
79 */
80
81var DialogOverlay = /*#__PURE__*/React.forwardRef(function DialogOverlay(_ref, forwardedRef) {
82 var _ref$as = _ref.as,
83 Comp = _ref$as === void 0 ? "div" : _ref$as,
84 _ref$isOpen = _ref.isOpen,
85 isOpen = _ref$isOpen === void 0 ? true : _ref$isOpen,
86 props = _objectWithoutPropertiesLoose(_ref, _excluded);
87
88 devUtils.useCheckStyles("dialog"); // We want to ignore the immediate focus of a tooltip so it doesn't pop
89 // up again when the menu closes, only pops up when focus returns again
90 // to the tooltip (like native OS tooltips).
91
92 React.useEffect(function () {
93 if (isOpen) {
94 // @ts-ignore
95 window.__REACH_DISABLE_TOOLTIPS = true;
96 } else {
97 window.requestAnimationFrame(function () {
98 // Wait a frame so that this doesn't fire before tooltip does
99 // @ts-ignore
100 window.__REACH_DISABLE_TOOLTIPS = false;
101 });
102 }
103 }, [isOpen]);
104 return isOpen ? /*#__PURE__*/React.createElement(portal.Portal, {
105 "data-reach-dialog-wrapper": ""
106 }, /*#__PURE__*/React.createElement(DialogInner, _extends({
107 ref: forwardedRef,
108 as: Comp
109 }, props))) : null;
110});
111
112if (process.env.NODE_ENV !== "production") {
113 DialogOverlay.displayName = "DialogOverlay";
114 DialogOverlay.propTypes = /*#__PURE__*/_extends({}, overlayPropTypes, {
115 isOpen: PropTypes__default['default'].bool
116 });
117}
118
119////////////////////////////////////////////////////////////////////////////////
120
121/**
122 * DialogInner
123 */
124var DialogInner = /*#__PURE__*/React.forwardRef(function DialogInner(_ref2, forwardedRef) {
125 var allowPinchZoom = _ref2.allowPinchZoom,
126 _ref2$as = _ref2.as,
127 Comp = _ref2$as === void 0 ? "div" : _ref2$as,
128 _ref2$dangerouslyBypa = _ref2.dangerouslyBypassFocusLock,
129 dangerouslyBypassFocusLock = _ref2$dangerouslyBypa === void 0 ? false : _ref2$dangerouslyBypa,
130 _ref2$dangerouslyBypa2 = _ref2.dangerouslyBypassScrollLock,
131 dangerouslyBypassScrollLock = _ref2$dangerouslyBypa2 === void 0 ? false : _ref2$dangerouslyBypa2,
132 initialFocusRef = _ref2.initialFocusRef,
133 onClick = _ref2.onClick,
134 _ref2$onDismiss = _ref2.onDismiss,
135 onDismiss = _ref2$onDismiss === void 0 ? noop.noop : _ref2$onDismiss,
136 onKeyDown = _ref2.onKeyDown,
137 onMouseDown = _ref2.onMouseDown,
138 unstable_lockFocusAcrossFrames = _ref2.unstable_lockFocusAcrossFrames,
139 props = _objectWithoutPropertiesLoose(_ref2, _excluded2);
140
141 var lockFocusAcrossFramesIsDefined = unstable_lockFocusAcrossFrames !== undefined;
142
143 if (process.env.NODE_ENV !== "production") {
144 // eslint-disable-next-line react-hooks/rules-of-hooks
145 React.useEffect(function () {
146 if (lockFocusAcrossFramesIsDefined) {
147 console.warn("The unstable_lockFocusAcrossFrames in @reach/dialog is deprecated. It will be removed in the next minor release.");
148 }
149 }, [lockFocusAcrossFramesIsDefined]);
150 }
151
152 var mouseDownTarget = React.useRef(null);
153 var overlayNode = React.useRef(null);
154 var ref = composeRefs.useComposedRefs(overlayNode, forwardedRef);
155 var activateFocusLock = React.useCallback(function () {
156 if (initialFocusRef && initialFocusRef.current) {
157 initialFocusRef.current.focus();
158 }
159 }, [initialFocusRef]);
160
161 function handleClick(event) {
162 if (mouseDownTarget.current === event.target) {
163 event.stopPropagation();
164 onDismiss(event);
165 }
166 }
167
168 function handleKeyDown(event) {
169 if (event.key === "Escape") {
170 event.stopPropagation();
171 onDismiss(event);
172 }
173 }
174
175 function handleMouseDown(event) {
176 mouseDownTarget.current = event.target;
177 }
178
179 React.useEffect(function () {
180 return overlayNode.current ? createAriaHider(overlayNode.current) : void null;
181 }, []);
182 return /*#__PURE__*/React.createElement(FocusLock__default['default'], {
183 autoFocus: true,
184 returnFocus: true,
185 onActivation: activateFocusLock,
186 disabled: dangerouslyBypassFocusLock,
187 crossFrame: unstable_lockFocusAcrossFrames != null ? unstable_lockFocusAcrossFrames : true
188 }, /*#__PURE__*/React.createElement(reactRemoveScroll.RemoveScroll, {
189 allowPinchZoom: allowPinchZoom,
190 enabled: !dangerouslyBypassScrollLock
191 }, /*#__PURE__*/React.createElement(Comp, _extends({}, props, {
192 ref: ref,
193 "data-reach-dialog-overlay": ""
194 /*
195 * We can ignore the `no-static-element-interactions` warning here
196 * because our overlay is only designed to capture any outside
197 * clicks, not to serve as a clickable element itself.
198 */
199 ,
200 onClick: composeEventHandlers.composeEventHandlers(onClick, handleClick),
201 onKeyDown: composeEventHandlers.composeEventHandlers(onKeyDown, handleKeyDown),
202 onMouseDown: composeEventHandlers.composeEventHandlers(onMouseDown, handleMouseDown)
203 }))));
204});
205
206if (process.env.NODE_ENV !== "production") {
207 DialogOverlay.displayName = "DialogOverlay";
208 DialogOverlay.propTypes = /*#__PURE__*/_extends({}, overlayPropTypes);
209} ////////////////////////////////////////////////////////////////////////////////
210
211/**
212 * DialogContent
213 *
214 * Low-level component if you need more control over the styles or rendering of
215 * the dialog content.
216 *
217 * Note: Must be a child of `DialogOverlay`.
218 *
219 * Note: You only need to use this when you are also styling `DialogOverlay`,
220 * otherwise you can use the high-level `Dialog` component and pass the props
221 * to it. Any props passed to `Dialog` component (besides `isOpen` and
222 * `onDismiss`) will be spread onto `DialogContent`.
223 *
224 * @see Docs https://reach.tech/dialog#dialogcontent
225 */
226
227
228var DialogContent = /*#__PURE__*/React.forwardRef(function DialogContent(_ref3, forwardedRef) {
229 var _ref3$as = _ref3.as,
230 Comp = _ref3$as === void 0 ? "div" : _ref3$as,
231 onClick = _ref3.onClick;
232 _ref3.onKeyDown;
233 var props = _objectWithoutPropertiesLoose(_ref3, _excluded3);
234
235 return /*#__PURE__*/React.createElement(Comp, _extends({
236 "aria-modal": "true",
237 role: "dialog",
238 tabIndex: -1
239 }, props, {
240 ref: forwardedRef,
241 "data-reach-dialog-content": "",
242 onClick: composeEventHandlers.composeEventHandlers(onClick, function (event) {
243 event.stopPropagation();
244 })
245 }));
246});
247/**
248 * @see Docs https://reach.tech/dialog#dialogcontent-props
249 */
250
251if (process.env.NODE_ENV !== "production") {
252 DialogContent.displayName = "DialogContent";
253 DialogContent.propTypes = {
254 "aria-label": ariaLabelType,
255 "aria-labelledby": ariaLabelType
256 };
257} ////////////////////////////////////////////////////////////////////////////////
258
259/**
260 * Dialog
261 *
262 * High-level component to render a modal dialog window over the top of the page
263 * (or another dialog).
264 *
265 * @see Docs https://reach.tech/dialog#dialog
266 */
267
268
269var Dialog = /*#__PURE__*/React.forwardRef(function Dialog(_ref4, forwardedRef) {
270 var _ref4$allowPinchZoom = _ref4.allowPinchZoom,
271 allowPinchZoom = _ref4$allowPinchZoom === void 0 ? false : _ref4$allowPinchZoom,
272 initialFocusRef = _ref4.initialFocusRef,
273 isOpen = _ref4.isOpen,
274 _ref4$onDismiss = _ref4.onDismiss,
275 onDismiss = _ref4$onDismiss === void 0 ? noop.noop : _ref4$onDismiss,
276 props = _objectWithoutPropertiesLoose(_ref4, _excluded4);
277
278 return /*#__PURE__*/React.createElement(DialogOverlay, {
279 allowPinchZoom: allowPinchZoom,
280 initialFocusRef: initialFocusRef,
281 isOpen: isOpen,
282 onDismiss: onDismiss
283 }, /*#__PURE__*/React.createElement(DialogContent, _extends({
284 ref: forwardedRef
285 }, props)));
286});
287/**
288 * @see Docs https://reach.tech/dialog#dialog-props
289 */
290
291if (process.env.NODE_ENV !== "production") {
292 Dialog.displayName = "Dialog";
293 Dialog.propTypes = {
294 isOpen: PropTypes__default['default'].bool,
295 onDismiss: PropTypes__default['default'].func,
296 "aria-label": ariaLabelType,
297 "aria-labelledby": ariaLabelType
298 };
299} ////////////////////////////////////////////////////////////////////////////////
300
301
302function createAriaHider(dialogNode) {
303 var originalValues = [];
304 var rootNodes = [];
305 var ownerDocument$1 = ownerDocument.getOwnerDocument(dialogNode);
306
307 if (!dialogNode) {
308 if (process.env.NODE_ENV !== "production") {
309 console.warn("A ref has not yet been attached to a dialog node when attempting to call `createAriaHider`.");
310 }
311
312 return noop.noop;
313 }
314
315 Array.prototype.forEach.call(ownerDocument$1.querySelectorAll("body > *"), function (node) {
316 var _dialogNode$parentNod, _dialogNode$parentNod2;
317
318 var portalNode = (_dialogNode$parentNod = dialogNode.parentNode) == null ? void 0 : (_dialogNode$parentNod2 = _dialogNode$parentNod.parentNode) == null ? void 0 : _dialogNode$parentNod2.parentNode;
319
320 if (node === portalNode) {
321 return;
322 }
323
324 var attr = node.getAttribute("aria-hidden");
325 var alreadyHidden = attr !== null && attr !== "false";
326
327 if (alreadyHidden) {
328 return;
329 }
330
331 originalValues.push(attr);
332 rootNodes.push(node);
333 node.setAttribute("aria-hidden", "true");
334 });
335 return function () {
336 rootNodes.forEach(function (node, index) {
337 var originalValue = originalValues[index];
338
339 if (originalValue === null) {
340 node.removeAttribute("aria-hidden");
341 } else {
342 node.setAttribute("aria-hidden", originalValue);
343 }
344 });
345 };
346}
347
348function ariaLabelType(props, propName, compName, location, propFullName) {
349 var details = "\nSee https://www.w3.org/TR/wai-aria/#aria-label for details.";
350
351 if (!props["aria-label"] && !props["aria-labelledby"]) {
352 return new Error("A <" + compName + "> must have either an `aria-label` or `aria-labelledby` prop.\n " + details);
353 }
354
355 if (props["aria-label"] && props["aria-labelledby"]) {
356 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);
357 } else if (props[propName] != null && !typeCheck.isString(props[propName])) {
358 return new Error("Invalid prop `" + propName + "` supplied to `" + compName + "`. Expected `string`, received `" + (Array.isArray(propFullName) ? "array" : typeof propFullName) + "`.");
359 }
360
361 return null;
362} ////////////////////////////////////////////////////////////////////////////////
363
364exports.Dialog = Dialog;
365exports.DialogContent = DialogContent;
366exports.DialogOverlay = DialogOverlay;
367exports.default = Dialog;