react-modal-sheet 5.4.0 → 5.4.1
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/dist/index.d.mts +0 -5
- package/dist/index.d.ts +0 -5
- package/dist/index.js +234 -187
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +235 -188
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/SheetContent.tsx +9 -3
- package/src/hooks/use-keyboard-avoidance.ts +75 -0
- package/src/hooks/use-prevent-scroll.ts +5 -27
- package/src/hooks/use-scroll-position.ts +0 -1
- package/src/hooks/use-virtual-keyboard.ts +127 -52
- package/src/sheet.tsx +58 -86
- package/src/types.tsx +1 -2
- package/src/utils.ts +26 -0
- package/src/hooks/use-focused-input.ts +0 -46
package/src/SheetContent.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { useDragConstraints } from './hooks/use-drag-constraints';
|
|
|
6
6
|
import { useScrollPosition } from './hooks/use-scroll-position';
|
|
7
7
|
import { styles } from './styles';
|
|
8
8
|
import { type SheetContentProps } from './types';
|
|
9
|
-
import { applyStyles, mergeRefs } from './utils';
|
|
9
|
+
import { applyStyles, isHTTPS, mergeRefs } from './utils';
|
|
10
10
|
|
|
11
11
|
export const SheetContent = forwardRef<any, SheetContentProps>(
|
|
12
12
|
(
|
|
@@ -73,8 +73,14 @@ export const SheetContent = forwardRef<any, SheetContentProps>(
|
|
|
73
73
|
const scrollStyle: MotionStyle = applyStyles(styles.scroller, isUnstyled);
|
|
74
74
|
|
|
75
75
|
if (sheetContext.avoidKeyboard) {
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Virtual Keyboard API is only available in secure context (https)
|
|
78
|
+
* and it will have 0px height in insecure context,
|
|
79
|
+
* which causes issues when developing and testing on localhost (http).
|
|
80
|
+
*/
|
|
81
|
+
scrollStyle.paddingBottom = isHTTPS()
|
|
82
|
+
? 'env(keyboard-inset-height, var(--keyboard-inset-height, 0px))'
|
|
83
|
+
: 'var(--keyboard-inset-height, 0px)';
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
if (disableScroll) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type RefObject, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { willOpenKeyboard } from '../utils';
|
|
4
|
+
import { useStableCallback } from './use-stable-callback';
|
|
5
|
+
import { useVirtualKeyboard } from './use-virtual-keyboard';
|
|
6
|
+
|
|
7
|
+
export function useKeyboardAvoidance({
|
|
8
|
+
isEnabled,
|
|
9
|
+
containerRef,
|
|
10
|
+
onWillOpenKeyboard,
|
|
11
|
+
onDidOpenKeyboard,
|
|
12
|
+
}: {
|
|
13
|
+
isEnabled: boolean;
|
|
14
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
15
|
+
onWillOpenKeyboard?: (event: FocusEvent) => Promise<void>;
|
|
16
|
+
onDidOpenKeyboard?: (focusedElement: HTMLElement) => void;
|
|
17
|
+
}) {
|
|
18
|
+
const [focusedElement, setFocusedElement] = useState<HTMLElement | null>(
|
|
19
|
+
null
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const keyboard = useVirtualKeyboard({
|
|
23
|
+
isEnabled,
|
|
24
|
+
containerRef,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const handleFocusIn = useStableCallback(async (event: FocusEvent) => {
|
|
28
|
+
const element = event.target as HTMLElement;
|
|
29
|
+
|
|
30
|
+
if (willOpenKeyboard(element) && containerRef.current?.contains(element)) {
|
|
31
|
+
await onWillOpenKeyboard?.(event);
|
|
32
|
+
setFocusedElement(element);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const handleFocusOut = useStableCallback((event: FocusEvent) => {
|
|
37
|
+
const element = event.target as HTMLElement;
|
|
38
|
+
|
|
39
|
+
if (focusedElement === element) {
|
|
40
|
+
setFocusedElement(null);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Keep track of the currently focused input within the container
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!isEnabled) return;
|
|
47
|
+
|
|
48
|
+
document.addEventListener('focusin', handleFocusIn);
|
|
49
|
+
document.addEventListener('focusout', handleFocusOut);
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
document.removeEventListener('focusin', handleFocusIn);
|
|
53
|
+
document.removeEventListener('focusout', handleFocusOut);
|
|
54
|
+
};
|
|
55
|
+
}, [isEnabled]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const containerElement = containerRef.current;
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
!isEnabled ||
|
|
62
|
+
!focusedElement ||
|
|
63
|
+
!containerElement ||
|
|
64
|
+
!keyboard.isKeyboardOpen
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
requestAnimationFrame(() => {
|
|
70
|
+
onDidOpenKeyboard?.(focusedElement);
|
|
71
|
+
});
|
|
72
|
+
}, [isEnabled, keyboard.isKeyboardOpen, focusedElement]);
|
|
73
|
+
|
|
74
|
+
return keyboard;
|
|
75
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// This code originates from https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/usePreventScroll.ts
|
|
2
2
|
|
|
3
|
+
import { isIOS, willOpenKeyboard } from '../utils';
|
|
3
4
|
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
|
|
4
|
-
import { isIOS } from '../utils';
|
|
5
5
|
|
|
6
6
|
const KEYBOARD_BUFFER = 24;
|
|
7
7
|
|
|
@@ -32,17 +32,17 @@ export function isScrollable(
|
|
|
32
32
|
|
|
33
33
|
const style = window.getComputedStyle(node);
|
|
34
34
|
|
|
35
|
-
let
|
|
35
|
+
let scrollable = /(auto|scroll)/.test(
|
|
36
36
|
style.overflow + style.overflowX + style.overflowY
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
if (
|
|
40
|
-
|
|
39
|
+
if (scrollable && checkForOverflow) {
|
|
40
|
+
scrollable =
|
|
41
41
|
node.scrollHeight !== node.clientHeight ||
|
|
42
42
|
node.scrollWidth !== node.clientWidth;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
return
|
|
45
|
+
return scrollable;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export function getScrollParent(
|
|
@@ -64,19 +64,6 @@ export function getScrollParent(
|
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// HTML input types that do not cause the software keyboard to appear.
|
|
68
|
-
const nonTextInputTypes = new Set([
|
|
69
|
-
'checkbox',
|
|
70
|
-
'radio',
|
|
71
|
-
'range',
|
|
72
|
-
'color',
|
|
73
|
-
'file',
|
|
74
|
-
'image',
|
|
75
|
-
'button',
|
|
76
|
-
'submit',
|
|
77
|
-
'reset',
|
|
78
|
-
]);
|
|
79
|
-
|
|
80
67
|
// The number of active usePreventScroll calls. Used to determine whether to revert back to the original page style/scroll position
|
|
81
68
|
let preventScrollCount = 0;
|
|
82
69
|
let restore: () => void;
|
|
@@ -367,12 +354,3 @@ function scrollIntoView(target: Element) {
|
|
|
367
354
|
target = scrollable.parentElement;
|
|
368
355
|
}
|
|
369
356
|
}
|
|
370
|
-
|
|
371
|
-
function willOpenKeyboard(target: Element) {
|
|
372
|
-
return (
|
|
373
|
-
(target instanceof HTMLInputElement &&
|
|
374
|
-
!nonTextInputTypes.has(target.type)) ||
|
|
375
|
-
target instanceof HTMLTextAreaElement ||
|
|
376
|
-
(target instanceof HTMLElement && target.isContentEditable)
|
|
377
|
-
);
|
|
378
|
-
}
|
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
import { type RefObject, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { isHTTPS, willOpenKeyboard } from '../utils';
|
|
3
|
+
|
|
4
|
+
type VirtualKeyboardApi = {
|
|
5
|
+
overlaysContent: boolean;
|
|
6
|
+
boundingRect: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
height: number;
|
|
10
|
+
width: number;
|
|
11
|
+
};
|
|
12
|
+
addEventListener: (
|
|
13
|
+
type: 'geometrychange',
|
|
14
|
+
listener: EventListenerOrEventListenerObject
|
|
15
|
+
) => void;
|
|
16
|
+
removeEventListener: (
|
|
17
|
+
type: 'geometrychange',
|
|
18
|
+
listener: EventListenerOrEventListenerObject
|
|
19
|
+
) => void;
|
|
20
|
+
};
|
|
3
21
|
|
|
4
22
|
type VirtualKeyboardState = {
|
|
5
23
|
isVisible: boolean;
|
|
6
24
|
height: number;
|
|
7
25
|
};
|
|
8
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Keep track of how many components are using the virtual keyboard API
|
|
29
|
+
* to avoid conflicts when toggling `overlaysContent` which is a global setting.
|
|
30
|
+
*/
|
|
31
|
+
let virtualKeyboardOverlayUsers = 0;
|
|
32
|
+
let initialVirtualKeyboardOverlaysContent: boolean | null = null;
|
|
33
|
+
|
|
9
34
|
type UseVirtualKeyboardOptions = {
|
|
10
35
|
/**
|
|
11
36
|
* Ref to the container element to apply `keyboard-inset-height` CSS variable updates.
|
|
@@ -22,11 +47,6 @@ type UseVirtualKeyboardOptions = {
|
|
|
22
47
|
* @default 100
|
|
23
48
|
*/
|
|
24
49
|
visualViewportThreshold?: number;
|
|
25
|
-
/**
|
|
26
|
-
* Whether to treat contenteditable elements as text inputs.
|
|
27
|
-
* @default true
|
|
28
|
-
*/
|
|
29
|
-
includeContentEditable?: boolean;
|
|
30
50
|
/**
|
|
31
51
|
* Delay in ms for debouncing viewport changes.
|
|
32
52
|
* @default 100
|
|
@@ -66,7 +86,6 @@ export function useVirtualKeyboard(options: UseVirtualKeyboardOptions = {}) {
|
|
|
66
86
|
containerRef,
|
|
67
87
|
isEnabled = true,
|
|
68
88
|
debounceDelay = 100,
|
|
69
|
-
includeContentEditable = true,
|
|
70
89
|
visualViewportThreshold = 100,
|
|
71
90
|
} = options;
|
|
72
91
|
|
|
@@ -78,27 +97,14 @@ export function useVirtualKeyboard(options: UseVirtualKeyboardOptions = {}) {
|
|
|
78
97
|
const focusedElementRef = useRef<HTMLElement | null>(null);
|
|
79
98
|
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
80
99
|
|
|
81
|
-
const isTextInput = useStableCallback((el: Element | null) => {
|
|
82
|
-
return (
|
|
83
|
-
el?.tagName === 'INPUT' ||
|
|
84
|
-
el?.tagName === 'TEXTAREA' ||
|
|
85
|
-
(includeContentEditable &&
|
|
86
|
-
el instanceof HTMLElement &&
|
|
87
|
-
el.isContentEditable)
|
|
88
|
-
);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
100
|
useEffect(() => {
|
|
92
|
-
if (!isEnabled) return;
|
|
93
|
-
|
|
94
101
|
const vv = window.visualViewport;
|
|
95
|
-
const vk = (
|
|
102
|
+
const vk = getVirtualKeyboardApi();
|
|
96
103
|
|
|
97
104
|
function setKeyboardInsetHeightEnv(height: number) {
|
|
98
105
|
const element = containerRef?.current || document.documentElement;
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
if (window.isSecureContext) {
|
|
107
|
+
if (vk) {
|
|
102
108
|
element.style.setProperty(
|
|
103
109
|
'--keyboard-inset-height',
|
|
104
110
|
`env(keyboard-inset-height, ${height}px)`
|
|
@@ -108,29 +114,46 @@ export function useVirtualKeyboard(options: UseVirtualKeyboardOptions = {}) {
|
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
function setKeyboardState(nextState: VirtualKeyboardState) {
|
|
118
|
+
setState((prevState) =>
|
|
119
|
+
prevState.isVisible === nextState.isVisible &&
|
|
120
|
+
prevState.height === nextState.height
|
|
121
|
+
? prevState
|
|
122
|
+
: nextState
|
|
123
|
+
);
|
|
116
124
|
}
|
|
117
125
|
|
|
118
|
-
function
|
|
126
|
+
function resetKeyboardState() {
|
|
119
127
|
focusedElementRef.current = null;
|
|
120
|
-
|
|
128
|
+
setKeyboardInsetHeightEnv(0);
|
|
129
|
+
setKeyboardState({ isVisible: false, height: 0 });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!isEnabled) {
|
|
133
|
+
resetKeyboardState();
|
|
134
|
+
return;
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
function updateKeyboardState() {
|
|
124
138
|
if (debounceTimer.current) {
|
|
125
139
|
clearTimeout(debounceTimer.current);
|
|
126
140
|
}
|
|
141
|
+
|
|
127
142
|
debounceTimer.current = setTimeout(() => {
|
|
128
|
-
const active = focusedElementRef.current;
|
|
129
|
-
const inputIsFocused =
|
|
143
|
+
const active = getActiveElement() ?? focusedElementRef.current;
|
|
144
|
+
const inputIsFocused = active ? willOpenKeyboard(active) : false;
|
|
130
145
|
|
|
131
146
|
if (!inputIsFocused) {
|
|
132
|
-
|
|
133
|
-
|
|
147
|
+
resetKeyboardState();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
focusedElementRef.current = active as HTMLElement;
|
|
152
|
+
|
|
153
|
+
if (vk?.overlaysContent) {
|
|
154
|
+
const keyboardHeight = vk.boundingRect.height;
|
|
155
|
+
setKeyboardInsetHeightEnv(keyboardHeight);
|
|
156
|
+
setKeyboardState({ isVisible: true, height: keyboardHeight });
|
|
134
157
|
return;
|
|
135
158
|
}
|
|
136
159
|
|
|
@@ -139,33 +162,53 @@ export function useVirtualKeyboard(options: UseVirtualKeyboardOptions = {}) {
|
|
|
139
162
|
|
|
140
163
|
if (heightDiff > visualViewportThreshold) {
|
|
141
164
|
setKeyboardInsetHeightEnv(heightDiff);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
setKeyboardInsetHeightEnv(0);
|
|
145
|
-
setState({ isVisible: false, height: 0 });
|
|
165
|
+
setKeyboardState({ isVisible: true, height: heightDiff });
|
|
166
|
+
return;
|
|
146
167
|
}
|
|
147
168
|
}
|
|
169
|
+
|
|
170
|
+
resetKeyboardState();
|
|
148
171
|
}, debounceDelay);
|
|
149
172
|
}
|
|
150
173
|
|
|
151
|
-
|
|
152
|
-
|
|
174
|
+
function handleFocusIn(e: FocusEvent) {
|
|
175
|
+
if (e.target instanceof HTMLElement && willOpenKeyboard(e.target)) {
|
|
176
|
+
focusedElementRef.current = e.target;
|
|
177
|
+
updateKeyboardState();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function handleFocusOut() {
|
|
182
|
+
requestAnimationFrame(() => {
|
|
183
|
+
focusedElementRef.current = getActiveElement();
|
|
184
|
+
updateKeyboardState();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
document.addEventListener('focusin', handleFocusIn);
|
|
189
|
+
document.addEventListener('focusout', handleFocusOut);
|
|
153
190
|
|
|
154
191
|
if (vv) {
|
|
155
192
|
vv.addEventListener('resize', updateKeyboardState);
|
|
156
193
|
vv.addEventListener('scroll', updateKeyboardState);
|
|
157
194
|
}
|
|
158
195
|
|
|
159
|
-
let currentOverlaysContent = false;
|
|
160
|
-
|
|
161
196
|
if (vk) {
|
|
162
|
-
|
|
163
|
-
|
|
197
|
+
if (virtualKeyboardOverlayUsers === 0) {
|
|
198
|
+
initialVirtualKeyboardOverlaysContent = vk.overlaysContent;
|
|
199
|
+
vk.overlaysContent = true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
virtualKeyboardOverlayUsers++;
|
|
203
|
+
vk.addEventListener('geometrychange', updateKeyboardState);
|
|
164
204
|
}
|
|
165
205
|
|
|
206
|
+
focusedElementRef.current = getActiveElement();
|
|
207
|
+
updateKeyboardState();
|
|
208
|
+
|
|
166
209
|
return () => {
|
|
167
|
-
|
|
168
|
-
|
|
210
|
+
document.removeEventListener('focusin', handleFocusIn);
|
|
211
|
+
document.removeEventListener('focusout', handleFocusOut);
|
|
169
212
|
|
|
170
213
|
if (vv) {
|
|
171
214
|
vv.removeEventListener('resize', updateKeyboardState);
|
|
@@ -173,22 +216,54 @@ export function useVirtualKeyboard(options: UseVirtualKeyboardOptions = {}) {
|
|
|
173
216
|
}
|
|
174
217
|
|
|
175
218
|
if (vk) {
|
|
176
|
-
vk.
|
|
219
|
+
vk.removeEventListener('geometrychange', updateKeyboardState);
|
|
220
|
+
|
|
221
|
+
virtualKeyboardOverlayUsers = Math.max(
|
|
222
|
+
0,
|
|
223
|
+
virtualKeyboardOverlayUsers - 1
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
if (virtualKeyboardOverlayUsers === 0) {
|
|
227
|
+
vk.overlaysContent = initialVirtualKeyboardOverlaysContent ?? false;
|
|
228
|
+
initialVirtualKeyboardOverlaysContent = null;
|
|
229
|
+
}
|
|
177
230
|
}
|
|
178
231
|
|
|
179
232
|
if (debounceTimer.current) {
|
|
180
233
|
clearTimeout(debounceTimer.current);
|
|
181
234
|
}
|
|
235
|
+
|
|
236
|
+
resetKeyboardState();
|
|
182
237
|
};
|
|
183
|
-
}, [
|
|
184
|
-
debounceDelay,
|
|
185
|
-
includeContentEditable,
|
|
186
|
-
isEnabled,
|
|
187
|
-
visualViewportThreshold,
|
|
188
|
-
]);
|
|
238
|
+
}, [debounceDelay, isEnabled, visualViewportThreshold]);
|
|
189
239
|
|
|
190
240
|
return {
|
|
191
241
|
keyboardHeight: state.height,
|
|
192
242
|
isKeyboardOpen: state.isVisible,
|
|
193
243
|
};
|
|
194
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Virtual Keyboard API is only available in secure contexts (HTTPS)
|
|
248
|
+
* and may not be supported in all browsers.
|
|
249
|
+
*/
|
|
250
|
+
function getVirtualKeyboardApi() {
|
|
251
|
+
return isHTTPS() && 'virtualKeyboard' in navigator
|
|
252
|
+
? (navigator.virtualKeyboard as VirtualKeyboardApi)
|
|
253
|
+
: null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getActiveElement() {
|
|
257
|
+
let activeElement: Element | null = document.activeElement;
|
|
258
|
+
|
|
259
|
+
while (
|
|
260
|
+
activeElement instanceof HTMLElement &&
|
|
261
|
+
activeElement.shadowRoot?.activeElement
|
|
262
|
+
) {
|
|
263
|
+
activeElement = activeElement.shadowRoot.activeElement;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return activeElement && willOpenKeyboard(activeElement)
|
|
267
|
+
? (activeElement as HTMLElement)
|
|
268
|
+
: null;
|
|
269
|
+
}
|
package/src/sheet.tsx
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
} from 'motion/react';
|
|
10
10
|
import React, {
|
|
11
11
|
forwardRef,
|
|
12
|
-
useEffect,
|
|
13
12
|
useImperativeHandle,
|
|
14
13
|
useRef,
|
|
15
14
|
useState,
|
|
@@ -26,12 +25,11 @@ import {
|
|
|
26
25
|
} from './constants';
|
|
27
26
|
import { SheetContext } from './context';
|
|
28
27
|
import { useDimensions } from './hooks/use-dimensions';
|
|
29
|
-
import {
|
|
28
|
+
import { useKeyboardAvoidance } from './hooks/use-keyboard-avoidance';
|
|
30
29
|
import { useModalEffect } from './hooks/use-modal-effect';
|
|
31
30
|
import { usePreventScroll } from './hooks/use-prevent-scroll';
|
|
32
31
|
import { useSheetState } from './hooks/use-sheet-state';
|
|
33
32
|
import { useStableCallback } from './hooks/use-stable-callback';
|
|
34
|
-
import { useVirtualKeyboard } from './hooks/use-virtual-keyboard';
|
|
35
33
|
import {
|
|
36
34
|
computeSnapPoints,
|
|
37
35
|
handleHighVelocityDrag,
|
|
@@ -39,7 +37,7 @@ import {
|
|
|
39
37
|
} from './snap';
|
|
40
38
|
import { styles } from './styles';
|
|
41
39
|
import { type SheetContextType, type SheetProps } from './types';
|
|
42
|
-
import { applyStyles, waitForElement } from './utils';
|
|
40
|
+
import { applyStyles, waitForElement, willOpenKeyboard } from './utils';
|
|
43
41
|
|
|
44
42
|
export const Sheet = forwardRef<any, SheetProps>(
|
|
45
43
|
(
|
|
@@ -98,14 +96,6 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
98
96
|
...(reduceMotion ? REDUCED_MOTION_TWEEN_CONFIG : tweenConfig),
|
|
99
97
|
};
|
|
100
98
|
|
|
101
|
-
const keyboard = useVirtualKeyboard({
|
|
102
|
-
isEnabled: isOpen && avoidKeyboard,
|
|
103
|
-
containerRef: sheetRef,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Disable drag if the keyboard is open to avoid weird behavior
|
|
107
|
-
const disableDrag = keyboard.isKeyboardOpen || disableDragProp;
|
|
108
|
-
|
|
109
99
|
// +2 for tolerance in case the animated value is slightly off
|
|
110
100
|
const zIndex = useTransform(y, (val) =>
|
|
111
101
|
val + 2 >= closedY ? -1 : (style?.zIndex ?? 9999)
|
|
@@ -158,22 +148,70 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
158
148
|
});
|
|
159
149
|
});
|
|
160
150
|
|
|
151
|
+
const keyboard = useKeyboardAvoidance({
|
|
152
|
+
isEnabled: isOpen && avoidKeyboard,
|
|
153
|
+
containerRef: sheetRef,
|
|
154
|
+
onWillOpenKeyboard: async () => {
|
|
155
|
+
const lastSnapPoint = snapPoints[snapPoints.length - 1];
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* If there are snap points and the sheet is not already at the last snap point,
|
|
159
|
+
* move it there to make sure the focused input is not covered by the keyboard
|
|
160
|
+
*/
|
|
161
|
+
if (lastSnapPoint && lastSnapPoint.snapIndex !== currentSnap) {
|
|
162
|
+
await animate(y, lastSnapPoint.snapValueY, animationOptions);
|
|
163
|
+
updateSnap(lastSnapPoint.snapIndex);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
onDidOpenKeyboard: (focusedElement) => {
|
|
167
|
+
const sheetElement = sheetRef.current;
|
|
168
|
+
if (!sheetElement) return;
|
|
169
|
+
|
|
170
|
+
const inputRect = focusedElement.getBoundingClientRect();
|
|
171
|
+
const containerRect = sheetElement.getBoundingClientRect();
|
|
172
|
+
const scroller = sheetElement.querySelector(
|
|
173
|
+
'.react-modal-sheet-content-scroller'
|
|
174
|
+
) as HTMLElement;
|
|
175
|
+
|
|
176
|
+
const scrollTarget = Math.max(
|
|
177
|
+
inputRect.top -
|
|
178
|
+
containerRect.top +
|
|
179
|
+
scroller.scrollTop -
|
|
180
|
+
inputRect.height,
|
|
181
|
+
0
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
requestAnimationFrame(() => {
|
|
185
|
+
scroller.scrollTo({ top: scrollTarget, behavior: 'smooth' });
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Disable drag if the keyboard is open to avoid weird behavior
|
|
191
|
+
const disableDrag = keyboard.isKeyboardOpen || disableDragProp;
|
|
192
|
+
|
|
161
193
|
const blurActiveInput = useStableCallback(() => {
|
|
162
|
-
|
|
163
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Find focused input inside the sheet and blur it when dragging starts
|
|
196
|
+
* to prevent a weird ghost caret "bug" on mobile
|
|
197
|
+
*/
|
|
164
198
|
const focusedElement = document.activeElement as HTMLElement | null;
|
|
165
|
-
if (!focusedElement || !sheetRef.current) return;
|
|
166
|
-
|
|
167
|
-
const isInput =
|
|
168
|
-
focusedElement.tagName === 'INPUT' ||
|
|
169
|
-
focusedElement.tagName === 'TEXTAREA';
|
|
170
199
|
|
|
171
200
|
// Only blur the focused element if it's inside the sheet
|
|
172
|
-
if (
|
|
201
|
+
if (
|
|
202
|
+
focusedElement &&
|
|
203
|
+
willOpenKeyboard(focusedElement) &&
|
|
204
|
+
sheetRef.current?.contains(focusedElement)
|
|
205
|
+
) {
|
|
173
206
|
focusedElement.blur();
|
|
174
207
|
}
|
|
175
208
|
});
|
|
176
209
|
|
|
210
|
+
const onDragStart = useStableCallback<DragHandler>((event, info) => {
|
|
211
|
+
blurActiveInput();
|
|
212
|
+
onDragStartProp?.(event, info);
|
|
213
|
+
});
|
|
214
|
+
|
|
177
215
|
const onDrag = useStableCallback<DragHandler>((event, info) => {
|
|
178
216
|
onDragProp?.(event, info);
|
|
179
217
|
|
|
@@ -188,11 +226,6 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
188
226
|
y.set(Math.max(currentY + info.delta.y, 0));
|
|
189
227
|
});
|
|
190
228
|
|
|
191
|
-
const onDragStart = useStableCallback<DragHandler>((event, info) => {
|
|
192
|
-
blurActiveInput();
|
|
193
|
-
onDragStartProp?.(event, info);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
229
|
const onDragEnd = useStableCallback<DragHandler>((event, info) => {
|
|
197
230
|
blurActiveInput();
|
|
198
231
|
onDragEndProp?.(event, info);
|
|
@@ -295,67 +328,6 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
295
328
|
isDisabled: disableScrollLocking || !isOpen,
|
|
296
329
|
});
|
|
297
330
|
|
|
298
|
-
const focusedInput = useFocusedInput({
|
|
299
|
-
isEnabled: isOpen && avoidKeyboard,
|
|
300
|
-
sheetRef,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Keyboard avoidance.
|
|
305
|
-
*
|
|
306
|
-
* Move the sheet up when the keyboard opens if the sheet has snap points,
|
|
307
|
-
* and then scroll the focused input into view to make sure it's not covered
|
|
308
|
-
* by the keyboard.
|
|
309
|
-
*/
|
|
310
|
-
useEffect(() => {
|
|
311
|
-
if (!isOpen || !avoidKeyboard) {
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function adjustSheetForKeyboard() {
|
|
316
|
-
const sheetElement = sheetRef.current;
|
|
317
|
-
|
|
318
|
-
if (!sheetElement || !focusedInput || !keyboard.isKeyboardOpen) {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* If we have snap points and the sheet is currently snapped to a point
|
|
324
|
-
* that is being covered by the keyboard, we want to move the sheet up
|
|
325
|
-
* to reveal it.
|
|
326
|
-
*/
|
|
327
|
-
const lastSnapPoint = snapPoints[snapPoints.length - 1];
|
|
328
|
-
|
|
329
|
-
if (lastSnapPoint) {
|
|
330
|
-
await animate(y, lastSnapPoint.snapValueY, animationOptions);
|
|
331
|
-
updateSnap(lastSnapPoint.snapIndex);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Then scroll the focused input into view if it's covered by the keyboard
|
|
335
|
-
requestAnimationFrame(() => {
|
|
336
|
-
const inputRect = focusedInput.getBoundingClientRect();
|
|
337
|
-
const sheetRect = sheetElement.getBoundingClientRect();
|
|
338
|
-
const scroller = sheetElement.querySelector(
|
|
339
|
-
'.react-modal-sheet-content-scroller'
|
|
340
|
-
) as HTMLElement;
|
|
341
|
-
|
|
342
|
-
const scrollTarget = Math.max(
|
|
343
|
-
inputRect.top -
|
|
344
|
-
sheetRect.top +
|
|
345
|
-
scroller.scrollTop -
|
|
346
|
-
inputRect.height,
|
|
347
|
-
0
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
requestAnimationFrame(() => {
|
|
351
|
-
scroller.scrollTo({ top: scrollTarget, behavior: 'smooth' });
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
adjustSheetForKeyboard();
|
|
357
|
-
}, [isOpen, avoidKeyboard, keyboard.isKeyboardOpen, focusedInput]);
|
|
358
|
-
|
|
359
331
|
const state = useSheetState({
|
|
360
332
|
isOpen,
|
|
361
333
|
onOpen: async () => {
|
package/src/types.tsx
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
} from 'motion/react';
|
|
8
8
|
import {
|
|
9
9
|
type ComponentPropsWithoutRef,
|
|
10
|
-
type CSSProperties,
|
|
11
10
|
type ForwardRefExoticComponent,
|
|
12
11
|
type FunctionComponent,
|
|
13
12
|
type HTMLAttributes,
|
|
@@ -108,7 +107,7 @@ export interface SheetContextType {
|
|
|
108
107
|
currentSnap?: number;
|
|
109
108
|
detent: SheetDetent;
|
|
110
109
|
disableDrag: boolean;
|
|
111
|
-
dragProps
|
|
110
|
+
dragProps: SheetDragProps;
|
|
112
111
|
indicatorRotation: MotionValue<number>;
|
|
113
112
|
avoidKeyboard: boolean;
|
|
114
113
|
prefersReducedMotion: boolean;
|
package/src/utils.ts
CHANGED
|
@@ -88,3 +88,29 @@ export function waitForElement(
|
|
|
88
88
|
}, interval);
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
// HTML input types that do not cause the software keyboard to appear.
|
|
93
|
+
const nonTextInputTypes = new Set([
|
|
94
|
+
'checkbox',
|
|
95
|
+
'radio',
|
|
96
|
+
'range',
|
|
97
|
+
'color',
|
|
98
|
+
'file',
|
|
99
|
+
'image',
|
|
100
|
+
'button',
|
|
101
|
+
'submit',
|
|
102
|
+
'reset',
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
export function willOpenKeyboard(target: Element) {
|
|
106
|
+
return (
|
|
107
|
+
(target instanceof HTMLInputElement &&
|
|
108
|
+
!nonTextInputTypes.has(target.type)) ||
|
|
109
|
+
target instanceof HTMLTextAreaElement ||
|
|
110
|
+
(target instanceof HTMLElement && target.isContentEditable)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function isHTTPS() {
|
|
115
|
+
return typeof window !== 'undefined' && window.isSecureContext;
|
|
116
|
+
}
|