@ringcentral/spring-ui 1.11.2 → 1.13.0
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/Accordion.js +90 -34
- package/AccordionHeader.js +72 -20
- package/Alert.js +120 -28
- package/Animation.js +2 -45
- package/Announcement.js +46 -35
- package/Autocomplete.js +1011 -580
- package/Avatar.js +134 -73
- package/AvatarGroup.js +50 -7
- package/Backdrop.js +45 -7
- package/Badge.js +77 -15
- package/Block.js +34 -12
- package/BlockHeader.js +55 -11
- package/BottomNavigation.js +181 -65
- package/BottomNavigationItem.js +93 -30
- package/Button.js +160 -24
- package/CallButton.js +53 -31
- package/Checkbox.js +88 -26
- package/Chip.js +126 -26
- package/CircularProgressIndicator.js +80 -8
- package/ClickAwayListener.js +4 -1
- package/ComponentsProps.js +1 -0
- package/DatePicker.js +611 -190
- package/DateUtilsContext.js +43 -25
- package/Dialer.js +576 -250
- package/Dialog.js +100 -45
- package/Divider.js +43 -7
- package/DragHandle.js +32 -6
- package/Drawer.js +117 -19
- package/Dropdown.js +154 -74
- package/EmptyState.js +63 -11
- package/FabButton.js +51 -17
- package/FocusTrap.js +109 -12
- package/FormField.js +106 -8
- package/FormLabel.js +81 -7
- package/Icon.js +1 -8
- package/IconButton.js +142 -26
- package/IconButtonGroup.js +35 -12
- package/InlineEditable.js +98 -44
- package/LinearProgressIndicator.js +73 -24
- package/Link.js +40 -11
- package/List.js +54 -9
- package/ListItem.js +152 -23
- package/ListItemText.js +44 -16
- package/ListNavigation.js +272 -20
- package/Menu.js +139 -23
- package/MenuActions.js +37 -12
- package/MenuDivider.js +34 -8
- package/MenuFooter.js +36 -12
- package/MenuHeader.js +54 -15
- package/MenuItem.js +402 -33
- package/MenuItemText.js +51 -8
- package/MenuList.js +87 -34
- package/Modal.js +345 -16
- package/NestedMenuContext.js +183 -11
- package/Option.js +32 -34
- package/Overlap.js +54 -7
- package/PageHeader.js +62 -24
- package/PickerTextField.js +156 -33
- package/Popover.js +262 -21
- package/Popper.js +130 -77
- package/PopperPaper.js +54 -8
- package/Portal.js +6 -1
- package/Radio.js +65 -21
- package/RadioGroup.js +56 -17
- package/Rating.js +223 -87
- package/Select.js +387 -41
- package/SingleFilter.js +148 -77
- package/Skeleton.js +68 -17
- package/Slider.js +168 -69
- package/Snackbar.js +122 -59
- package/SortableList.js +32 -12
- package/SortableListDndContext.js +69 -22
- package/SortableListItem.js +92 -47
- package/SortableVirtualizedList.js +52 -28
- package/SplitButton.js +124 -86
- package/Squircle.js +4 -7
- package/StatusIndicator.js +51 -9
- package/Step.js +63 -38
- package/StepButton.js +51 -23
- package/StepConnector.js +48 -8
- package/StepContent.js +68 -25
- package/StepIcon.js +90 -9
- package/StepLabel.js +90 -14
- package/Stepper.js +65 -23
- package/SubMenu.js +203 -99
- package/SwipeableDrawer.js +439 -194
- package/Switch.js +70 -23
- package/Tab.js +120 -57
- package/TabContext.js +59 -15
- package/TabPanel.js +40 -13
- package/Table.js +60 -17
- package/TableBody.js +42 -12
- package/TableCell.js +84 -50
- package/TableContainer.js +32 -10
- package/TableFooter.js +38 -10
- package/TableHead.js +49 -15
- package/TablePagination.js +153 -77
- package/TableRow.js +60 -21
- package/Tabs.js +593 -268
- package/Tag.js +51 -16
- package/Text.js +68 -37
- package/TextField.js +209 -29
- package/Textarea.js +51 -40
- package/TimePicker.js +490 -230
- package/Tooltip.js +48 -23
- package/Tray.js +237 -13
- package/TrayMenu.js +62 -41
- package/VirtualizedList.js +208 -109
- package/VirtualizedMenu.js +359 -29
- package/VisuallyHidden.js +29 -6
- package/chunks/1250.js +98 -0
- package/chunks/1294.js +126 -0
- package/chunks/2956.js +18 -0
- package/chunks/4476.js +460 -0
- package/chunks/4491.js +10 -0
- package/chunks/4535.js +364 -0
- package/chunks/5047.js +3 -0
- package/chunks/6335.js +24 -0
- package/chunks/7279.js +10 -0
- package/chunks/8011.js +8 -0
- package/chunks/8307.js +56 -0
- package/chunks/8467.js +8 -0
- package/chunks/8809.js +8 -0
- package/chunks/{RadioGroup.context.js → 9551.js} +12 -6
- package/chunks/987.js +8 -0
- package/index.css +2624 -2416
- package/index.js +245 -193
- package/internal/utils/index.d.ts +1 -0
- package/internal/utils/setRef.d.ts +2 -0
- package/package.json +8 -8
- package/src/Alert/Alert.type.d.ts +2 -1
- package/src/Animation/components/Fade/Fade.d.ts +4 -2
- package/src/Animation/components/Grow/Grow.d.ts +4 -2
- package/src/Animation/components/ShowHideEye/ShowHideEye.d.ts +1 -1
- package/src/Animation/components/SlideIn/SlideIn.d.ts +4 -2
- package/src/Animation/components/SlideIn/getSlideIn.d.ts +1 -1
- package/src/Animation/hooks/useMotionSafeTransition/useMotionSafeTransition.d.ts +2 -0
- package/src/Autocomplete/SuggestionList/SuggestionList.d.ts +1 -1
- package/src/Autocomplete/useAutocomplete.d.ts +18 -18
- package/src/Autocomplete/useAutocompleteTag.d.ts +18 -18
- package/src/Badge/Badge.d.ts +2 -1
- package/src/Button/Button.classes.d.ts +1 -1
- package/src/ComponentsProps.d.ts +111 -110
- package/src/DatePicker/DatePickerDataAttributes.d.ts +11 -0
- package/src/DatePicker/utils/DatePickerAriaLabelUtils.d.ts +2 -1
- package/src/DatePicker/utils/DatePickerUtils.d.ts +1 -1
- package/src/Dialer/DialDelete/DialDelete.d.ts +2 -2
- package/src/Dialer/DialPad/DialPad.d.ts +2 -2
- package/src/Dialer/Dialer.d.ts +4 -2
- package/src/Dropdown/Dropdown.type.d.ts +2 -2
- package/src/InlineEditable/InlineEditable.type.d.ts +2 -1
- package/src/ListNavigation/useListNavigation.d.ts +3 -3
- package/src/MenuActions/MenuActions.d.ts +2 -2
- package/src/MenuItem/MenuItem.type.d.ts +1 -0
- package/src/MenuItem/useMenuItem.d.ts +1 -1
- package/src/MenuItem/useVirtualizedMenuItem.d.ts +1 -1
- package/src/MenuList/MenuList.type.d.ts +1 -1
- package/src/MenuList/MenuListContext.d.ts +2 -3
- package/src/Modal/Modal.type.d.ts +1 -0
- package/src/NestedMenuContext/MenuOverlayGroupContext.d.ts +2 -2
- package/src/Popover/Popover.classes.d.ts +1 -1
- package/src/Popover/Popover.internal.d.ts +8 -0
- package/src/Popover/Popover.type.d.ts +1 -0
- package/src/Popper/Popper.type.d.ts +10 -7
- package/src/Rating/utils.d.ts +3 -2
- package/src/Select/Select.type.d.ts +7 -5
- package/src/Slider/Slider.d.ts +2 -2
- package/src/Slider/SliderThumb.d.ts +2 -1
- package/src/Squircle/Squircle.d.ts +1 -1
- package/src/StatusIndicator/StatusIndicator.d.ts +2 -1
- package/src/SubMenu/PopperMenu.type.d.ts +4 -1
- package/src/Table/Table.type.d.ts +3 -0
- package/src/Table/TableContext.d.ts +3 -0
- package/src/Table/TableRowContext.d.ts +7 -0
- package/src/Table/index.d.ts +1 -0
- package/src/TableCell/TableCell.classes.d.ts +1 -1
- package/src/TableFooter/TableFooter.classes.d.ts +1 -1
- package/src/TableFooter/TableFooter.type.d.ts +1 -0
- package/src/TableHead/TableHead.classes.d.ts +1 -1
- package/src/TableHead/TableHead.type.d.ts +1 -0
- package/src/TableRow/TableRow.type.d.ts +2 -0
- package/chunks/AccordionContext.js +0 -6
- package/chunks/Alert.js +0 -48
- package/chunks/AvatarGroup.js +0 -22
- package/chunks/Backdrop.js +0 -18
- package/chunks/Badge.js +0 -32
- package/chunks/BottomNavigationItem.js +0 -36
- package/chunks/Button.js +0 -88
- package/chunks/Chip.js +0 -73
- package/chunks/CircularProgressIndicator.js +0 -36
- package/chunks/Divider.js +0 -19
- package/chunks/DragHandle.js +0 -11
- package/chunks/Drawer.js +0 -32
- package/chunks/ExpandCollapseCaret.js +0 -24
- package/chunks/FormLabel.js +0 -27
- package/chunks/Icon.js +0 -25
- package/chunks/IconButtonLabel.js +0 -68
- package/chunks/List.js +0 -22
- package/chunks/ListItem.js +0 -83
- package/chunks/Menu.js +0 -61
- package/chunks/MenuDivider.js +0 -14
- package/chunks/MenuItemText.js +0 -17
- package/chunks/MenuList.js +0 -46
- package/chunks/Modal.js +0 -212
- package/chunks/Overlap.js +0 -25
- package/chunks/PickerTextField.js +0 -85
- package/chunks/Popover.js +0 -53
- package/chunks/PopperPaper.js +0 -15
- package/chunks/Select.js +0 -220
- package/chunks/ShowHideEye.js +0 -56
- package/chunks/SlideIn.js +0 -19
- package/chunks/SortableArrow.js +0 -73
- package/chunks/Squircle.js +0 -31
- package/chunks/StatusIndicator.js +0 -20
- package/chunks/StepConnector.js +0 -21
- package/chunks/StepIcon.js +0 -43
- package/chunks/StepLabel.js +0 -29
- package/chunks/StepperContext.js +0 -9
- package/chunks/TableContext.js +0 -6
- package/chunks/Tablelvl2Context.js +0 -6
- package/chunks/TabsContext.js +0 -6
- package/chunks/TextField.js +0 -110
- package/chunks/Tooltip.js +0 -217
- package/chunks/VirtualizedMenu.js +0 -164
- package/chunks/VisuallyHidden.js +0 -12
- package/chunks/getFade.js +0 -27
- package/chunks/getGrow.js +0 -70
- package/chunks/getSlideIn.js +0 -30
- package/chunks/hasContent.js +0 -3
- package/chunks/useAnimationFrame.js +0 -15
- package/chunks/useFocusTrapStack.js +0 -36
- package/chunks/useHelperTextId.js +0 -22
- package/chunks/useListNavigation.js +0 -199
- package/chunks/useMenuItemSelect.js +0 -273
- package/chunks/useMotionSafeTransition.js +0 -9
- package/chunks/useNestedMenuItem.js +0 -27
- package/chunks/useNestedParentMenu.js +0 -113
- package/chunks/usePrefersReducedMotion.js +0 -36
- package/chunks/useTooltipForceHide.js +0 -27
- package/chunks/useTrayNavigate.js +0 -150
- package/chunks/withTooltip.js +0 -15
package/SwipeableDrawer.js
CHANGED
|
@@ -1,35 +1,47 @@
|
|
|
1
|
-
import { jsx, jsxs
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { useThemeProps } from
|
|
5
|
-
import clsx from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useClasses, useEventCallback, useWindowContext } from "@ringcentral/spring-base";
|
|
4
|
+
import { useThemeProps } from "@ringcentral/spring-theme";
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import { animate, useMotionValue, useTransform } from "framer-motion";
|
|
7
|
+
import { rubberbandIfOutOfBounds, useDrag } from "@use-gesture/react";
|
|
8
|
+
import { ResizeObserver } from "@juggle/resize-observer";
|
|
9
|
+
import { useAnimationFrame } from "./chunks/6335.js";
|
|
10
|
+
import { Drawer as Drawer_Drawer } from "./Drawer.js";
|
|
11
|
+
import { DragHandle as DragHandle_DragHandle } from "./DragHandle.js";
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
// extracted by css-extract-rspack-plugin
|
|
19
|
+
/* export default */ var SwipeableDrawer = ({"sui-swipeable-drawer-header-drag":"sui-swipeable-drawer-header-drag"});
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// Keeps track of whether everything is good to go or not, in the most efficient way possible
|
|
22
26
|
|
|
23
27
|
function useReady() {
|
|
24
28
|
const [ready, setReady] = useState(false);
|
|
25
29
|
const [readyMap, updateReadyMap] = useState({});
|
|
26
|
-
const registerReady = useCallback((key)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
const registerReady = useCallback((key)=>{
|
|
31
|
+
// Register the check we're gonna wait for until it's ready
|
|
32
|
+
updateReadyMap((ready)=>({
|
|
33
|
+
...ready,
|
|
34
|
+
[key]: false
|
|
35
|
+
}));
|
|
36
|
+
return ()=>{
|
|
37
|
+
// Set it to ready
|
|
38
|
+
updateReadyMap((ready)=>({
|
|
39
|
+
...ready,
|
|
40
|
+
[key]: true
|
|
41
|
+
}));
|
|
30
42
|
};
|
|
31
43
|
}, []);
|
|
32
|
-
useEffect(()
|
|
44
|
+
useEffect(()=>{
|
|
33
45
|
const states = Object.values(readyMap);
|
|
34
46
|
if (states.length === 0) {
|
|
35
47
|
return;
|
|
@@ -37,76 +49,106 @@ function useReady() {
|
|
|
37
49
|
const isReady = states.every(Boolean);
|
|
38
50
|
if (isReady) {
|
|
39
51
|
setReady(true);
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
52
|
+
} else {
|
|
42
53
|
setReady(false);
|
|
43
54
|
}
|
|
44
|
-
}, [
|
|
45
|
-
|
|
55
|
+
}, [
|
|
56
|
+
readyMap
|
|
57
|
+
]);
|
|
58
|
+
return {
|
|
59
|
+
ready,
|
|
60
|
+
registerReady
|
|
61
|
+
};
|
|
46
62
|
}
|
|
47
63
|
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// Hook for determining the size of an element using the Resize Observer API
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
48
72
|
const observerOptions = {
|
|
49
|
-
|
|
73
|
+
// Respond to changes to padding, happens often on iOS when using env(safe-area-inset-bottom)
|
|
74
|
+
// And the user hides or shows the Safari browser toolbar
|
|
75
|
+
box: 'border-box'
|
|
50
76
|
};
|
|
51
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Hook for determining the size of an element using the Resize Observer API.
|
|
79
|
+
*
|
|
80
|
+
* @param ref - A React ref to an element
|
|
81
|
+
* @param options - Configuration options
|
|
82
|
+
* @param options.enabled - Whether the observer is enabled
|
|
83
|
+
* @param options.resizeSourceRef - Optional ref to track resize source
|
|
84
|
+
*/ function useElementSizeObserver(ref, { enabled, resizeSourceRef }) {
|
|
52
85
|
const { requestAnimationFrame, cancelAnimationFrame } = useAnimationFrame();
|
|
53
86
|
const [size, setSize] = useState(0);
|
|
54
|
-
const handleResize = useCallback((entries)
|
|
87
|
+
const handleResize = useCallback((entries)=>{
|
|
88
|
+
// we only observe one element, so accessing the first entry here is fine
|
|
55
89
|
const entry = entries?.[0];
|
|
56
90
|
if (!entry) {
|
|
91
|
+
// Early return if no entry - can happen during observer disconnect
|
|
57
92
|
return;
|
|
58
93
|
}
|
|
59
94
|
const el = entry.target;
|
|
60
95
|
if (!el) {
|
|
96
|
+
// Early return if no target element
|
|
61
97
|
return;
|
|
62
98
|
}
|
|
99
|
+
// Prefer intrinsic content size to visible height so drag doesn't change the measured height.
|
|
100
|
+
// @juggle/resize-observer provides consistent borderBoxSize access across browsers
|
|
63
101
|
const roBlock = entry?.borderBoxSize?.[0]?.blockSize ?? 0;
|
|
64
102
|
const contentRectH = entry?.contentRect?.height ?? 0;
|
|
65
103
|
const scrollH = el?.scrollHeight ?? 0;
|
|
66
104
|
const offsetH = el?.offsetHeight ?? 0;
|
|
67
105
|
const newSize = Math.max(scrollH, roBlock, contentRectH, offsetH);
|
|
68
106
|
setSize(newSize);
|
|
107
|
+
// Update resize source if provided
|
|
69
108
|
if (resizeSourceRef) {
|
|
70
109
|
resizeSourceRef.current = 'element';
|
|
71
110
|
}
|
|
72
|
-
}, [
|
|
73
|
-
|
|
111
|
+
}, [
|
|
112
|
+
resizeSourceRef
|
|
113
|
+
]);
|
|
114
|
+
useLayoutEffect(()=>{
|
|
74
115
|
if (!enabled) {
|
|
75
116
|
return;
|
|
76
117
|
}
|
|
77
118
|
let resizeObserver = null;
|
|
78
119
|
let rafId = 0;
|
|
79
120
|
let initialMeasurementRafId = 0;
|
|
80
|
-
const startObserving = ()
|
|
121
|
+
const startObserving = ()=>{
|
|
81
122
|
const el = ref.current;
|
|
82
|
-
if (!el || !el.isConnected)
|
|
83
|
-
|
|
123
|
+
if (!el || !el.isConnected) return;
|
|
124
|
+
// @juggle/resize-observer provides cross-browser compatibility and polyfills
|
|
84
125
|
resizeObserver = new ResizeObserver(handleResize);
|
|
85
126
|
resizeObserver.observe(el, observerOptions);
|
|
86
|
-
|
|
127
|
+
// Seed an initial measurement in case RO hasn't fired yet
|
|
128
|
+
initialMeasurementRafId = requestAnimationFrame(()=>{
|
|
87
129
|
const initial = Math.max(el.scrollHeight, el.offsetHeight, el.getBoundingClientRect().height);
|
|
88
130
|
if (initial > 0) {
|
|
89
131
|
setSize(initial);
|
|
90
132
|
}
|
|
133
|
+
// else keep waiting for RO to update on next ticks
|
|
91
134
|
});
|
|
92
135
|
};
|
|
93
|
-
|
|
136
|
+
// If the element is not yet mounted (e.g. Modal shouldRender=false), keep
|
|
137
|
+
// polling via rAF until it's available, then attach the observer.
|
|
138
|
+
const waitForElement = ()=>{
|
|
94
139
|
if (ref.current) {
|
|
95
140
|
startObserving();
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
141
|
+
} else {
|
|
98
142
|
rafId = requestAnimationFrame(waitForElement);
|
|
99
143
|
}
|
|
100
144
|
};
|
|
101
145
|
if (ref.current) {
|
|
102
146
|
startObserving();
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
147
|
+
} else {
|
|
105
148
|
rafId = requestAnimationFrame(waitForElement);
|
|
106
149
|
}
|
|
107
|
-
return ()
|
|
108
|
-
if (rafId)
|
|
109
|
-
cancelAnimationFrame(rafId);
|
|
150
|
+
return ()=>{
|
|
151
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
110
152
|
if (initialMeasurementRafId) {
|
|
111
153
|
cancelAnimationFrame(initialMeasurementRafId);
|
|
112
154
|
}
|
|
@@ -114,95 +156,138 @@ function useElementSizeObserver(ref, { enabled, resizeSourceRef, }) {
|
|
|
114
156
|
resizeObserver.disconnect();
|
|
115
157
|
}
|
|
116
158
|
};
|
|
117
|
-
}, [
|
|
159
|
+
}, [
|
|
160
|
+
ref,
|
|
161
|
+
handleResize,
|
|
162
|
+
enabled,
|
|
163
|
+
requestAnimationFrame,
|
|
164
|
+
cancelAnimationFrame
|
|
165
|
+
]);
|
|
118
166
|
return enabled ? size : 0;
|
|
119
167
|
}
|
|
120
168
|
|
|
169
|
+
// Hook for measuring drawer dimensions
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
// Hook for tracking viewport height
|
|
121
175
|
function useMaxHeight(registerReady) {
|
|
122
176
|
const { externalWindow, globalThis: contextGlobal } = useWindowContext();
|
|
123
177
|
const win = externalWindow ?? contextGlobal;
|
|
124
178
|
const { requestAnimationFrame, cancelAnimationFrame } = useAnimationFrame();
|
|
125
|
-
const setReady = useMemo(()
|
|
179
|
+
const setReady = useMemo(()=>{
|
|
126
180
|
return registerReady ? registerReady('maxHeight') : undefined;
|
|
127
|
-
}, [
|
|
128
|
-
|
|
181
|
+
}, [
|
|
182
|
+
registerReady
|
|
183
|
+
]);
|
|
184
|
+
const [maxHeight, setMaxHeight] = useState(()=>win.innerHeight ?? 0);
|
|
129
185
|
const raf = useRef(0);
|
|
130
186
|
const maxReadyCalledRef = useRef(false);
|
|
131
|
-
|
|
187
|
+
// Reset the ready flag when setReady changes (drawer reset)
|
|
188
|
+
useLayoutEffect(()=>{
|
|
132
189
|
maxReadyCalledRef.current = false;
|
|
133
|
-
}, [
|
|
134
|
-
|
|
135
|
-
|
|
190
|
+
}, [
|
|
191
|
+
setReady
|
|
192
|
+
]);
|
|
193
|
+
useLayoutEffect(()=>{
|
|
194
|
+
const handleResize = ()=>{
|
|
136
195
|
if (raf.current) {
|
|
196
|
+
// bail to throttle the amount of resize changes
|
|
137
197
|
return;
|
|
138
198
|
}
|
|
139
|
-
|
|
199
|
+
// throttle state changes using rAF from target window
|
|
200
|
+
raf.current = requestAnimationFrame(()=>{
|
|
201
|
+
// Check if cleanup already ran (raf.current would be 0)
|
|
140
202
|
if (raf.current !== 0) {
|
|
141
|
-
setMaxHeight(win.innerHeight);
|
|
203
|
+
setMaxHeight(win.innerHeight); // Safe to update
|
|
142
204
|
raf.current = 0;
|
|
143
205
|
}
|
|
144
206
|
});
|
|
145
207
|
};
|
|
146
208
|
win.addEventListener('resize', handleResize);
|
|
147
209
|
setMaxHeight(win.innerHeight);
|
|
210
|
+
// Call setReady exactly once for window height readiness
|
|
148
211
|
if (setReady && !maxReadyCalledRef.current) {
|
|
149
212
|
maxReadyCalledRef.current = true;
|
|
150
213
|
setReady();
|
|
151
214
|
}
|
|
152
|
-
return ()
|
|
215
|
+
return ()=>{
|
|
153
216
|
win.removeEventListener('resize', handleResize);
|
|
154
217
|
cancelAnimationFrame(raf.current);
|
|
155
218
|
};
|
|
156
|
-
}, [
|
|
219
|
+
}, [
|
|
220
|
+
setReady,
|
|
221
|
+
win,
|
|
222
|
+
requestAnimationFrame,
|
|
223
|
+
cancelAnimationFrame
|
|
224
|
+
]);
|
|
157
225
|
return maxHeight;
|
|
158
226
|
}
|
|
159
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Custom hook to preserve last non-zero height values
|
|
229
|
+
*/ function usePreservedHeight(currentHeight) {
|
|
160
230
|
const lastHeightRef = useRef(0);
|
|
161
|
-
useLayoutEffect(()
|
|
231
|
+
useLayoutEffect(()=>{
|
|
162
232
|
if (currentHeight > 0) {
|
|
163
233
|
lastHeightRef.current = currentHeight;
|
|
164
234
|
}
|
|
165
|
-
}, [
|
|
235
|
+
}, [
|
|
236
|
+
currentHeight
|
|
237
|
+
]);
|
|
166
238
|
return currentHeight > 0 ? currentHeight : lastHeightRef.current;
|
|
167
239
|
}
|
|
168
|
-
function useDimensions({ contentRef, footerEnabled, footerRef, headerEnabled, headerRef, registerReady, resizeSourceRef, open
|
|
169
|
-
const setReady = useMemo(()
|
|
240
|
+
function useDimensions({ contentRef, footerEnabled, footerRef, headerEnabled, headerRef, registerReady, resizeSourceRef, open }) {
|
|
241
|
+
const setReady = useMemo(()=>registerReady('contentHeight'), [
|
|
242
|
+
registerReady
|
|
243
|
+
]);
|
|
244
|
+
// Do not re-register contentHeight readiness on disconnect; register once above and let
|
|
245
|
+
// the ResizeObserver drive readiness when size > 0. This avoids readiness loops.
|
|
246
|
+
// (Intentionally removed disconnection re-register logic)
|
|
170
247
|
const maxHeight = useMaxHeight(registerReady);
|
|
248
|
+
// Measure all elements - only when drawer is open
|
|
171
249
|
const headerHeight = useElementSizeObserver(headerRef, {
|
|
172
250
|
enabled: open && headerEnabled,
|
|
173
|
-
resizeSourceRef
|
|
251
|
+
resizeSourceRef
|
|
174
252
|
});
|
|
175
253
|
const contentHeight = useElementSizeObserver(contentRef, {
|
|
176
254
|
enabled: open,
|
|
177
|
-
resizeSourceRef
|
|
255
|
+
resizeSourceRef
|
|
178
256
|
});
|
|
179
257
|
const footerHeight = useElementSizeObserver(footerRef, {
|
|
180
258
|
enabled: open && footerEnabled,
|
|
181
|
-
resizeSourceRef
|
|
259
|
+
resizeSourceRef
|
|
182
260
|
});
|
|
261
|
+
// Preserve last non-zero heights to avoid collapsing to 0 when the
|
|
262
|
+
// modal/portal temporarily unmounts or RO reports 0 during transitions
|
|
183
263
|
const effectiveContentHeight = usePreservedHeight(contentHeight);
|
|
184
264
|
const effectiveHeaderHeight = usePreservedHeight(headerHeight);
|
|
185
265
|
const effectiveFooterHeight = usePreservedHeight(footerHeight);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
266
|
+
// When content is measured (>0), mark ready for contentHeight
|
|
267
|
+
useLayoutEffect(()=>{
|
|
268
|
+
if (!setReady) return;
|
|
189
269
|
const ok = contentHeight > 0;
|
|
190
270
|
if (ok) {
|
|
191
271
|
setReady();
|
|
192
272
|
}
|
|
193
|
-
}, [
|
|
273
|
+
}, [
|
|
274
|
+
contentHeight,
|
|
275
|
+
contentRef,
|
|
276
|
+
setReady
|
|
277
|
+
]);
|
|
278
|
+
// Calculate available space for content, ensuring it's never negative
|
|
194
279
|
const availableContentSpace = Math.max(0, maxHeight - effectiveHeaderHeight - effectiveFooterHeight);
|
|
195
|
-
const minHeight = Math.min(availableContentSpace, effectiveContentHeight) +
|
|
196
|
-
effectiveHeaderHeight +
|
|
197
|
-
effectiveFooterHeight;
|
|
280
|
+
const minHeight = Math.min(availableContentSpace, effectiveContentHeight) + effectiveHeaderHeight + effectiveFooterHeight;
|
|
198
281
|
return {
|
|
199
282
|
maxHeight,
|
|
200
283
|
minHeight,
|
|
201
284
|
headerHeight,
|
|
202
|
-
footerHeight: effectiveFooterHeight
|
|
285
|
+
footerHeight: effectiveFooterHeight
|
|
203
286
|
};
|
|
204
287
|
}
|
|
205
288
|
|
|
289
|
+
// Hook for calculating snap points
|
|
290
|
+
|
|
206
291
|
function roundAndCheckForNaN(value) {
|
|
207
292
|
const rounded = Math.round(value);
|
|
208
293
|
if (Number.isNaN(rounded)) {
|
|
@@ -210,12 +295,15 @@ function roundAndCheckForNaN(value) {
|
|
|
210
295
|
}
|
|
211
296
|
return rounded;
|
|
212
297
|
}
|
|
298
|
+
// Clamp function with clear NaN handling
|
|
213
299
|
function clamp(number, lower, upper) {
|
|
214
300
|
number = +number;
|
|
215
301
|
lower = +lower;
|
|
216
302
|
upper = +upper;
|
|
303
|
+
// Handle NaN values with sensible defaults
|
|
217
304
|
const safeLower = Number.isNaN(lower) ? 0 : lower;
|
|
218
305
|
const safeUpper = Number.isNaN(upper) ? Number.POSITIVE_INFINITY : upper;
|
|
306
|
+
// Ensure lower <= upper to avoid invalid ranges
|
|
219
307
|
const finalLower = Math.min(safeLower, safeUpper);
|
|
220
308
|
const finalUpper = Math.max(safeLower, safeUpper);
|
|
221
309
|
if (Number.isNaN(number)) {
|
|
@@ -224,20 +312,20 @@ function clamp(number, lower, upper) {
|
|
|
224
312
|
return Math.min(Math.max(number, finalLower), finalUpper);
|
|
225
313
|
}
|
|
226
314
|
function processSnapPoints(snapPoints, maxHeight) {
|
|
315
|
+
// Validate maxHeight input
|
|
227
316
|
if (!Number.isFinite(maxHeight) || maxHeight <= 0) {
|
|
228
317
|
throw new Error(`Invalid maxHeight: ${maxHeight}. Must be a positive finite number.`);
|
|
229
318
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const snapPointsDedupedSet = safeSnaps.reduce((acc, snapPoint) => {
|
|
319
|
+
// Match React Spring Bottom Sheet implementation exactly
|
|
320
|
+
const safeSnaps = [].concat(snapPoints).map(roundAndCheckForNaN);
|
|
321
|
+
const snapPointsDedupedSet = safeSnaps.reduce((acc, snapPoint)=>{
|
|
234
322
|
acc.add(clamp(snapPoint, 0, maxHeight));
|
|
235
323
|
return acc;
|
|
236
324
|
}, new Set());
|
|
237
325
|
const processedPoints = Array.from(snapPointsDedupedSet);
|
|
326
|
+
// Handle empty snap points array
|
|
238
327
|
if (processedPoints.length === 0) {
|
|
239
|
-
throw new Error('No valid snap points available. The snapPoints function must return at least one valid number.\n' +
|
|
240
|
-
'Example: snapPoints: ({ minHeight, maxHeight }) => [minHeight, maxHeight]');
|
|
328
|
+
throw new Error('No valid snap points available. The snapPoints function must return at least one valid number.\n' + 'Example: snapPoints: ({ minHeight, maxHeight }) => [minHeight, maxHeight]');
|
|
241
329
|
}
|
|
242
330
|
const minSnap = Math.min(...processedPoints);
|
|
243
331
|
if (Number.isNaN(minSnap)) {
|
|
@@ -250,10 +338,10 @@ function processSnapPoints(snapPoints, maxHeight) {
|
|
|
250
338
|
return {
|
|
251
339
|
snapPoints: processedPoints,
|
|
252
340
|
minSnap,
|
|
253
|
-
maxSnap
|
|
341
|
+
maxSnap
|
|
254
342
|
};
|
|
255
343
|
}
|
|
256
|
-
function useSnapPoints({ contentRef, footerEnabled, footerRef, getSnapPoints, headerEnabled, headerRef, heightRef, lastSnapRef, ready, registerReady, resizeSourceRef, open
|
|
344
|
+
function useSnapPoints({ contentRef, footerEnabled, footerRef, getSnapPoints, headerEnabled, headerRef, heightRef, lastSnapRef, ready, registerReady, resizeSourceRef, open }) {
|
|
257
345
|
const { maxHeight, minHeight, headerHeight, footerHeight } = useDimensions({
|
|
258
346
|
contentRef,
|
|
259
347
|
footerEnabled,
|
|
@@ -262,18 +350,19 @@ function useSnapPoints({ contentRef, footerEnabled, footerRef, getSnapPoints, he
|
|
|
262
350
|
headerRef,
|
|
263
351
|
registerReady,
|
|
264
352
|
resizeSourceRef,
|
|
265
|
-
open
|
|
353
|
+
open
|
|
266
354
|
});
|
|
267
|
-
const { snapPoints, minSnap, maxSnap } = processSnapPoints(ready
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
355
|
+
const { snapPoints, minSnap, maxSnap } = processSnapPoints(ready ? getSnapPoints({
|
|
356
|
+
height: heightRef.current ?? 0,
|
|
357
|
+
footerHeight,
|
|
358
|
+
headerHeight,
|
|
359
|
+
minHeight,
|
|
360
|
+
maxHeight
|
|
361
|
+
}) : [
|
|
362
|
+
0
|
|
363
|
+
], maxHeight);
|
|
276
364
|
function findSnap(numberOrCallback) {
|
|
365
|
+
// Defensive check: should never happen due to processSnapPoints validation
|
|
277
366
|
if (snapPoints.length === 0) {
|
|
278
367
|
throw new Error('Cannot find snap point: no snap points available');
|
|
279
368
|
}
|
|
@@ -287,76 +376,106 @@ function useSnapPoints({ contentRef, footerEnabled, footerRef, getSnapPoints, he
|
|
|
287
376
|
minHeight,
|
|
288
377
|
maxHeight,
|
|
289
378
|
snapPoints,
|
|
290
|
-
lastSnap: lastSnapRef.current
|
|
379
|
+
lastSnap: lastSnapRef.current
|
|
291
380
|
});
|
|
292
|
-
}
|
|
293
|
-
|
|
381
|
+
} catch (error) {
|
|
382
|
+
// If callback throws, fall back to current position or minSnap
|
|
294
383
|
unsafeSearch = heightRef.current ?? minSnap;
|
|
295
384
|
}
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
385
|
+
} else {
|
|
298
386
|
unsafeSearch = numberOrCallback;
|
|
299
387
|
}
|
|
300
388
|
let querySnap;
|
|
301
389
|
try {
|
|
302
390
|
querySnap = roundAndCheckForNaN(unsafeSearch);
|
|
303
|
-
}
|
|
304
|
-
|
|
391
|
+
} catch (error) {
|
|
392
|
+
// If number is invalid (NaN, etc), fall back to current position or minSnap
|
|
305
393
|
querySnap = heightRef.current ?? minSnap;
|
|
306
394
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
Number.isInteger(querySnap);
|
|
395
|
+
// Support index-based selection: if querySnap is a small integer and within bounds, treat as index
|
|
396
|
+
const isValidIndex = querySnap >= 0 && querySnap < snapPoints.length && Number.isInteger(querySnap);
|
|
310
397
|
if (isValidIndex) {
|
|
311
398
|
return snapPoints[querySnap];
|
|
312
399
|
}
|
|
313
|
-
|
|
400
|
+
// Otherwise, find closest snap point by value
|
|
401
|
+
return snapPoints.reduce((prev, curr)=>Math.abs(curr - querySnap) < Math.abs(prev - querySnap) ? curr : prev, minSnap);
|
|
314
402
|
}
|
|
315
|
-
return {
|
|
403
|
+
return {
|
|
404
|
+
minSnap,
|
|
405
|
+
maxSnap,
|
|
406
|
+
findSnap,
|
|
407
|
+
maxHeight,
|
|
408
|
+
snapPoints
|
|
409
|
+
};
|
|
316
410
|
}
|
|
317
411
|
|
|
412
|
+
//implementation for Drawer gestures
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
// Standardized spring configuration
|
|
318
420
|
const SPRING_CONFIG = {
|
|
319
421
|
type: 'spring',
|
|
320
422
|
stiffness: 600,
|
|
321
423
|
damping: 50,
|
|
322
|
-
mass: 1
|
|
424
|
+
mass: 1
|
|
323
425
|
};
|
|
426
|
+
// Rubberband effect constants
|
|
324
427
|
const RUBBERBAND_RESISTANCE = 0.8;
|
|
325
428
|
const SINGLE_SNAP_STRETCH_FACTOR = 1.2;
|
|
326
429
|
const MULTI_SNAP_STRETCH_FACTOR = 1.8;
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
const
|
|
430
|
+
// Gesture behavior constants
|
|
431
|
+
const DISMISS_THRESHOLD = 50; // Minimum movement in pixels to trigger dismiss
|
|
432
|
+
const VELOCITY_THRESHOLD = 0.1; // Minimum velocity to apply to spring animation
|
|
433
|
+
const PREDICTION_MULTIPLIER = 2; // Multiplier for velocity-based position prediction
|
|
434
|
+
const DISMISS_POSITION_FACTOR = 2; // Divisor for calculating dismiss threshold (minSnap / 2)
|
|
435
|
+
// Default functions
|
|
331
436
|
function defaultSnapPoints({ minHeight }) {
|
|
332
|
-
|
|
437
|
+
// When no snap points are specified, just use content height
|
|
438
|
+
// This means the drawer shows content at natural size and can only dismiss
|
|
439
|
+
return [
|
|
440
|
+
minHeight
|
|
441
|
+
];
|
|
333
442
|
}
|
|
334
|
-
function defaultInitialSnap({ snapPoints, lastSnap: _lastSnap
|
|
443
|
+
function defaultInitialSnap({ snapPoints, lastSnap: _lastSnap }) {
|
|
444
|
+
// Default behavior: choose the smallest snap point (usually content height)
|
|
445
|
+
// Users can override this in their initialSnap function if they want lastSnap behavior
|
|
335
446
|
return Math.min(...snapPoints);
|
|
336
447
|
}
|
|
337
448
|
function useDrawerGestures(options) {
|
|
338
|
-
const { anchor, open, onClose, snapPoints: getSnapPoints = defaultSnapPoints, initialSnap: getInitialSnap = defaultInitialSnap, contentRef, headerRef, footerRef, headerEnabled, footerEnabled
|
|
449
|
+
const { anchor, open, onClose, snapPoints: getSnapPoints = defaultSnapPoints, initialSnap: getInitialSnap = defaultInitialSnap, contentRef, headerRef, footerRef, headerEnabled, footerEnabled } = options;
|
|
339
450
|
const { externalWindow } = useWindowContext();
|
|
340
451
|
const gestureWindow = externalWindow;
|
|
341
452
|
const { ready, registerReady } = useReady();
|
|
453
|
+
// Height tracking refs
|
|
342
454
|
const heightRef = useRef(0);
|
|
343
455
|
const lastSnapRef = useRef(null);
|
|
344
456
|
const resizeSourceRef = useRef('window');
|
|
457
|
+
// Animation cleanup ref to prevent memory leaks
|
|
345
458
|
const animationRef = useRef(null);
|
|
346
|
-
|
|
347
|
-
|
|
459
|
+
// Cleanup animations on unmount to prevent memory leaks
|
|
460
|
+
useLayoutEffect(()=>{
|
|
461
|
+
return ()=>{
|
|
348
462
|
animationRef.current?.stop();
|
|
349
463
|
};
|
|
350
464
|
}, []);
|
|
465
|
+
// Motion values
|
|
351
466
|
const heightMotionValue = useMotionValue(0);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
heightMotionValue.get() === 0) {
|
|
467
|
+
// We animate with spring only on release; during drag we set the value directly for immediate follow
|
|
468
|
+
// Ensure motion value starts with correct value when drawer opens
|
|
469
|
+
useLayoutEffect(()=>{
|
|
470
|
+
if (open && ready && heightRef.current > 0 && heightMotionValue.get() === 0) {
|
|
357
471
|
heightMotionValue.set(heightRef.current);
|
|
358
472
|
}
|
|
359
|
-
}, [
|
|
473
|
+
}, [
|
|
474
|
+
open,
|
|
475
|
+
ready,
|
|
476
|
+
heightMotionValue
|
|
477
|
+
]);
|
|
478
|
+
// Snap points calculation
|
|
360
479
|
const { minSnap, maxSnap, findSnap, maxHeight } = useSnapPoints({
|
|
361
480
|
contentRef: contentRef,
|
|
362
481
|
footerEnabled,
|
|
@@ -369,33 +488,48 @@ function useDrawerGestures(options) {
|
|
|
369
488
|
ready,
|
|
370
489
|
registerReady,
|
|
371
490
|
resizeSourceRef,
|
|
372
|
-
open
|
|
491
|
+
open
|
|
373
492
|
});
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
493
|
+
// Calculate initial snap
|
|
494
|
+
const defaultSnap = useMemo(()=>{
|
|
495
|
+
if (!ready) return 0;
|
|
496
|
+
// If initial snap is a function, it receives state via findSnap
|
|
377
497
|
return findSnap(getInitialSnap);
|
|
378
|
-
}, [
|
|
498
|
+
}, [
|
|
499
|
+
ready,
|
|
500
|
+
getInitialSnap,
|
|
501
|
+
findSnap
|
|
502
|
+
]);
|
|
503
|
+
// Only set the initial height once per open cycle when content is present
|
|
379
504
|
const didInitRef = useRef(false);
|
|
380
505
|
const isContentPresent = !!contentRef?.current && contentRef.current.isConnected;
|
|
381
|
-
useLayoutEffect(()
|
|
382
|
-
if (!open || !ready)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return;
|
|
506
|
+
useLayoutEffect(()=>{
|
|
507
|
+
if (!open || !ready) return;
|
|
508
|
+
if (didInitRef.current) return;
|
|
509
|
+
// Use the calculated defaultSnap (which may or may not include lastSnap based on user's initialSnap function)
|
|
386
510
|
const safeInitial = defaultSnap > 0 ? defaultSnap : minSnap;
|
|
387
|
-
|
|
388
|
-
|
|
511
|
+
// If we don't have content present and no valid fallback height, wait
|
|
512
|
+
if (!isContentPresent && safeInitial <= 0) return;
|
|
513
|
+
// 1. Set to 0 immediately
|
|
389
514
|
heightMotionValue.set(0);
|
|
515
|
+
// 2. Update heightRef
|
|
390
516
|
heightRef.current = safeInitial;
|
|
517
|
+
// 3. Animate to target position
|
|
391
518
|
animationRef.current = animate(heightMotionValue, safeInitial, SPRING_CONFIG);
|
|
392
519
|
didInitRef.current = true;
|
|
393
|
-
}, [
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
520
|
+
}, [
|
|
521
|
+
open,
|
|
522
|
+
ready,
|
|
523
|
+
isContentPresent,
|
|
524
|
+
defaultSnap,
|
|
525
|
+
minSnap,
|
|
526
|
+
heightMotionValue
|
|
527
|
+
]);
|
|
528
|
+
// Content resize:
|
|
529
|
+
useLayoutEffect(()=>{
|
|
530
|
+
if (!open || !ready || !isContentPresent) return;
|
|
531
|
+
if (!didInitRef.current) return; // Only after initial setup is complete
|
|
532
|
+
// Find closest snap point to current height (preserves user positioning)
|
|
399
533
|
const currentHeight = heightRef.current ?? 0;
|
|
400
534
|
if (currentHeight > 0) {
|
|
401
535
|
const closestSnap = findSnap(currentHeight);
|
|
@@ -403,66 +537,82 @@ function useDrawerGestures(options) {
|
|
|
403
537
|
heightRef.current = closestSnap;
|
|
404
538
|
lastSnapRef.current = closestSnap;
|
|
405
539
|
}
|
|
406
|
-
}, [
|
|
407
|
-
|
|
540
|
+
}, [
|
|
541
|
+
minSnap,
|
|
542
|
+
maxSnap
|
|
543
|
+
]); // Triggers when snap points change (content resize)
|
|
544
|
+
// Reset init flag when closing so next open can initialize again
|
|
545
|
+
useLayoutEffect(()=>{
|
|
408
546
|
if (!open) {
|
|
409
547
|
didInitRef.current = false;
|
|
548
|
+
// Preserve the current height as lastSnap for next open
|
|
410
549
|
if (heightRef.current > 0) {
|
|
411
550
|
lastSnapRef.current = heightRef.current;
|
|
412
551
|
}
|
|
552
|
+
// Animate motion value to 0 on close
|
|
413
553
|
animationRef.current = animate(heightMotionValue, 0, SPRING_CONFIG);
|
|
414
554
|
}
|
|
415
|
-
}, [
|
|
416
|
-
|
|
555
|
+
}, [
|
|
556
|
+
open,
|
|
557
|
+
heightMotionValue
|
|
558
|
+
]);
|
|
559
|
+
// Drag handler
|
|
560
|
+
const handleDrag = useEventCallback(({ args: [{ closeOnTap = false } = {}] = [], cancel, down, last, memo = heightMotionValue.get(), movement: [_mx, _my], tap, velocity: [_, velocityY], event })=>{
|
|
417
561
|
if (!ready) {
|
|
418
562
|
cancel();
|
|
419
563
|
return memo;
|
|
420
564
|
}
|
|
565
|
+
// Handle close on tap (backdrop tap)
|
|
421
566
|
if (closeOnTap && tap) {
|
|
422
567
|
cancel();
|
|
568
|
+
// Use timeout to avoid tap events on backdrop triggering click events underneath
|
|
423
569
|
onClose(event, 'gestureSwipe');
|
|
424
570
|
return memo;
|
|
425
571
|
}
|
|
572
|
+
// Filter out taps
|
|
426
573
|
if (tap) {
|
|
427
574
|
return memo;
|
|
428
575
|
}
|
|
429
|
-
|
|
430
|
-
|
|
576
|
+
// dismissal logic for all anchors
|
|
577
|
+
const getDismissInfo = ()=>{
|
|
578
|
+
switch(anchor){
|
|
431
579
|
case 'bottom':
|
|
432
580
|
return {
|
|
433
581
|
isDismissDirection: _my > 0,
|
|
434
582
|
movement: _my,
|
|
435
|
-
threshold: DISMISS_THRESHOLD
|
|
583
|
+
threshold: DISMISS_THRESHOLD
|
|
436
584
|
};
|
|
437
585
|
case 'top':
|
|
438
586
|
return {
|
|
439
587
|
isDismissDirection: _my < 0,
|
|
440
588
|
movement: Math.abs(_my),
|
|
441
|
-
threshold: DISMISS_THRESHOLD
|
|
589
|
+
threshold: DISMISS_THRESHOLD
|
|
442
590
|
};
|
|
443
591
|
case 'left':
|
|
444
592
|
return {
|
|
445
593
|
isDismissDirection: _mx < 0,
|
|
446
594
|
movement: Math.abs(_mx),
|
|
447
|
-
threshold: DISMISS_THRESHOLD
|
|
595
|
+
threshold: DISMISS_THRESHOLD
|
|
448
596
|
};
|
|
449
597
|
case 'right':
|
|
450
598
|
return {
|
|
451
599
|
isDismissDirection: _mx > 0,
|
|
452
600
|
movement: Math.abs(_mx),
|
|
453
|
-
threshold: DISMISS_THRESHOLD
|
|
601
|
+
threshold: DISMISS_THRESHOLD
|
|
454
602
|
};
|
|
455
603
|
default:
|
|
456
604
|
return {
|
|
457
605
|
isDismissDirection: false,
|
|
458
606
|
movement: 0,
|
|
459
|
-
threshold: DISMISS_THRESHOLD
|
|
607
|
+
threshold: DISMISS_THRESHOLD
|
|
460
608
|
};
|
|
461
609
|
}
|
|
462
610
|
};
|
|
463
611
|
const { isDismissDirection, movement, threshold } = getDismissInfo();
|
|
612
|
+
// For horizontal drawers, only handle dismiss (no snap points)
|
|
464
613
|
const isVertical = anchor === 'top' || anchor === 'bottom';
|
|
465
614
|
if (!isVertical) {
|
|
615
|
+
// Check for dismiss condition (horizontal drawers only)
|
|
466
616
|
if (!down && isDismissDirection && movement > threshold) {
|
|
467
617
|
cancel();
|
|
468
618
|
onClose(event, 'gestureSwipe');
|
|
@@ -474,6 +624,7 @@ function useDrawerGestures(options) {
|
|
|
474
624
|
const rawY = memo + my;
|
|
475
625
|
const predictedDistance = my * velocityY;
|
|
476
626
|
const predictedY = Math.max(minSnap, Math.min(maxHeight, rawY + predictedDistance * PREDICTION_MULTIPLIER));
|
|
627
|
+
// Check for vertical dismissal - only when trying to go below minimum snap point
|
|
477
628
|
if (!down && isDismissDirection) {
|
|
478
629
|
const dismissThreshold = minSnap / DISMISS_POSITION_FACTOR;
|
|
479
630
|
const currentPosition = rawY + predictedDistance;
|
|
@@ -486,88 +637,145 @@ function useDrawerGestures(options) {
|
|
|
486
637
|
let newY;
|
|
487
638
|
if (down) {
|
|
488
639
|
if (minSnap === maxSnap) {
|
|
640
|
+
// Special case when min and max snap are the same (only one snap point)
|
|
489
641
|
if (rawY < minSnap) {
|
|
490
|
-
newY = rawY;
|
|
491
|
-
}
|
|
492
|
-
|
|
642
|
+
newY = rawY; // Allow smooth dragging down (toward dismiss) with no resistance
|
|
643
|
+
} else {
|
|
644
|
+
// Strong resistance when dragging up beyond content
|
|
493
645
|
newY = rubberbandIfOutOfBounds(rawY, minSnap, maxSnap * SINGLE_SNAP_STRETCH_FACTOR, RUBBERBAND_RESISTANCE);
|
|
494
646
|
}
|
|
495
|
-
}
|
|
496
|
-
|
|
647
|
+
} else {
|
|
648
|
+
// Multiple snap points: allow stretching beyond maxSnap for rubberband effect
|
|
497
649
|
newY = rubberbandIfOutOfBounds(rawY, 0, maxSnap * MULTI_SNAP_STRETCH_FACTOR, RUBBERBAND_RESISTANCE);
|
|
498
650
|
}
|
|
499
|
-
}
|
|
500
|
-
|
|
651
|
+
} else {
|
|
652
|
+
// On release: use predicted position
|
|
501
653
|
newY = predictedY;
|
|
502
654
|
}
|
|
503
655
|
if (last) {
|
|
656
|
+
// End of gesture: find closest snap point and animate to it
|
|
504
657
|
const targetSnap = findSnap(newY);
|
|
658
|
+
// Animate to target snap point using spring
|
|
505
659
|
animationRef.current = animate(heightMotionValue, targetSnap, {
|
|
506
660
|
...SPRING_CONFIG,
|
|
507
|
-
velocity: Math.abs(velocityY) > VELOCITY_THRESHOLD ? velocityY : 0
|
|
661
|
+
velocity: Math.abs(velocityY) > VELOCITY_THRESHOLD ? velocityY : 0
|
|
508
662
|
});
|
|
509
663
|
heightRef.current = targetSnap;
|
|
510
664
|
lastSnapRef.current = targetSnap;
|
|
511
665
|
return memo;
|
|
512
666
|
}
|
|
667
|
+
// During drag: directly set height to follow finger with rubberband
|
|
513
668
|
heightMotionValue.set(newY);
|
|
514
669
|
return memo;
|
|
515
670
|
});
|
|
671
|
+
// Gesture binding
|
|
516
672
|
const isVertical = anchor === 'top' || anchor === 'bottom';
|
|
517
|
-
const gestureConfig = useMemo(()
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
673
|
+
const gestureConfig = useMemo(()=>({
|
|
674
|
+
filterTaps: true,
|
|
675
|
+
...gestureWindow && {
|
|
676
|
+
window: gestureWindow
|
|
677
|
+
}
|
|
678
|
+
}), [
|
|
679
|
+
gestureWindow
|
|
680
|
+
]);
|
|
523
681
|
const bind = useDrag(handleDrag, gestureConfig);
|
|
524
|
-
|
|
682
|
+
// animate height for vertical drawers only
|
|
683
|
+
const heightStyle = useTransform(heightMotionValue, (y)=>{
|
|
684
|
+
// Ensure drawer never exceeds viewport bounds
|
|
525
685
|
const clamped = Math.max(0, Math.min(y, maxHeight));
|
|
526
686
|
return `${Math.round(clamped)}px`;
|
|
527
687
|
});
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
688
|
+
// Imperative snapTo function
|
|
689
|
+
const snapTo = useCallback((numberOrCallback)=>{
|
|
690
|
+
if (!ready) return;
|
|
531
691
|
const targetSnap = findSnap(numberOrCallback);
|
|
692
|
+
// Animate to target snap point
|
|
532
693
|
animationRef.current = animate(heightMotionValue, targetSnap, SPRING_CONFIG);
|
|
694
|
+
// Update refs
|
|
533
695
|
heightRef.current = targetSnap;
|
|
534
696
|
lastSnapRef.current = targetSnap;
|
|
535
|
-
}, [
|
|
697
|
+
}, [
|
|
698
|
+
ready,
|
|
699
|
+
findSnap,
|
|
700
|
+
heightMotionValue,
|
|
701
|
+
heightRef,
|
|
702
|
+
lastSnapRef
|
|
703
|
+
]);
|
|
536
704
|
return {
|
|
537
705
|
bind,
|
|
538
706
|
motionProps: {
|
|
539
|
-
style: ready && isVertical
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
: {},
|
|
707
|
+
style: ready && isVertical ? {
|
|
708
|
+
height: heightStyle
|
|
709
|
+
} : {}
|
|
544
710
|
},
|
|
545
711
|
isGestureEnabled: ready,
|
|
546
712
|
contentRef,
|
|
547
713
|
headerRef,
|
|
548
714
|
footerRef,
|
|
549
|
-
snapTo
|
|
715
|
+
snapTo
|
|
550
716
|
};
|
|
551
717
|
}
|
|
552
718
|
|
|
553
|
-
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* SwipeableDrawer Component - Gesture-enhanced drawer with swipe support
|
|
730
|
+
*
|
|
731
|
+
* Wraps the base Drawer component and adds touch/mouse gesture support for resizing and closing.
|
|
732
|
+
* Perfect for mobile bottom sheets and interactive panels.
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* ```tsx
|
|
736
|
+
* bottom sheet with gestures
|
|
737
|
+
* <SwipeableDrawer
|
|
738
|
+
* open={open}
|
|
739
|
+
* onClose={onClose}
|
|
740
|
+
* anchor="bottom"
|
|
741
|
+
* gestureControlsProps={{
|
|
742
|
+
* snapPoints: ({ minHeight, maxHeight }) => [
|
|
743
|
+
* minHeight, // Content height
|
|
744
|
+
* maxHeight * 0.6, // 60% of screen
|
|
745
|
+
* maxHeight // Full screen
|
|
746
|
+
* ],
|
|
747
|
+
* initialSnap: 0 // Start at content height
|
|
748
|
+
* }}
|
|
749
|
+
* >
|
|
750
|
+
* <Content />
|
|
751
|
+
* </SwipeableDrawer>
|
|
752
|
+
* ```
|
|
753
|
+
*/ const SwipeableDrawer_SwipeableDrawer = /*#__PURE__*/ forwardRef((inProps, ref)=>{
|
|
554
754
|
const props = useThemeProps({
|
|
555
755
|
props: inProps,
|
|
556
|
-
name: 'SwipeableDrawer'
|
|
756
|
+
name: 'SwipeableDrawer'
|
|
557
757
|
});
|
|
558
758
|
const { anchor = 'bottom', bodyProps, classes, className, children, header, footer, open, onClose, variant = 'temporary', blocking = true, gestureControlsProps, ...rest } = props;
|
|
759
|
+
// Refs for gesture system
|
|
559
760
|
const contentRef = useRef(null);
|
|
560
761
|
const headerRef = useRef(null);
|
|
561
762
|
const footerRef = useRef(null);
|
|
562
763
|
const isVertical = anchor === 'top' || anchor === 'bottom';
|
|
563
|
-
|
|
564
|
-
|
|
764
|
+
// Stabilize header/footer enabled state to prevent measurement fluctuations
|
|
765
|
+
// Header is enabled if there's a header prop OR if there's a drag handle (for vertical drawers)
|
|
766
|
+
const headerEnabled = useMemo(()=>!!header || isVertical, [
|
|
767
|
+
header,
|
|
768
|
+
isVertical
|
|
769
|
+
]);
|
|
770
|
+
const footerEnabled = useMemo(()=>!!footer, [
|
|
771
|
+
footer
|
|
772
|
+
]);
|
|
565
773
|
const cls = useClasses('swipeable-drawer', classes, className);
|
|
566
774
|
const { action, ...gestureProps } = gestureControlsProps || {};
|
|
567
775
|
const gestureControls = useDrawerGestures({
|
|
568
776
|
anchor,
|
|
569
777
|
open,
|
|
570
|
-
onClose: (event, reason)
|
|
778
|
+
onClose: (event, reason)=>{
|
|
571
779
|
onClose?.(event, reason);
|
|
572
780
|
},
|
|
573
781
|
contentRef,
|
|
@@ -575,33 +783,70 @@ const SwipeableDrawer = forwardRef((inProps, ref) => {
|
|
|
575
783
|
footerRef,
|
|
576
784
|
headerEnabled,
|
|
577
785
|
footerEnabled,
|
|
578
|
-
...gestureProps
|
|
786
|
+
...gestureProps
|
|
579
787
|
});
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
788
|
+
// Expose snapTo function through action ref
|
|
789
|
+
useImperativeHandle(action, ()=>({
|
|
790
|
+
snapTo: gestureControls.snapTo
|
|
791
|
+
}), [
|
|
792
|
+
gestureControls.snapTo
|
|
793
|
+
]);
|
|
583
794
|
const gestureBindProps = gestureControls.bind();
|
|
795
|
+
// Enhanced body props that include gesture motion
|
|
584
796
|
const enhancedBodyProps = {
|
|
585
797
|
...bodyProps,
|
|
586
|
-
|
|
798
|
+
// Apply gesture bindings for horizontal drawers (left/right)
|
|
799
|
+
// Vertical drawers (top/bottom) use motion values instead
|
|
800
|
+
...!isVertical ? gestureBindProps : {},
|
|
587
801
|
style: {
|
|
588
802
|
...bodyProps?.style,
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
803
|
+
// Apply motion styles for vertical drawers (top/bottom)
|
|
804
|
+
...isVertical && {
|
|
805
|
+
...gestureControls.motionProps.style
|
|
806
|
+
}
|
|
807
|
+
}
|
|
593
808
|
};
|
|
809
|
+
// Consolidate measurement refs (refs only)
|
|
594
810
|
const measurementRefs = {
|
|
595
811
|
header: headerRef,
|
|
596
812
|
footer: footerRef,
|
|
597
|
-
content: contentRef
|
|
813
|
+
content: contentRef
|
|
598
814
|
};
|
|
815
|
+
// Gesture props for header
|
|
599
816
|
const headerGestureProps = {
|
|
600
817
|
className: clsx(isVertical && cls('header-drag')),
|
|
601
|
-
...
|
|
818
|
+
...isVertical && gestureBindProps
|
|
602
819
|
};
|
|
603
|
-
return
|
|
820
|
+
return /*#__PURE__*/ jsx(Drawer_Drawer, {
|
|
821
|
+
ref: ref,
|
|
822
|
+
anchor: anchor,
|
|
823
|
+
open: open,
|
|
824
|
+
onClose: onClose,
|
|
825
|
+
variant: variant,
|
|
826
|
+
blocking: blocking,
|
|
827
|
+
bodyProps: enhancedBodyProps,
|
|
828
|
+
classes: classes,
|
|
829
|
+
className: className,
|
|
830
|
+
measurementRefs: measurementRefs,
|
|
831
|
+
headerProps: headerGestureProps,
|
|
832
|
+
header: isVertical ? /*#__PURE__*/ jsxs(Fragment, {
|
|
833
|
+
children: [
|
|
834
|
+
/*#__PURE__*/ jsx(DragHandle_DragHandle, {
|
|
835
|
+
anchor: anchor
|
|
836
|
+
}),
|
|
837
|
+
header
|
|
838
|
+
]
|
|
839
|
+
}) : header,
|
|
840
|
+
footer: footer,
|
|
841
|
+
...rest,
|
|
842
|
+
children: children
|
|
843
|
+
});
|
|
604
844
|
});
|
|
605
|
-
|
|
845
|
+
SwipeableDrawer_SwipeableDrawer.displayName = 'SuiSwipeableDrawer';
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
|
|
606
851
|
|
|
607
|
-
export { SwipeableDrawer,
|
|
852
|
+
export { SwipeableDrawer as swipeableDrawerStyles, SwipeableDrawer_SwipeableDrawer as SwipeableDrawer };
|