UNPKG

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