1 | 'use client';
|
2 |
|
3 | import * as React from 'react';
|
4 | import * as ReactDOM from 'react-dom';
|
5 | import PropTypes from 'prop-types';
|
6 | import elementTypeAcceptingRef from '@mui/utils/elementTypeAcceptingRef';
|
7 | import NoSsr from "../NoSsr/index.js";
|
8 | import Drawer, { getAnchor, isHorizontal } from "../Drawer/Drawer.js";
|
9 | import useForkRef from "../utils/useForkRef.js";
|
10 | import ownerDocument from "../utils/ownerDocument.js";
|
11 | import ownerWindow from "../utils/ownerWindow.js";
|
12 | import useEventCallback from "../utils/useEventCallback.js";
|
13 | import useEnhancedEffect from "../utils/useEnhancedEffect.js";
|
14 | import { useTheme } from "../zero-styled/index.js";
|
15 | import { useDefaultProps } from "../DefaultPropsProvider/index.js";
|
16 | import { getTransitionProps } from "../transitions/utils.js";
|
17 | import SwipeArea from "./SwipeArea.js";
|
18 |
|
19 |
|
20 |
|
21 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
22 | const UNCERTAINTY_THRESHOLD = 3;
|
23 |
|
24 |
|
25 | const DRAG_STARTED_SIGNAL = 20;
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | let claimedSwipeInstance = null;
|
31 |
|
32 |
|
33 | export function reset() {
|
34 | claimedSwipeInstance = null;
|
35 | }
|
36 | function calculateCurrentX(anchor, touches, doc) {
|
37 | return anchor === 'right' ? doc.body.offsetWidth - touches[0].pageX : touches[0].pageX;
|
38 | }
|
39 | function calculateCurrentY(anchor, touches, containerWindow) {
|
40 | return anchor === 'bottom' ? containerWindow.innerHeight - touches[0].clientY : touches[0].clientY;
|
41 | }
|
42 | function getMaxTranslate(horizontalSwipe, paperInstance) {
|
43 | return horizontalSwipe ? paperInstance.clientWidth : paperInstance.clientHeight;
|
44 | }
|
45 | function getTranslate(currentTranslate, startLocation, open, maxTranslate) {
|
46 | return Math.min(Math.max(open ? startLocation - currentTranslate : maxTranslate + startLocation - currentTranslate, 0), maxTranslate);
|
47 | }
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | function getDomTreeShapes(element, rootNode) {
|
54 |
|
55 | const domTreeShapes = [];
|
56 | while (element && element !== rootNode.parentElement) {
|
57 | const style = ownerWindow(rootNode).getComputedStyle(element);
|
58 | if (
|
59 |
|
60 | style.getPropertyValue('position') === 'absolute' ||
|
61 |
|
62 | style.getPropertyValue('overflow-x') === 'hidden') {
|
63 |
|
64 | } else if (element.clientWidth > 0 && element.scrollWidth > element.clientWidth || element.clientHeight > 0 && element.scrollHeight > element.clientHeight) {
|
65 |
|
66 |
|
67 | domTreeShapes.push(element);
|
68 | }
|
69 | element = element.parentElement;
|
70 | }
|
71 | return domTreeShapes;
|
72 | }
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 | function computeHasNativeHandler({
|
79 | domTreeShapes,
|
80 | start,
|
81 | current,
|
82 | anchor
|
83 | }) {
|
84 |
|
85 | const axisProperties = {
|
86 | scrollPosition: {
|
87 | x: 'scrollLeft',
|
88 | y: 'scrollTop'
|
89 | },
|
90 | scrollLength: {
|
91 | x: 'scrollWidth',
|
92 | y: 'scrollHeight'
|
93 | },
|
94 | clientLength: {
|
95 | x: 'clientWidth',
|
96 | y: 'clientHeight'
|
97 | }
|
98 | };
|
99 | return domTreeShapes.some(shape => {
|
100 |
|
101 | let goingForward = current >= start;
|
102 | if (anchor === 'top' || anchor === 'left') {
|
103 | goingForward = !goingForward;
|
104 | }
|
105 | const axis = anchor === 'left' || anchor === 'right' ? 'x' : 'y';
|
106 | const scrollPosition = Math.round(shape[axisProperties.scrollPosition[axis]]);
|
107 | const areNotAtStart = scrollPosition > 0;
|
108 | const areNotAtEnd = scrollPosition + shape[axisProperties.clientLength[axis]] < shape[axisProperties.scrollLength[axis]];
|
109 | if (goingForward && areNotAtEnd || !goingForward && areNotAtStart) {
|
110 | return true;
|
111 | }
|
112 | return false;
|
113 | });
|
114 | }
|
115 | const iOS = typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
116 | const SwipeableDrawer = React.forwardRef(function SwipeableDrawer(inProps, ref) {
|
117 | const props = useDefaultProps({
|
118 | name: 'MuiSwipeableDrawer',
|
119 | props: inProps
|
120 | });
|
121 | const theme = useTheme();
|
122 | const transitionDurationDefault = {
|
123 | enter: theme.transitions.duration.enteringScreen,
|
124 | exit: theme.transitions.duration.leavingScreen
|
125 | };
|
126 | const {
|
127 | anchor = 'left',
|
128 | disableBackdropTransition = false,
|
129 | disableDiscovery = false,
|
130 | disableSwipeToOpen = iOS,
|
131 | hideBackdrop,
|
132 | hysteresis = 0.52,
|
133 | allowSwipeInChildren = false,
|
134 | minFlingVelocity = 450,
|
135 | ModalProps: {
|
136 | BackdropProps,
|
137 | ...ModalPropsProp
|
138 | } = {},
|
139 | onClose,
|
140 | onOpen,
|
141 | open = false,
|
142 | PaperProps = {},
|
143 | SwipeAreaProps,
|
144 | swipeAreaWidth = 20,
|
145 | transitionDuration = transitionDurationDefault,
|
146 | variant = 'temporary',
|
147 |
|
148 | ...other
|
149 | } = props;
|
150 | const [maybeSwiping, setMaybeSwiping] = React.useState(false);
|
151 | const swipeInstance = React.useRef({
|
152 | isSwiping: null
|
153 | });
|
154 | const swipeAreaRef = React.useRef();
|
155 | const backdropRef = React.useRef();
|
156 | const paperRef = React.useRef();
|
157 | const handleRef = useForkRef(PaperProps.ref, paperRef);
|
158 | const touchDetected = React.useRef(false);
|
159 |
|
160 |
|
161 | const calculatedDurationRef = React.useRef();
|
162 |
|
163 |
|
164 | useEnhancedEffect(() => {
|
165 | calculatedDurationRef.current = null;
|
166 | }, [open]);
|
167 | const setPosition = React.useCallback((translate, options = {}) => {
|
168 | const {
|
169 | mode = null,
|
170 | changeTransition = true
|
171 | } = options;
|
172 | const anchorRtl = getAnchor(theme, anchor);
|
173 | const rtlTranslateMultiplier = ['right', 'bottom'].includes(anchorRtl) ? 1 : -1;
|
174 | const horizontalSwipe = isHorizontal(anchor);
|
175 | const transform = horizontalSwipe ? `translate(${rtlTranslateMultiplier * translate}px, 0)` : `translate(0, ${rtlTranslateMultiplier * translate}px)`;
|
176 | const drawerStyle = paperRef.current.style;
|
177 | drawerStyle.webkitTransform = transform;
|
178 | drawerStyle.transform = transform;
|
179 | let transition = '';
|
180 | if (mode) {
|
181 | transition = theme.transitions.create('all', getTransitionProps({
|
182 | easing: undefined,
|
183 | style: undefined,
|
184 | timeout: transitionDuration
|
185 | }, {
|
186 | mode
|
187 | }));
|
188 | }
|
189 | if (changeTransition) {
|
190 | drawerStyle.webkitTransition = transition;
|
191 | drawerStyle.transition = transition;
|
192 | }
|
193 | if (!disableBackdropTransition && !hideBackdrop) {
|
194 | const backdropStyle = backdropRef.current.style;
|
195 | backdropStyle.opacity = 1 - translate / getMaxTranslate(horizontalSwipe, paperRef.current);
|
196 | if (changeTransition) {
|
197 | backdropStyle.webkitTransition = transition;
|
198 | backdropStyle.transition = transition;
|
199 | }
|
200 | }
|
201 | }, [anchor, disableBackdropTransition, hideBackdrop, theme, transitionDuration]);
|
202 | const handleBodyTouchEnd = useEventCallback(nativeEvent => {
|
203 | if (!touchDetected.current) {
|
204 | return;
|
205 | }
|
206 |
|
207 | claimedSwipeInstance = null;
|
208 | touchDetected.current = false;
|
209 | ReactDOM.flushSync(() => {
|
210 | setMaybeSwiping(false);
|
211 | });
|
212 |
|
213 |
|
214 | if (!swipeInstance.current.isSwiping) {
|
215 | swipeInstance.current.isSwiping = null;
|
216 | return;
|
217 | }
|
218 | swipeInstance.current.isSwiping = null;
|
219 | const anchorRtl = getAnchor(theme, anchor);
|
220 | const horizontal = isHorizontal(anchor);
|
221 | let current;
|
222 | if (horizontal) {
|
223 | current = calculateCurrentX(anchorRtl, nativeEvent.changedTouches, ownerDocument(nativeEvent.currentTarget));
|
224 | } else {
|
225 | current = calculateCurrentY(anchorRtl, nativeEvent.changedTouches, ownerWindow(nativeEvent.currentTarget));
|
226 | }
|
227 | const startLocation = horizontal ? swipeInstance.current.startX : swipeInstance.current.startY;
|
228 | const maxTranslate = getMaxTranslate(horizontal, paperRef.current);
|
229 | const currentTranslate = getTranslate(current, startLocation, open, maxTranslate);
|
230 | const translateRatio = currentTranslate / maxTranslate;
|
231 | if (Math.abs(swipeInstance.current.velocity) > minFlingVelocity) {
|
232 |
|
233 | calculatedDurationRef.current = Math.abs((maxTranslate - currentTranslate) / swipeInstance.current.velocity) * 1000;
|
234 | }
|
235 | if (open) {
|
236 | if (swipeInstance.current.velocity > minFlingVelocity || translateRatio > hysteresis) {
|
237 | onClose();
|
238 | } else {
|
239 |
|
240 | setPosition(0, {
|
241 | mode: 'exit'
|
242 | });
|
243 | }
|
244 | return;
|
245 | }
|
246 | if (swipeInstance.current.velocity < -minFlingVelocity || 1 - translateRatio > hysteresis) {
|
247 | onOpen();
|
248 | } else {
|
249 |
|
250 | setPosition(getMaxTranslate(horizontal, paperRef.current), {
|
251 | mode: 'enter'
|
252 | });
|
253 | }
|
254 | });
|
255 | const startMaybeSwiping = (force = false) => {
|
256 | if (!maybeSwiping) {
|
257 |
|
258 |
|
259 |
|
260 | if (force || !(disableDiscovery && allowSwipeInChildren)) {
|
261 | ReactDOM.flushSync(() => {
|
262 | setMaybeSwiping(true);
|
263 | });
|
264 | }
|
265 | const horizontalSwipe = isHorizontal(anchor);
|
266 | if (!open && paperRef.current) {
|
267 |
|
268 | setPosition(getMaxTranslate(horizontalSwipe, paperRef.current) + (disableDiscovery ? 15 : -DRAG_STARTED_SIGNAL), {
|
269 | changeTransition: false
|
270 | });
|
271 | }
|
272 | swipeInstance.current.velocity = 0;
|
273 | swipeInstance.current.lastTime = null;
|
274 | swipeInstance.current.lastTranslate = null;
|
275 | swipeInstance.current.paperHit = false;
|
276 | touchDetected.current = true;
|
277 | }
|
278 | };
|
279 | const handleBodyTouchMove = useEventCallback(nativeEvent => {
|
280 |
|
281 | if (!paperRef.current || !touchDetected.current) {
|
282 | return;
|
283 | }
|
284 |
|
285 |
|
286 | if (claimedSwipeInstance !== null && claimedSwipeInstance !== swipeInstance.current) {
|
287 | return;
|
288 | }
|
289 | startMaybeSwiping(true);
|
290 | const anchorRtl = getAnchor(theme, anchor);
|
291 | const horizontalSwipe = isHorizontal(anchor);
|
292 | const currentX = calculateCurrentX(anchorRtl, nativeEvent.touches, ownerDocument(nativeEvent.currentTarget));
|
293 | const currentY = calculateCurrentY(anchorRtl, nativeEvent.touches, ownerWindow(nativeEvent.currentTarget));
|
294 | if (open && paperRef.current.contains(nativeEvent.target) && claimedSwipeInstance === null) {
|
295 | const domTreeShapes = getDomTreeShapes(nativeEvent.target, paperRef.current);
|
296 | const hasNativeHandler = computeHasNativeHandler({
|
297 | domTreeShapes,
|
298 | start: horizontalSwipe ? swipeInstance.current.startX : swipeInstance.current.startY,
|
299 | current: horizontalSwipe ? currentX : currentY,
|
300 | anchor
|
301 | });
|
302 | if (hasNativeHandler) {
|
303 | claimedSwipeInstance = true;
|
304 | return;
|
305 | }
|
306 | claimedSwipeInstance = swipeInstance.current;
|
307 | }
|
308 |
|
309 |
|
310 | if (swipeInstance.current.isSwiping == null) {
|
311 | const dx = Math.abs(currentX - swipeInstance.current.startX);
|
312 | const dy = Math.abs(currentY - swipeInstance.current.startY);
|
313 | const definitelySwiping = horizontalSwipe ? dx > dy && dx > UNCERTAINTY_THRESHOLD : dy > dx && dy > UNCERTAINTY_THRESHOLD;
|
314 | if (definitelySwiping && nativeEvent.cancelable) {
|
315 | nativeEvent.preventDefault();
|
316 | }
|
317 | if (definitelySwiping === true || (horizontalSwipe ? dy > UNCERTAINTY_THRESHOLD : dx > UNCERTAINTY_THRESHOLD)) {
|
318 | swipeInstance.current.isSwiping = definitelySwiping;
|
319 | if (!definitelySwiping) {
|
320 | handleBodyTouchEnd(nativeEvent);
|
321 | return;
|
322 | }
|
323 |
|
324 |
|
325 | swipeInstance.current.startX = currentX;
|
326 | swipeInstance.current.startY = currentY;
|
327 |
|
328 |
|
329 | if (!disableDiscovery && !open) {
|
330 | if (horizontalSwipe) {
|
331 | swipeInstance.current.startX -= DRAG_STARTED_SIGNAL;
|
332 | } else {
|
333 | swipeInstance.current.startY -= DRAG_STARTED_SIGNAL;
|
334 | }
|
335 | }
|
336 | }
|
337 | }
|
338 | if (!swipeInstance.current.isSwiping) {
|
339 | return;
|
340 | }
|
341 | const maxTranslate = getMaxTranslate(horizontalSwipe, paperRef.current);
|
342 | let startLocation = horizontalSwipe ? swipeInstance.current.startX : swipeInstance.current.startY;
|
343 | if (open && !swipeInstance.current.paperHit) {
|
344 | startLocation = Math.min(startLocation, maxTranslate);
|
345 | }
|
346 | const translate = getTranslate(horizontalSwipe ? currentX : currentY, startLocation, open, maxTranslate);
|
347 | if (open) {
|
348 | if (!swipeInstance.current.paperHit) {
|
349 | const paperHit = horizontalSwipe ? currentX < maxTranslate : currentY < maxTranslate;
|
350 | if (paperHit) {
|
351 | swipeInstance.current.paperHit = true;
|
352 | swipeInstance.current.startX = currentX;
|
353 | swipeInstance.current.startY = currentY;
|
354 | } else {
|
355 | return;
|
356 | }
|
357 | } else if (translate === 0) {
|
358 | swipeInstance.current.startX = currentX;
|
359 | swipeInstance.current.startY = currentY;
|
360 | }
|
361 | }
|
362 | if (swipeInstance.current.lastTranslate === null) {
|
363 | swipeInstance.current.lastTranslate = translate;
|
364 | swipeInstance.current.lastTime = performance.now() + 1;
|
365 | }
|
366 | const velocity = (translate - swipeInstance.current.lastTranslate) / (performance.now() - swipeInstance.current.lastTime) * 1e3;
|
367 |
|
368 |
|
369 | swipeInstance.current.velocity = swipeInstance.current.velocity * 0.4 + velocity * 0.6;
|
370 | swipeInstance.current.lastTranslate = translate;
|
371 | swipeInstance.current.lastTime = performance.now();
|
372 |
|
373 |
|
374 | if (nativeEvent.cancelable) {
|
375 | nativeEvent.preventDefault();
|
376 | }
|
377 | setPosition(translate);
|
378 | });
|
379 | const handleBodyTouchStart = useEventCallback(nativeEvent => {
|
380 |
|
381 |
|
382 | if (nativeEvent.defaultPrevented) {
|
383 | return;
|
384 | }
|
385 |
|
386 |
|
387 | if (nativeEvent.defaultMuiPrevented) {
|
388 | return;
|
389 | }
|
390 |
|
391 |
|
392 | if (open && (hideBackdrop || !backdropRef.current.contains(nativeEvent.target)) && !paperRef.current.contains(nativeEvent.target)) {
|
393 | return;
|
394 | }
|
395 | const anchorRtl = getAnchor(theme, anchor);
|
396 | const horizontalSwipe = isHorizontal(anchor);
|
397 | const currentX = calculateCurrentX(anchorRtl, nativeEvent.touches, ownerDocument(nativeEvent.currentTarget));
|
398 | const currentY = calculateCurrentY(anchorRtl, nativeEvent.touches, ownerWindow(nativeEvent.currentTarget));
|
399 | if (!open) {
|
400 |
|
401 |
|
402 |
|
403 |
|
404 | if (disableSwipeToOpen || !(nativeEvent.target === swipeAreaRef.current || paperRef.current?.contains(nativeEvent.target) && (typeof allowSwipeInChildren === 'function' ? allowSwipeInChildren(nativeEvent, swipeAreaRef.current, paperRef.current) : allowSwipeInChildren))) {
|
405 | return;
|
406 | }
|
407 | if (horizontalSwipe) {
|
408 | if (currentX > swipeAreaWidth) {
|
409 | return;
|
410 | }
|
411 | } else if (currentY > swipeAreaWidth) {
|
412 | return;
|
413 | }
|
414 | }
|
415 | nativeEvent.defaultMuiPrevented = true;
|
416 | claimedSwipeInstance = null;
|
417 | swipeInstance.current.startX = currentX;
|
418 | swipeInstance.current.startY = currentY;
|
419 | startMaybeSwiping();
|
420 | });
|
421 | React.useEffect(() => {
|
422 | if (variant === 'temporary') {
|
423 | const doc = ownerDocument(paperRef.current);
|
424 | doc.addEventListener('touchstart', handleBodyTouchStart);
|
425 |
|
426 |
|
427 |
|
428 | doc.addEventListener('touchmove', handleBodyTouchMove, {
|
429 | passive: !open
|
430 | });
|
431 | doc.addEventListener('touchend', handleBodyTouchEnd);
|
432 | return () => {
|
433 | doc.removeEventListener('touchstart', handleBodyTouchStart);
|
434 | doc.removeEventListener('touchmove', handleBodyTouchMove, {
|
435 | passive: !open
|
436 | });
|
437 | doc.removeEventListener('touchend', handleBodyTouchEnd);
|
438 | };
|
439 | }
|
440 | return undefined;
|
441 | }, [variant, open, handleBodyTouchStart, handleBodyTouchMove, handleBodyTouchEnd]);
|
442 | React.useEffect(() => () => {
|
443 |
|
444 | if (claimedSwipeInstance === swipeInstance.current) {
|
445 | claimedSwipeInstance = null;
|
446 | }
|
447 | }, []);
|
448 | React.useEffect(() => {
|
449 | if (!open) {
|
450 | setMaybeSwiping(false);
|
451 | }
|
452 | }, [open]);
|
453 | return _jsxs(React.Fragment, {
|
454 | children: [_jsx(Drawer, {
|
455 | open: variant === 'temporary' && maybeSwiping ? true : open,
|
456 | variant: variant,
|
457 | ModalProps: {
|
458 | BackdropProps: {
|
459 | ...BackdropProps,
|
460 | ref: backdropRef
|
461 | },
|
462 |
|
463 |
|
464 | ...(variant === 'temporary' && {
|
465 | keepMounted: true
|
466 | }),
|
467 | ...ModalPropsProp
|
468 | },
|
469 | hideBackdrop: hideBackdrop,
|
470 | PaperProps: {
|
471 | ...PaperProps,
|
472 | style: {
|
473 | pointerEvents: variant === 'temporary' && !open && !allowSwipeInChildren ? 'none' : '',
|
474 | ...PaperProps.style
|
475 | },
|
476 | ref: handleRef
|
477 | },
|
478 | anchor: anchor,
|
479 | transitionDuration: calculatedDurationRef.current || transitionDuration,
|
480 | onClose: onClose,
|
481 | ref: ref,
|
482 | ...other
|
483 | }), !disableSwipeToOpen && variant === 'temporary' && _jsx(NoSsr, {
|
484 | children: _jsx(SwipeArea, {
|
485 | anchor: anchor,
|
486 | ref: swipeAreaRef,
|
487 | width: swipeAreaWidth,
|
488 | ...SwipeAreaProps
|
489 | })
|
490 | })]
|
491 | });
|
492 | });
|
493 | process.env.NODE_ENV !== "production" ? SwipeableDrawer.propTypes = {
|
494 |
|
495 |
|
496 |
|
497 |
|
498 | |
499 |
|
500 |
|
501 |
|
502 |
|
503 |
|
504 |
|
505 |
|
506 |
|
507 |
|
508 |
|
509 |
|
510 | allowSwipeInChildren: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
|
511 | |
512 |
|
513 |
|
514 | anchor: PropTypes.oneOf(['bottom', 'left', 'right', 'top']),
|
515 | |
516 |
|
517 |
|
518 | children: PropTypes.node,
|
519 | |
520 |
|
521 |
|
522 |
|
523 |
|
524 | disableBackdropTransition: PropTypes.bool,
|
525 | |
526 |
|
527 |
|
528 |
|
529 |
|
530 | disableDiscovery: PropTypes.bool,
|
531 | |
532 |
|
533 |
|
534 |
|
535 |
|
536 | disableSwipeToOpen: PropTypes.bool,
|
537 | |
538 |
|
539 |
|
540 | hideBackdrop: PropTypes.bool,
|
541 | |
542 |
|
543 |
|
544 |
|
545 |
|
546 | hysteresis: PropTypes.number,
|
547 | |
548 |
|
549 |
|
550 |
|
551 |
|
552 |
|
553 | minFlingVelocity: PropTypes.number,
|
554 | |
555 |
|
556 |
|
557 | ModalProps: PropTypes .shape({
|
558 | BackdropProps: PropTypes.shape({
|
559 | component: elementTypeAcceptingRef
|
560 | })
|
561 | }),
|
562 | |
563 |
|
564 |
|
565 |
|
566 |
|
567 | onClose: PropTypes.func.isRequired,
|
568 | |
569 |
|
570 |
|
571 |
|
572 |
|
573 | onOpen: PropTypes.func.isRequired,
|
574 | |
575 |
|
576 |
|
577 |
|
578 | open: PropTypes.bool,
|
579 | |
580 |
|
581 |
|
582 | PaperProps: PropTypes .shape({
|
583 | component: elementTypeAcceptingRef,
|
584 | style: PropTypes.object
|
585 | }),
|
586 | |
587 |
|
588 |
|
589 | SwipeAreaProps: PropTypes.object,
|
590 | |
591 |
|
592 |
|
593 |
|
594 |
|
595 | swipeAreaWidth: PropTypes.number,
|
596 | |
597 |
|
598 |
|
599 |
|
600 |
|
601 |
|
602 |
|
603 |
|
604 | transitionDuration: PropTypes.oneOfType([PropTypes.number, PropTypes.shape({
|
605 | appear: PropTypes.number,
|
606 | enter: PropTypes.number,
|
607 | exit: PropTypes.number
|
608 | })]),
|
609 | |
610 |
|
611 |
|
612 | variant: PropTypes.oneOf(['permanent', 'persistent', 'temporary'])
|
613 | } : void 0;
|
614 | export default SwipeableDrawer; |
\ | No newline at end of file |