@seed-design/react-drawer 0.0.0-alpha-20260414104312
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Drawer-12s-D38FEQUp.cjs +1361 -0
- package/lib/Drawer-12s-DPrL_6Um.js +1346 -0
- package/lib/index.cjs +29 -0
- package/lib/index.d.ts +4317 -0
- package/lib/index.js +18 -0
- package/package.json +52 -0
- package/src/Drawer.namespace.ts +22 -0
- package/src/Drawer.tsx +451 -0
- package/src/browser.ts +44 -0
- package/src/constants.ts +21 -0
- package/src/helpers.ts +145 -0
- package/src/index.ts +27 -0
- package/src/types.ts +7 -0
- package/src/use-position-fixed.ts +149 -0
- package/src/use-snap-points.ts +353 -0
- package/src/useDrawer.test.tsx +341 -0
- package/src/useDrawer.ts +810 -0
- package/src/useDrawerContext.tsx +15 -0
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { AnyFunction, DrawerDirection } from "./types";
|
|
2
|
+
|
|
3
|
+
interface Style {
|
|
4
|
+
[key: string]: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const cache = new WeakMap();
|
|
8
|
+
|
|
9
|
+
// HTML input types that do not cause the software keyboard to appear.
|
|
10
|
+
const nonTextInputTypes = new Set([
|
|
11
|
+
"checkbox",
|
|
12
|
+
"radio",
|
|
13
|
+
"range",
|
|
14
|
+
"color",
|
|
15
|
+
"file",
|
|
16
|
+
"image",
|
|
17
|
+
"button",
|
|
18
|
+
"submit",
|
|
19
|
+
"reset",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function isInput(target: Element) {
|
|
23
|
+
return (
|
|
24
|
+
(target instanceof HTMLInputElement && !nonTextInputTypes.has(target.type)) ||
|
|
25
|
+
target instanceof HTMLTextAreaElement ||
|
|
26
|
+
(target instanceof HTMLElement && target.isContentEditable)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isInView(el: HTMLElement): boolean {
|
|
31
|
+
const rect = el.getBoundingClientRect();
|
|
32
|
+
|
|
33
|
+
if (!window.visualViewport) return false;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
rect.top >= 0 &&
|
|
37
|
+
rect.left >= 0 &&
|
|
38
|
+
// Need + 40 for safari detection
|
|
39
|
+
rect.bottom <= window.visualViewport.height - 40 &&
|
|
40
|
+
rect.right <= window.visualViewport.width
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function set(
|
|
45
|
+
el: Element | HTMLElement | null | undefined,
|
|
46
|
+
styles: Style,
|
|
47
|
+
ignoreCache = false,
|
|
48
|
+
) {
|
|
49
|
+
if (!el || !(el instanceof HTMLElement)) return;
|
|
50
|
+
let originalStyles: Style = {};
|
|
51
|
+
|
|
52
|
+
Object.entries(styles).forEach(([key, value]: [string, string]) => {
|
|
53
|
+
if (key.startsWith("--")) {
|
|
54
|
+
el.style.setProperty(key, value);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
originalStyles[key] = (el.style as any)[key];
|
|
59
|
+
(el.style as any)[key] = value;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (ignoreCache) return;
|
|
63
|
+
|
|
64
|
+
cache.set(el, originalStyles);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function reset(el: Element | HTMLElement | null, prop?: string) {
|
|
68
|
+
if (!el || !(el instanceof HTMLElement)) return;
|
|
69
|
+
let originalStyles = cache.get(el);
|
|
70
|
+
|
|
71
|
+
if (!originalStyles) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (prop) {
|
|
76
|
+
(el.style as any)[prop] = originalStyles[prop];
|
|
77
|
+
} else {
|
|
78
|
+
Object.entries(originalStyles).forEach(([key, value]) => {
|
|
79
|
+
(el.style as any)[key] = value;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const isVertical = (direction: DrawerDirection) => {
|
|
85
|
+
switch (direction) {
|
|
86
|
+
case "top":
|
|
87
|
+
case "bottom":
|
|
88
|
+
return true;
|
|
89
|
+
case "left":
|
|
90
|
+
case "right":
|
|
91
|
+
return false;
|
|
92
|
+
default:
|
|
93
|
+
return direction satisfies never;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export function getTranslate(element: HTMLElement, direction: DrawerDirection): number | null {
|
|
98
|
+
if (!element) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const style = window.getComputedStyle(element);
|
|
102
|
+
const transform =
|
|
103
|
+
// @ts-ignore
|
|
104
|
+
style.transform || style.webkitTransform || style.mozTransform;
|
|
105
|
+
let mat = transform.match(/^matrix3d\((.+)\)$/);
|
|
106
|
+
if (mat) {
|
|
107
|
+
// https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix3d
|
|
108
|
+
return Number.parseFloat(mat[1].split(", ")[isVertical(direction) ? 13 : 12]);
|
|
109
|
+
}
|
|
110
|
+
// https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix
|
|
111
|
+
mat = transform.match(/^matrix\((.+)\)$/);
|
|
112
|
+
return mat ? Number.parseFloat(mat[1].split(", ")[isVertical(direction) ? 5 : 4]) : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function dampenValue(v: number) {
|
|
116
|
+
return 8 * (Math.log(v + 1) - 2);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function assignStyle(
|
|
120
|
+
element: HTMLElement | null | undefined,
|
|
121
|
+
style: Partial<CSSStyleDeclaration>,
|
|
122
|
+
) {
|
|
123
|
+
if (!element) return () => {};
|
|
124
|
+
|
|
125
|
+
const prevStyle = element.style.cssText;
|
|
126
|
+
Object.assign(element.style, style);
|
|
127
|
+
|
|
128
|
+
return () => {
|
|
129
|
+
element.style.cssText = prevStyle;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Receives functions as arguments and returns a new function that calls all.
|
|
135
|
+
*/
|
|
136
|
+
export function chain<T>(...fns: T[]) {
|
|
137
|
+
return (...args: T extends AnyFunction ? Parameters<T> : never) => {
|
|
138
|
+
for (const fn of fns) {
|
|
139
|
+
if (typeof fn === "function") {
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
fn(...args);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DrawerBackdrop,
|
|
3
|
+
DrawerCloseButton,
|
|
4
|
+
DrawerContent,
|
|
5
|
+
DrawerDescription,
|
|
6
|
+
DrawerHandle,
|
|
7
|
+
DrawerHeader,
|
|
8
|
+
DrawerPositioner,
|
|
9
|
+
DrawerRoot,
|
|
10
|
+
DrawerTitle,
|
|
11
|
+
DrawerTrigger,
|
|
12
|
+
type DrawerBackdropProps,
|
|
13
|
+
type DrawerCloseButtonProps,
|
|
14
|
+
type DrawerContentProps,
|
|
15
|
+
type DrawerDescriptionProps,
|
|
16
|
+
type DrawerHandleProps,
|
|
17
|
+
type DrawerHeaderProps,
|
|
18
|
+
type DrawerPositionerProps,
|
|
19
|
+
type DrawerRootProps,
|
|
20
|
+
type DrawerTitleProps,
|
|
21
|
+
type DrawerTriggerProps,
|
|
22
|
+
} from "./Drawer";
|
|
23
|
+
|
|
24
|
+
export { useDrawer, type UseDrawerProps } from "./useDrawer";
|
|
25
|
+
export { useDrawerContext, type DrawerContextValue } from "./useDrawerContext";
|
|
26
|
+
|
|
27
|
+
export * as Drawer from "./Drawer.namespace";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// This code comes from https://github.com/emilkowalski/vaul/blob/main/src/use-position-fixed.ts
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { isSafari } from "./browser";
|
|
5
|
+
|
|
6
|
+
let previousBodyPosition: Record<string, string> | null = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This hook is necessary to prevent buggy behavior on iOS devices (need to test on Android).
|
|
10
|
+
* I won't get into too much detail about what bugs it solves, but so far I've found that setting the body to `position: fixed` is the most reliable way to prevent those bugs.
|
|
11
|
+
* Issues that this hook solves:
|
|
12
|
+
* https://github.com/emilkowalski/vaul/issues/435
|
|
13
|
+
* https://github.com/emilkowalski/vaul/issues/433
|
|
14
|
+
* And more that I discovered, but were just not reported.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export function usePositionFixed({
|
|
18
|
+
isOpen,
|
|
19
|
+
modal,
|
|
20
|
+
nested,
|
|
21
|
+
hasBeenOpened,
|
|
22
|
+
preventScrollRestoration,
|
|
23
|
+
noBodyStyles,
|
|
24
|
+
}: {
|
|
25
|
+
isOpen: boolean;
|
|
26
|
+
modal: boolean;
|
|
27
|
+
nested: boolean;
|
|
28
|
+
hasBeenOpened: boolean;
|
|
29
|
+
preventScrollRestoration: boolean;
|
|
30
|
+
noBodyStyles: boolean;
|
|
31
|
+
}) {
|
|
32
|
+
const [activeUrl, setActiveUrl] = React.useState(() =>
|
|
33
|
+
typeof window !== "undefined" ? window.location.href : "",
|
|
34
|
+
);
|
|
35
|
+
const scrollPos = React.useRef(0);
|
|
36
|
+
|
|
37
|
+
const setPositionFixed = React.useCallback(() => {
|
|
38
|
+
// All browsers on iOS will return true here.
|
|
39
|
+
if (!isSafari()) return;
|
|
40
|
+
|
|
41
|
+
// If previousBodyPosition is already set, don't set it again.
|
|
42
|
+
if (previousBodyPosition === null && isOpen && !noBodyStyles) {
|
|
43
|
+
previousBodyPosition = {
|
|
44
|
+
position: document.body.style.position,
|
|
45
|
+
top: document.body.style.top,
|
|
46
|
+
left: document.body.style.left,
|
|
47
|
+
height: document.body.style.height,
|
|
48
|
+
right: "unset",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Update the dom inside an animation frame
|
|
52
|
+
const { scrollX, innerHeight } = window;
|
|
53
|
+
|
|
54
|
+
document.body.style.setProperty("position", "fixed", "important");
|
|
55
|
+
Object.assign(document.body.style, {
|
|
56
|
+
top: `${-scrollPos.current}px`,
|
|
57
|
+
left: `${-scrollX}px`,
|
|
58
|
+
right: "0px",
|
|
59
|
+
height: "auto",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
window.setTimeout(
|
|
63
|
+
() =>
|
|
64
|
+
window.requestAnimationFrame(() => {
|
|
65
|
+
// Attempt to check if the bottom bar appeared due to the position change
|
|
66
|
+
const bottomBarHeight = innerHeight - window.innerHeight;
|
|
67
|
+
if (bottomBarHeight && scrollPos.current >= innerHeight) {
|
|
68
|
+
// Move the content further up so that the bottom bar doesn't hide it
|
|
69
|
+
document.body.style.top = `${-(scrollPos.current + bottomBarHeight)}px`;
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
300,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}, [isOpen]);
|
|
76
|
+
|
|
77
|
+
const restorePositionSetting = React.useCallback(() => {
|
|
78
|
+
// All browsers on iOS will return true here.
|
|
79
|
+
if (!isSafari()) return;
|
|
80
|
+
|
|
81
|
+
if (previousBodyPosition !== null && !noBodyStyles) {
|
|
82
|
+
// Convert the position from "px" to Int
|
|
83
|
+
const y = -parseInt(document.body.style.top, 10);
|
|
84
|
+
const x = -parseInt(document.body.style.left, 10);
|
|
85
|
+
|
|
86
|
+
// Restore styles
|
|
87
|
+
Object.assign(document.body.style, previousBodyPosition);
|
|
88
|
+
|
|
89
|
+
window.requestAnimationFrame(() => {
|
|
90
|
+
if (preventScrollRestoration && activeUrl !== window.location.href) {
|
|
91
|
+
setActiveUrl(window.location.href);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
window.scrollTo(x, y);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
previousBodyPosition = null;
|
|
99
|
+
}
|
|
100
|
+
}, [activeUrl]);
|
|
101
|
+
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
function onScroll() {
|
|
104
|
+
scrollPos.current = window.scrollY;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
onScroll();
|
|
108
|
+
|
|
109
|
+
window.addEventListener("scroll", onScroll);
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
window.removeEventListener("scroll", onScroll);
|
|
113
|
+
};
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
React.useEffect(() => {
|
|
117
|
+
if (!modal) return;
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
if (typeof document === "undefined") return;
|
|
121
|
+
|
|
122
|
+
// Another drawer is opened, safe to ignore the execution
|
|
123
|
+
const hasDrawerOpened = !!document.querySelector("[data-drawer]");
|
|
124
|
+
if (hasDrawerOpened) return;
|
|
125
|
+
|
|
126
|
+
restorePositionSetting();
|
|
127
|
+
};
|
|
128
|
+
}, [modal, restorePositionSetting]);
|
|
129
|
+
|
|
130
|
+
React.useEffect(() => {
|
|
131
|
+
if (nested || !hasBeenOpened) return;
|
|
132
|
+
// This is needed to force Safari toolbar to show **before** the drawer starts animating to prevent a gnarly shift from happening
|
|
133
|
+
if (isOpen) {
|
|
134
|
+
// avoid for standalone mode (PWA)
|
|
135
|
+
const isStandalone = window.matchMedia("(display-mode: standalone)").matches;
|
|
136
|
+
!isStandalone && setPositionFixed();
|
|
137
|
+
|
|
138
|
+
if (!modal) {
|
|
139
|
+
window.setTimeout(() => {
|
|
140
|
+
restorePositionSetting();
|
|
141
|
+
}, 500);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
restorePositionSetting();
|
|
145
|
+
}
|
|
146
|
+
}, [isOpen, hasBeenOpened, activeUrl, modal, nested, setPositionFixed, restorePositionSetting]);
|
|
147
|
+
|
|
148
|
+
return { restorePositionSetting };
|
|
149
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { useControllableState } from "@seed-design/react-use-controllable-state";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { TRANSITIONS, VELOCITY_THRESHOLD } from "./constants";
|
|
4
|
+
import { isVertical, set } from "./helpers";
|
|
5
|
+
import type { DrawerDirection } from "./types";
|
|
6
|
+
|
|
7
|
+
export function useSnapPoints({
|
|
8
|
+
activeSnapPointProp,
|
|
9
|
+
setActiveSnapPointProp,
|
|
10
|
+
snapPoints,
|
|
11
|
+
drawerRef,
|
|
12
|
+
overlayRef,
|
|
13
|
+
fadeFromIndex,
|
|
14
|
+
onSnapPointChange,
|
|
15
|
+
direction = "bottom",
|
|
16
|
+
container,
|
|
17
|
+
snapToSequentialPoint,
|
|
18
|
+
}: {
|
|
19
|
+
activeSnapPointProp?: number | string | null;
|
|
20
|
+
setActiveSnapPointProp?(snapPoint: number | null | string): void;
|
|
21
|
+
snapPoints?: (number | string)[];
|
|
22
|
+
fadeFromIndex?: number;
|
|
23
|
+
drawerRef: React.RefObject<HTMLDivElement | null>;
|
|
24
|
+
overlayRef: React.RefObject<HTMLDivElement | null>;
|
|
25
|
+
onSnapPointChange(activeSnapPointIndex: number): void;
|
|
26
|
+
direction?: DrawerDirection;
|
|
27
|
+
container?: HTMLElement | null | undefined;
|
|
28
|
+
snapToSequentialPoint?: boolean;
|
|
29
|
+
}) {
|
|
30
|
+
const [activeSnapPoint, setActiveSnapPoint] = useControllableState<string | number | null>({
|
|
31
|
+
prop: activeSnapPointProp,
|
|
32
|
+
defaultProp: snapPoints?.[0] ?? null,
|
|
33
|
+
onChange: setActiveSnapPointProp,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const [windowDimensions, setWindowDimensions] = React.useState(
|
|
37
|
+
typeof window !== "undefined"
|
|
38
|
+
? {
|
|
39
|
+
innerWidth: window.innerWidth,
|
|
40
|
+
innerHeight: window.innerHeight,
|
|
41
|
+
}
|
|
42
|
+
: undefined,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
function onResize() {
|
|
47
|
+
setWindowDimensions({
|
|
48
|
+
innerWidth: window.innerWidth,
|
|
49
|
+
innerHeight: window.innerHeight,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
window.addEventListener("resize", onResize);
|
|
53
|
+
|
|
54
|
+
return () => window.removeEventListener("resize", onResize);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const isLastSnapPoint = React.useMemo(
|
|
58
|
+
() => activeSnapPoint === snapPoints?.[snapPoints.length - 1] || null,
|
|
59
|
+
[snapPoints, activeSnapPoint],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const activeSnapPointIndex = React.useMemo(
|
|
63
|
+
() => snapPoints?.findIndex((snapPoint) => snapPoint === activeSnapPoint) ?? null,
|
|
64
|
+
[snapPoints, activeSnapPoint],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const shouldFade =
|
|
68
|
+
(snapPoints &&
|
|
69
|
+
snapPoints.length > 0 &&
|
|
70
|
+
(fadeFromIndex || fadeFromIndex === 0) &&
|
|
71
|
+
!Number.isNaN(fadeFromIndex) &&
|
|
72
|
+
snapPoints[fadeFromIndex] === activeSnapPoint) ||
|
|
73
|
+
!snapPoints;
|
|
74
|
+
|
|
75
|
+
const snapPointsOffset = React.useMemo(() => {
|
|
76
|
+
const containerSize = container
|
|
77
|
+
? {
|
|
78
|
+
width: container.getBoundingClientRect().width,
|
|
79
|
+
height: container.getBoundingClientRect().height,
|
|
80
|
+
}
|
|
81
|
+
: typeof window !== "undefined"
|
|
82
|
+
? { width: window.innerWidth, height: window.innerHeight }
|
|
83
|
+
: { width: 0, height: 0 };
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
snapPoints?.map((snapPoint) => {
|
|
87
|
+
// FIXME
|
|
88
|
+
// 1 -> container 100% << expected
|
|
89
|
+
// 0.5 -> container 50% << expected
|
|
90
|
+
// 300px -> 300 -> 300px << expected
|
|
91
|
+
// 15rem -> 15 -> 15px << this makes no sense, should fix or disallow
|
|
92
|
+
|
|
93
|
+
const isPx = typeof snapPoint === "string";
|
|
94
|
+
let snapPointAsNumber = 0;
|
|
95
|
+
|
|
96
|
+
if (isPx) {
|
|
97
|
+
snapPointAsNumber = Number.parseInt(snapPoint, 10);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isVertical(direction)) {
|
|
101
|
+
const height = isPx
|
|
102
|
+
? snapPointAsNumber
|
|
103
|
+
: windowDimensions
|
|
104
|
+
? snapPoint * containerSize.height
|
|
105
|
+
: 0;
|
|
106
|
+
|
|
107
|
+
if (windowDimensions) {
|
|
108
|
+
return direction === "bottom"
|
|
109
|
+
? containerSize.height - height
|
|
110
|
+
: -containerSize.height + height;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return height;
|
|
114
|
+
}
|
|
115
|
+
const width = isPx
|
|
116
|
+
? snapPointAsNumber
|
|
117
|
+
: windowDimensions
|
|
118
|
+
? snapPoint * containerSize.width
|
|
119
|
+
: 0;
|
|
120
|
+
|
|
121
|
+
if (windowDimensions) {
|
|
122
|
+
return direction === "right" ? containerSize.width - width : -containerSize.width + width;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return width;
|
|
126
|
+
}) ?? []
|
|
127
|
+
);
|
|
128
|
+
}, [snapPoints, windowDimensions, container, direction]);
|
|
129
|
+
|
|
130
|
+
const activeSnapPointOffset = React.useMemo(
|
|
131
|
+
() => (activeSnapPointIndex !== null ? snapPointsOffset?.[activeSnapPointIndex] : null),
|
|
132
|
+
[snapPointsOffset, activeSnapPointIndex],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const snapToPoint = React.useCallback(
|
|
136
|
+
(dimension: number) => {
|
|
137
|
+
const newSnapPointIndex =
|
|
138
|
+
snapPointsOffset?.findIndex((snapPointDim) => snapPointDim === dimension) ?? null;
|
|
139
|
+
onSnapPointChange(newSnapPointIndex);
|
|
140
|
+
|
|
141
|
+
set(drawerRef.current, {
|
|
142
|
+
transition: `transform ${TRANSITIONS.ENTER_DURATION}s ${TRANSITIONS.CONTENT_ENTER_TIMING_FUNCTION}`,
|
|
143
|
+
transform: isVertical(direction)
|
|
144
|
+
? `translate3d(0, ${dimension}px, 0)`
|
|
145
|
+
: `translate3d(${dimension}px, 0, 0)`,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
snapPointsOffset &&
|
|
150
|
+
newSnapPointIndex !== snapPointsOffset.length - 1 &&
|
|
151
|
+
fadeFromIndex !== undefined &&
|
|
152
|
+
newSnapPointIndex !== fadeFromIndex &&
|
|
153
|
+
newSnapPointIndex < fadeFromIndex
|
|
154
|
+
) {
|
|
155
|
+
if (fadeFromIndex !== 0) {
|
|
156
|
+
set(overlayRef.current, {
|
|
157
|
+
transition: `opacity ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.OVERLAY_EXIT_TIMING_FUNCTION}`,
|
|
158
|
+
opacity: "0",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
set(overlayRef.current, {
|
|
163
|
+
transition: `opacity ${TRANSITIONS.ENTER_DURATION}s ${TRANSITIONS.OVERLAY_ENTER_TIMING_FUNCTION}`,
|
|
164
|
+
opacity: "1",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setActiveSnapPoint(snapPoints?.[Math.max(newSnapPointIndex, 0)]);
|
|
169
|
+
},
|
|
170
|
+
[
|
|
171
|
+
drawerRef,
|
|
172
|
+
overlayRef,
|
|
173
|
+
snapPoints,
|
|
174
|
+
snapPointsOffset,
|
|
175
|
+
fadeFromIndex,
|
|
176
|
+
direction,
|
|
177
|
+
onSnapPointChange,
|
|
178
|
+
setActiveSnapPoint,
|
|
179
|
+
],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
if (activeSnapPoint || activeSnapPointProp) {
|
|
184
|
+
const newIndex =
|
|
185
|
+
snapPoints?.findIndex(
|
|
186
|
+
(snapPoint) => snapPoint === activeSnapPointProp || snapPoint === activeSnapPoint,
|
|
187
|
+
) ?? -1;
|
|
188
|
+
if (snapPointsOffset && newIndex !== -1 && typeof snapPointsOffset[newIndex] === "number") {
|
|
189
|
+
snapToPoint(snapPointsOffset[newIndex] as number);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}, [activeSnapPoint, activeSnapPointProp, snapPoints, snapPointsOffset, snapToPoint]);
|
|
193
|
+
|
|
194
|
+
function onRelease({
|
|
195
|
+
draggedDistance,
|
|
196
|
+
closeDrawer,
|
|
197
|
+
velocity,
|
|
198
|
+
dismissible,
|
|
199
|
+
event,
|
|
200
|
+
}: {
|
|
201
|
+
draggedDistance: number;
|
|
202
|
+
closeDrawer: (fromWithin: boolean, details: { reason: "drag"; event: PointerEvent }) => void;
|
|
203
|
+
velocity: number;
|
|
204
|
+
dismissible: boolean;
|
|
205
|
+
event: PointerEvent;
|
|
206
|
+
}) {
|
|
207
|
+
if (fadeFromIndex === undefined) return;
|
|
208
|
+
|
|
209
|
+
const currentPosition =
|
|
210
|
+
direction === "bottom" || direction === "right"
|
|
211
|
+
? (activeSnapPointOffset ?? 0) - draggedDistance
|
|
212
|
+
: (activeSnapPointOffset ?? 0) + draggedDistance;
|
|
213
|
+
const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
|
|
214
|
+
const isFirst = activeSnapPointIndex === 0;
|
|
215
|
+
const hasDraggedUp = draggedDistance > 0;
|
|
216
|
+
|
|
217
|
+
if (isOverlaySnapPoint) {
|
|
218
|
+
set(overlayRef.current, {
|
|
219
|
+
transition: `opacity ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.OVERLAY_EXIT_TIMING_FUNCTION}`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!snapToSequentialPoint && velocity > 2 && !hasDraggedUp) {
|
|
224
|
+
if (dismissible) closeDrawer(false, { reason: "drag", event });
|
|
225
|
+
else snapToPoint(snapPointsOffset[0]); // snap to initial point
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!snapToSequentialPoint && velocity > 2 && hasDraggedUp && snapPointsOffset && snapPoints) {
|
|
230
|
+
snapToPoint(snapPointsOffset[snapPoints.length - 1] as number);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Find the closest snap point to the current position
|
|
235
|
+
const closestSnapPoint = snapPointsOffset?.reduce((prev, curr) => {
|
|
236
|
+
if (typeof prev !== "number" || typeof curr !== "number") return prev;
|
|
237
|
+
|
|
238
|
+
return Math.abs(curr - currentPosition) < Math.abs(prev - currentPosition) ? curr : prev;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const dim = isVertical(direction) ? window.innerHeight : window.innerWidth;
|
|
242
|
+
if (velocity > VELOCITY_THRESHOLD && Math.abs(draggedDistance) < dim * 0.4) {
|
|
243
|
+
const dragDirection = hasDraggedUp ? 1 : -1; // 1 = up, -1 = down
|
|
244
|
+
|
|
245
|
+
// Don't do anything if we swipe upwards while being on the last snap point
|
|
246
|
+
if (dragDirection > 0 && isLastSnapPoint && snapPoints) {
|
|
247
|
+
snapToPoint(snapPointsOffset[snapPoints.length - 1]);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isFirst && dragDirection < 0 && dismissible) {
|
|
252
|
+
closeDrawer(false, { reason: "drag", event });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (activeSnapPointIndex === null) return;
|
|
256
|
+
|
|
257
|
+
snapToPoint(snapPointsOffset[activeSnapPointIndex + dragDirection]);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
snapToPoint(closestSnapPoint);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function onDrag({ draggedDistance }: { draggedDistance: number }) {
|
|
265
|
+
if (activeSnapPointOffset === null) return;
|
|
266
|
+
const newValue =
|
|
267
|
+
direction === "bottom" || direction === "right"
|
|
268
|
+
? activeSnapPointOffset - draggedDistance
|
|
269
|
+
: activeSnapPointOffset + draggedDistance;
|
|
270
|
+
|
|
271
|
+
// Don't do anything if we exceed the last(biggest) snap point
|
|
272
|
+
if (
|
|
273
|
+
(direction === "bottom" || direction === "right") &&
|
|
274
|
+
newValue < snapPointsOffset[snapPointsOffset.length - 1]
|
|
275
|
+
) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (
|
|
279
|
+
(direction === "top" || direction === "left") &&
|
|
280
|
+
newValue > snapPointsOffset[snapPointsOffset.length - 1]
|
|
281
|
+
) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
set(drawerRef.current, {
|
|
286
|
+
transform: isVertical(direction)
|
|
287
|
+
? `translate3d(0, ${newValue}px, 0)`
|
|
288
|
+
: `translate3d(${newValue}px, 0, 0)`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function getPercentageDragged(absDraggedDistance: number, isDraggingDown: boolean) {
|
|
293
|
+
if (
|
|
294
|
+
!snapPoints ||
|
|
295
|
+
typeof activeSnapPointIndex !== "number" ||
|
|
296
|
+
!snapPointsOffset ||
|
|
297
|
+
fadeFromIndex === undefined
|
|
298
|
+
)
|
|
299
|
+
return null;
|
|
300
|
+
|
|
301
|
+
// If this is true we are dragging to a snap point that is supposed to have an overlay
|
|
302
|
+
const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
|
|
303
|
+
const isOverlaySnapPointOrHigher = activeSnapPointIndex >= fadeFromIndex;
|
|
304
|
+
|
|
305
|
+
if (isOverlaySnapPointOrHigher && isDraggingDown) {
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Don't animate, but still use this one if we are dragging away from the overlaySnapPoint
|
|
310
|
+
if (isOverlaySnapPoint && !isDraggingDown) return 1;
|
|
311
|
+
if (!shouldFade && !isOverlaySnapPoint) return null;
|
|
312
|
+
|
|
313
|
+
// Either fadeFrom index or the one before
|
|
314
|
+
const targetSnapPointIndex = isOverlaySnapPoint
|
|
315
|
+
? activeSnapPointIndex + 1
|
|
316
|
+
: activeSnapPointIndex - 1;
|
|
317
|
+
|
|
318
|
+
if (fadeFromIndex === 0 && activeSnapPointIndex === 0) {
|
|
319
|
+
let firstSnapPoint = snapPoints[0];
|
|
320
|
+
if (typeof firstSnapPoint === "string") {
|
|
321
|
+
firstSnapPoint = Number.parseInt(firstSnapPoint, 10);
|
|
322
|
+
}
|
|
323
|
+
const snapPointDistance = firstSnapPoint;
|
|
324
|
+
const percentageDragged = absDraggedDistance / Math.abs(snapPointDistance);
|
|
325
|
+
return percentageDragged;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Get the distance from overlaySnapPoint to the one before or vice-versa to calculate the opacity percentage accordingly
|
|
329
|
+
const snapPointDistance = isOverlaySnapPoint
|
|
330
|
+
? snapPointsOffset[targetSnapPointIndex] - snapPointsOffset[targetSnapPointIndex - 1]
|
|
331
|
+
: snapPointsOffset[targetSnapPointIndex + 1] - snapPointsOffset[targetSnapPointIndex];
|
|
332
|
+
|
|
333
|
+
const percentageDragged = absDraggedDistance / Math.abs(snapPointDistance);
|
|
334
|
+
|
|
335
|
+
if (isOverlaySnapPoint) {
|
|
336
|
+
return 1 - percentageDragged;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return percentageDragged;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
isLastSnapPoint,
|
|
344
|
+
activeSnapPoint,
|
|
345
|
+
shouldFade,
|
|
346
|
+
getPercentageDragged,
|
|
347
|
+
setActiveSnapPoint,
|
|
348
|
+
activeSnapPointIndex,
|
|
349
|
+
onRelease,
|
|
350
|
+
onDrag,
|
|
351
|
+
snapPointsOffset,
|
|
352
|
+
};
|
|
353
|
+
}
|