react-modal-sheet 5.3.1 → 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/README.md +9 -6
- package/dist/index.d.mts +0 -5
- package/dist/index.d.ts +0 -5
- package/dist/index.js +253 -160
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +254 -161
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/SheetBackdrop.tsx +10 -4
- package/src/SheetContent.tsx +9 -3
- package/src/debug.ts +3 -2
- 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 +59 -73
- package/src/types.tsx +2 -2
- package/src/utils.ts +26 -0
package/src/SheetBackdrop.tsx
CHANGED
|
@@ -22,12 +22,18 @@ export const SheetBackdrop = forwardRef<any, SheetBackdropProps>(
|
|
|
22
22
|
pointerEvents,
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
const animationProps = sheetContext.prefersReducedMotion
|
|
26
|
+
? {}
|
|
27
|
+
: {
|
|
28
|
+
initial: { opacity: 0 },
|
|
29
|
+
animate: { opacity: 1 },
|
|
30
|
+
exit: { opacity: 0 },
|
|
31
|
+
transition: { duration: 1 },
|
|
32
|
+
};
|
|
33
|
+
|
|
25
34
|
return (
|
|
26
35
|
<Comp
|
|
27
|
-
|
|
28
|
-
animate={{ opacity: 1 }}
|
|
29
|
-
exit={{ opacity: 0 }}
|
|
30
|
-
transition={{ duration: 1 }}
|
|
36
|
+
{...animationProps}
|
|
31
37
|
{...(rest as any)}
|
|
32
38
|
ref={ref}
|
|
33
39
|
className={`react-modal-sheet-backdrop ${className}`}
|
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) {
|
package/src/debug.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* and hide it when that element is clicked. Create the element if it doesn't exist,
|
|
5
5
|
* and update its content with the payload (any JSON data).
|
|
6
6
|
*/
|
|
7
|
-
export function debug(payload: any, placement: 'top' | 'bottom' = '
|
|
7
|
+
export function debug(payload: any, placement: 'top' | 'bottom' = 'top') {
|
|
8
8
|
let debugElement = document.getElementById('debug-element');
|
|
9
9
|
|
|
10
10
|
if (!debugElement) {
|
|
@@ -32,6 +32,7 @@ export function debug(payload: any, placement: 'top' | 'bottom' = 'bottom') {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
const id = Math.random().toString(36).substring(2, 7);
|
|
36
|
+
debugElement.textContent = JSON.stringify(payload, null, 2) + ` (${id})`;
|
|
36
37
|
debugElement.style.display = 'block';
|
|
37
38
|
}
|
|
@@ -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,11 +25,11 @@ import {
|
|
|
26
25
|
} from './constants';
|
|
27
26
|
import { SheetContext } from './context';
|
|
28
27
|
import { useDimensions } from './hooks/use-dimensions';
|
|
28
|
+
import { useKeyboardAvoidance } from './hooks/use-keyboard-avoidance';
|
|
29
29
|
import { useModalEffect } from './hooks/use-modal-effect';
|
|
30
30
|
import { usePreventScroll } from './hooks/use-prevent-scroll';
|
|
31
31
|
import { useSheetState } from './hooks/use-sheet-state';
|
|
32
32
|
import { useStableCallback } from './hooks/use-stable-callback';
|
|
33
|
-
import { useVirtualKeyboard } from './hooks/use-virtual-keyboard';
|
|
34
33
|
import {
|
|
35
34
|
computeSnapPoints,
|
|
36
35
|
handleHighVelocityDrag,
|
|
@@ -38,7 +37,7 @@ import {
|
|
|
38
37
|
} from './snap';
|
|
39
38
|
import { styles } from './styles';
|
|
40
39
|
import { type SheetContextType, type SheetProps } from './types';
|
|
41
|
-
import { applyStyles, waitForElement } from './utils';
|
|
40
|
+
import { applyStyles, waitForElement, willOpenKeyboard } from './utils';
|
|
42
41
|
|
|
43
42
|
export const Sheet = forwardRef<any, SheetProps>(
|
|
44
43
|
(
|
|
@@ -97,14 +96,6 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
97
96
|
...(reduceMotion ? REDUCED_MOTION_TWEEN_CONFIG : tweenConfig),
|
|
98
97
|
};
|
|
99
98
|
|
|
100
|
-
const keyboard = useVirtualKeyboard({
|
|
101
|
-
isEnabled: isOpen && avoidKeyboard,
|
|
102
|
-
containerRef: sheetRef,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Disable drag if the keyboard is open to avoid weird behavior
|
|
106
|
-
const disableDrag = keyboard.isKeyboardOpen || disableDragProp;
|
|
107
|
-
|
|
108
99
|
// +2 for tolerance in case the animated value is slightly off
|
|
109
100
|
const zIndex = useTransform(y, (val) =>
|
|
110
101
|
val + 2 >= closedY ? -1 : (style?.zIndex ?? 9999)
|
|
@@ -157,22 +148,70 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
157
148
|
});
|
|
158
149
|
});
|
|
159
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
|
+
|
|
160
193
|
const blurActiveInput = useStableCallback(() => {
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
*/
|
|
163
198
|
const focusedElement = document.activeElement as HTMLElement | null;
|
|
164
|
-
if (!focusedElement || !sheetRef.current) return;
|
|
165
|
-
|
|
166
|
-
const isInput =
|
|
167
|
-
focusedElement.tagName === 'INPUT' ||
|
|
168
|
-
focusedElement.tagName === 'TEXTAREA';
|
|
169
199
|
|
|
170
200
|
// Only blur the focused element if it's inside the sheet
|
|
171
|
-
if (
|
|
201
|
+
if (
|
|
202
|
+
focusedElement &&
|
|
203
|
+
willOpenKeyboard(focusedElement) &&
|
|
204
|
+
sheetRef.current?.contains(focusedElement)
|
|
205
|
+
) {
|
|
172
206
|
focusedElement.blur();
|
|
173
207
|
}
|
|
174
208
|
});
|
|
175
209
|
|
|
210
|
+
const onDragStart = useStableCallback<DragHandler>((event, info) => {
|
|
211
|
+
blurActiveInput();
|
|
212
|
+
onDragStartProp?.(event, info);
|
|
213
|
+
});
|
|
214
|
+
|
|
176
215
|
const onDrag = useStableCallback<DragHandler>((event, info) => {
|
|
177
216
|
onDragProp?.(event, info);
|
|
178
217
|
|
|
@@ -187,11 +226,6 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
187
226
|
y.set(Math.max(currentY + info.delta.y, 0));
|
|
188
227
|
});
|
|
189
228
|
|
|
190
|
-
const onDragStart = useStableCallback<DragHandler>((event, info) => {
|
|
191
|
-
blurActiveInput();
|
|
192
|
-
onDragStartProp?.(event, info);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
229
|
const onDragEnd = useStableCallback<DragHandler>((event, info) => {
|
|
196
230
|
blurActiveInput();
|
|
197
231
|
onDragEndProp?.(event, info);
|
|
@@ -277,55 +311,6 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
277
311
|
snapTo,
|
|
278
312
|
}));
|
|
279
313
|
|
|
280
|
-
/**
|
|
281
|
-
* If the keyboard is opened while we are in a snap point we need to ensure
|
|
282
|
-
* the sheet content is not covered by the keyboard due to being at a lower
|
|
283
|
-
* snap point. We move the sheet up enough to ensure the content is visible
|
|
284
|
-
* and move it back down when the keyboard closes.
|
|
285
|
-
*/
|
|
286
|
-
useEffect(() => {
|
|
287
|
-
if (
|
|
288
|
-
isOpen &&
|
|
289
|
-
snapPoints.length > 0 &&
|
|
290
|
-
avoidKeyboard &&
|
|
291
|
-
keyboard.isKeyboardOpen
|
|
292
|
-
) {
|
|
293
|
-
const focusedElement = document.activeElement as HTMLElement | null;
|
|
294
|
-
const visibleSheetHeight = yInverted.get() - keyboard.keyboardHeight;
|
|
295
|
-
|
|
296
|
-
// If the keyboard is covering the sheet content, move the sheet up to reveal it
|
|
297
|
-
if (visibleSheetHeight <= 0 && focusedElement) {
|
|
298
|
-
const currentY = y.get();
|
|
299
|
-
const diffY = Math.max(currentY - Math.abs(visibleSheetHeight), 0);
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Get the position of the focused input relative to the sheet container
|
|
303
|
-
* and calculate how much we need to move the sheet up to ensure
|
|
304
|
-
* the focused input is visible above the keyboard.
|
|
305
|
-
*
|
|
306
|
-
* NOTE: RAF is needed to ensure the `top` values are correct.
|
|
307
|
-
*/
|
|
308
|
-
requestAnimationFrame(() => {
|
|
309
|
-
const inputRect = focusedElement.getBoundingClientRect();
|
|
310
|
-
const sheetRect = sheetRef.current?.getBoundingClientRect();
|
|
311
|
-
|
|
312
|
-
let inputOffset = 0;
|
|
313
|
-
|
|
314
|
-
if (sheetRect) {
|
|
315
|
-
inputOffset = inputRect.top - sheetRect.top + inputRect.height;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
animate(y, diffY - inputOffset, animationOptions);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// When keyboard closes animate back to the original snap point
|
|
322
|
-
return () => {
|
|
323
|
-
animate(y, currentY, animationOptions);
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}, [isOpen, keyboard.isKeyboardOpen]);
|
|
328
|
-
|
|
329
314
|
useModalEffect({
|
|
330
315
|
y,
|
|
331
316
|
detent,
|
|
@@ -394,6 +379,7 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
394
379
|
dragProps,
|
|
395
380
|
indicatorRotation,
|
|
396
381
|
avoidKeyboard,
|
|
382
|
+
prefersReducedMotion,
|
|
397
383
|
sheetBoundsRef,
|
|
398
384
|
sheetRef,
|
|
399
385
|
unstyled,
|