@simoneggert/react-modal-sheet 5.4.3
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/LICENSE +21 -0
- package/README.md +994 -0
- package/dist/index.d.mts +188 -0
- package/dist/index.d.ts +188 -0
- package/dist/index.js +1511 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1502 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +87 -0
- package/src/SheetBackdrop.tsx +46 -0
- package/src/SheetContainer.tsx +53 -0
- package/src/SheetContent.tsx +117 -0
- package/src/SheetDragIndicator.tsx +52 -0
- package/src/SheetHeader.tsx +52 -0
- package/src/constants.ts +19 -0
- package/src/context.tsx +12 -0
- package/src/debug.ts +38 -0
- package/src/hooks/use-dimensions.ts +30 -0
- package/src/hooks/use-drag-constraints.ts +14 -0
- package/src/hooks/use-isomorphic-layout-effect.ts +5 -0
- package/src/hooks/use-keyboard-avoidance.ts +75 -0
- package/src/hooks/use-modal-effect.ts +161 -0
- package/src/hooks/use-prevent-scroll.ts +357 -0
- package/src/hooks/use-resize-observer.ts +31 -0
- package/src/hooks/use-safe-area-insets.ts +40 -0
- package/src/hooks/use-scroll-position.ts +142 -0
- package/src/hooks/use-sheet-state.ts +63 -0
- package/src/hooks/use-stable-callback.ts +18 -0
- package/src/hooks/use-virtual-keyboard.ts +269 -0
- package/src/index.tsx +41 -0
- package/src/sheet.tsx +414 -0
- package/src/snap.ts +242 -0
- package/src/styles.ts +110 -0
- package/src/types.tsx +154 -0
- package/src/utils.ts +116 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { type RefObject, useEffect, useRef, useState } from 'react';
|
|
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
|
+
};
|
|
21
|
+
|
|
22
|
+
type VirtualKeyboardState = {
|
|
23
|
+
isVisible: boolean;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
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
|
+
|
|
34
|
+
type UseVirtualKeyboardOptions = {
|
|
35
|
+
/**
|
|
36
|
+
* Ref to the container element to apply `keyboard-inset-height` CSS variable updates.
|
|
37
|
+
* @default document.documentElement
|
|
38
|
+
*/
|
|
39
|
+
containerRef?: RefObject<HTMLElement | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Enable or disable the hook entirely.
|
|
42
|
+
* @default true
|
|
43
|
+
*/
|
|
44
|
+
isEnabled?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Minimum pixel height difference to consider the keyboard visible.
|
|
47
|
+
* @default 100
|
|
48
|
+
*/
|
|
49
|
+
visualViewportThreshold?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Delay in ms for debouncing viewport changes.
|
|
52
|
+
* @default 100
|
|
53
|
+
*/
|
|
54
|
+
debounceDelay?: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A hook that detects virtual keyboard visibility and height.
|
|
59
|
+
* It listens to focus events and visual viewport changes to determine
|
|
60
|
+
* if a text input is focused and the keyboard is likely visible.
|
|
61
|
+
*
|
|
62
|
+
* It also sets the `--keyboard-inset-height` CSS variable on the specified container
|
|
63
|
+
* (or `:root` by default) to allow for easy styling adjustments when the keyboard is open.
|
|
64
|
+
*
|
|
65
|
+
* @param options Configuration options for the hook.
|
|
66
|
+
* @returns An object containing `isKeyboardOpen` and `keyboardHeight`.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* import { useVirtualKeyboard } from 'react-modal-sheet';
|
|
71
|
+
*
|
|
72
|
+
* function MyComponent() {
|
|
73
|
+
* const { isKeyboardOpen, keyboardHeight } = useVirtualKeyboard();
|
|
74
|
+
*
|
|
75
|
+
* return (
|
|
76
|
+
* <div>
|
|
77
|
+
* <p>Keyboard is {isKeyboardOpen ? 'open' : 'closed'}</p>
|
|
78
|
+
* <p>Keyboard height: {keyboardHeight}px</p>
|
|
79
|
+
* </div>
|
|
80
|
+
* );
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function useVirtualKeyboard(options: UseVirtualKeyboardOptions = {}) {
|
|
85
|
+
const {
|
|
86
|
+
containerRef,
|
|
87
|
+
isEnabled = true,
|
|
88
|
+
debounceDelay = 100,
|
|
89
|
+
visualViewportThreshold = 100,
|
|
90
|
+
} = options;
|
|
91
|
+
|
|
92
|
+
const [state, setState] = useState<VirtualKeyboardState>({
|
|
93
|
+
isVisible: false,
|
|
94
|
+
height: 0,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const focusedElementRef = useRef<HTMLElement | null>(null);
|
|
98
|
+
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const vv = window.visualViewport;
|
|
102
|
+
const vk = getVirtualKeyboardApi();
|
|
103
|
+
|
|
104
|
+
function setKeyboardInsetHeightEnv(height: number) {
|
|
105
|
+
const element = containerRef?.current || document.documentElement;
|
|
106
|
+
|
|
107
|
+
if (vk) {
|
|
108
|
+
element.style.setProperty(
|
|
109
|
+
'--keyboard-inset-height',
|
|
110
|
+
`env(keyboard-inset-height, ${height}px)`
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
element.style.setProperty('--keyboard-inset-height', `${height}px`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function setKeyboardState(nextState: VirtualKeyboardState) {
|
|
118
|
+
setState((prevState) =>
|
|
119
|
+
prevState.isVisible === nextState.isVisible &&
|
|
120
|
+
prevState.height === nextState.height
|
|
121
|
+
? prevState
|
|
122
|
+
: nextState
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resetKeyboardState() {
|
|
127
|
+
focusedElementRef.current = null;
|
|
128
|
+
setKeyboardInsetHeightEnv(0);
|
|
129
|
+
setKeyboardState({ isVisible: false, height: 0 });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!isEnabled) {
|
|
133
|
+
resetKeyboardState();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function updateKeyboardState() {
|
|
138
|
+
if (debounceTimer.current) {
|
|
139
|
+
clearTimeout(debounceTimer.current);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
debounceTimer.current = setTimeout(() => {
|
|
143
|
+
const active = getActiveElement() ?? focusedElementRef.current;
|
|
144
|
+
const inputIsFocused = active ? willOpenKeyboard(active) : false;
|
|
145
|
+
|
|
146
|
+
if (!inputIsFocused) {
|
|
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 });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (vv) {
|
|
161
|
+
const heightDiff = window.innerHeight - vv.height;
|
|
162
|
+
|
|
163
|
+
if (heightDiff > visualViewportThreshold) {
|
|
164
|
+
setKeyboardInsetHeightEnv(heightDiff);
|
|
165
|
+
setKeyboardState({ isVisible: true, height: heightDiff });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
resetKeyboardState();
|
|
171
|
+
}, debounceDelay);
|
|
172
|
+
}
|
|
173
|
+
|
|
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);
|
|
190
|
+
|
|
191
|
+
if (vv) {
|
|
192
|
+
vv.addEventListener('resize', updateKeyboardState);
|
|
193
|
+
vv.addEventListener('scroll', updateKeyboardState);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (vk) {
|
|
197
|
+
if (virtualKeyboardOverlayUsers === 0) {
|
|
198
|
+
initialVirtualKeyboardOverlaysContent = vk.overlaysContent;
|
|
199
|
+
vk.overlaysContent = true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
virtualKeyboardOverlayUsers++;
|
|
203
|
+
vk.addEventListener('geometrychange', updateKeyboardState);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
focusedElementRef.current = getActiveElement();
|
|
207
|
+
updateKeyboardState();
|
|
208
|
+
|
|
209
|
+
return () => {
|
|
210
|
+
document.removeEventListener('focusin', handleFocusIn);
|
|
211
|
+
document.removeEventListener('focusout', handleFocusOut);
|
|
212
|
+
|
|
213
|
+
if (vv) {
|
|
214
|
+
vv.removeEventListener('resize', updateKeyboardState);
|
|
215
|
+
vv.removeEventListener('scroll', updateKeyboardState);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (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
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (debounceTimer.current) {
|
|
233
|
+
clearTimeout(debounceTimer.current);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
resetKeyboardState();
|
|
237
|
+
};
|
|
238
|
+
}, [debounceDelay, isEnabled, visualViewportThreshold]);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
keyboardHeight: state.height,
|
|
242
|
+
isKeyboardOpen: state.isVisible,
|
|
243
|
+
};
|
|
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/index.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MotionValue } from 'motion/react';
|
|
2
|
+
|
|
3
|
+
import { SheetBackdrop } from './SheetBackdrop';
|
|
4
|
+
import { SheetContainer } from './SheetContainer';
|
|
5
|
+
import { SheetContent } from './SheetContent';
|
|
6
|
+
import { SheetDragIndicator } from './SheetDragIndicator';
|
|
7
|
+
import { SheetHeader } from './SheetHeader';
|
|
8
|
+
import { Sheet as SheetBase } from './sheet';
|
|
9
|
+
import type { SheetCompound } from './types';
|
|
10
|
+
|
|
11
|
+
export interface SheetRef {
|
|
12
|
+
y: MotionValue<number>;
|
|
13
|
+
yInverted: MotionValue<number>;
|
|
14
|
+
height: number;
|
|
15
|
+
snapTo: (index: number) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Sheet: SheetCompound = Object.assign(SheetBase, {
|
|
19
|
+
Container: SheetContainer,
|
|
20
|
+
Header: SheetHeader,
|
|
21
|
+
DragIndicator: SheetDragIndicator,
|
|
22
|
+
Content: SheetContent,
|
|
23
|
+
Backdrop: SheetBackdrop,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export { useScrollPosition } from './hooks/use-scroll-position';
|
|
27
|
+
export { useVirtualKeyboard } from './hooks/use-virtual-keyboard';
|
|
28
|
+
|
|
29
|
+
// Export types
|
|
30
|
+
export type {
|
|
31
|
+
SheetBackdropProps,
|
|
32
|
+
SheetContainerProps,
|
|
33
|
+
SheetContentProps,
|
|
34
|
+
SheetDetent,
|
|
35
|
+
SheetDragIndicatorProps,
|
|
36
|
+
SheetHeaderProps,
|
|
37
|
+
SheetProps,
|
|
38
|
+
SheetSnapPoint,
|
|
39
|
+
SheetStateInfo,
|
|
40
|
+
SheetTweenConfig,
|
|
41
|
+
} from './types';
|