react-modal-sheet 5.4.0 → 5.5.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/dist/index.d.mts +0 -5
- package/dist/index.d.ts +0 -5
- package/dist/index.js +374 -275
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +375 -276
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/SheetContent.tsx +9 -3
- package/src/constants.ts +1 -1
- 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 +79 -143
- package/src/snap.ts +205 -102
- package/src/types.tsx +1 -2
- package/src/utils.ts +26 -0
- package/src/hooks/use-focused-input.ts +0 -46
package/package.json
CHANGED
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/constants.ts
CHANGED
|
@@ -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
|
+
}
|