react-modal-sheet 5.3.1 → 5.4.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/README.md +9 -6
- package/dist/index.js +73 -27
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +73 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/SheetBackdrop.tsx +10 -4
- package/src/debug.ts +3 -2
- package/src/hooks/use-focused-input.ts +46 -0
- package/src/sheet.tsx +63 -49
- package/src/types.tsx +1 -0
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,46 @@
|
|
|
1
|
+
import { type RefObject, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useFocusedInput({
|
|
4
|
+
isEnabled,
|
|
5
|
+
sheetRef,
|
|
6
|
+
}: {
|
|
7
|
+
isEnabled: boolean;
|
|
8
|
+
sheetRef: RefObject<HTMLElement | null>;
|
|
9
|
+
}) {
|
|
10
|
+
const [focusedInput, setFocusedInput] = useState<HTMLElement | null>(null);
|
|
11
|
+
|
|
12
|
+
// Keep track of the currently focused input within the sheet
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!isEnabled) return;
|
|
15
|
+
|
|
16
|
+
const handleFocusIn = (event: FocusEvent) => {
|
|
17
|
+
const focusedElement = event.target as HTMLElement;
|
|
18
|
+
|
|
19
|
+
const isInput =
|
|
20
|
+
focusedElement?.tagName === 'INPUT' ||
|
|
21
|
+
focusedElement?.tagName === 'TEXTAREA';
|
|
22
|
+
|
|
23
|
+
if (isInput && sheetRef.current?.contains(focusedElement)) {
|
|
24
|
+
setFocusedInput(focusedElement);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleFocusOut = (event: FocusEvent) => {
|
|
29
|
+
const focusedElement = event.target as HTMLElement;
|
|
30
|
+
|
|
31
|
+
if (focusedElement === focusedInput) {
|
|
32
|
+
setFocusedInput(null);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
document.addEventListener('focusin', handleFocusIn);
|
|
37
|
+
document.addEventListener('focusout', handleFocusOut);
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
document.removeEventListener('focusin', handleFocusIn);
|
|
41
|
+
document.removeEventListener('focusout', handleFocusOut);
|
|
42
|
+
};
|
|
43
|
+
}, [isEnabled, focusedInput]);
|
|
44
|
+
|
|
45
|
+
return focusedInput;
|
|
46
|
+
}
|
package/src/sheet.tsx
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from './constants';
|
|
27
27
|
import { SheetContext } from './context';
|
|
28
28
|
import { useDimensions } from './hooks/use-dimensions';
|
|
29
|
+
import { useFocusedInput } from './hooks/use-focused-input';
|
|
29
30
|
import { useModalEffect } from './hooks/use-modal-effect';
|
|
30
31
|
import { usePreventScroll } from './hooks/use-prevent-scroll';
|
|
31
32
|
import { useSheetState } from './hooks/use-sheet-state';
|
|
@@ -277,55 +278,6 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
277
278
|
snapTo,
|
|
278
279
|
}));
|
|
279
280
|
|
|
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
281
|
useModalEffect({
|
|
330
282
|
y,
|
|
331
283
|
detent,
|
|
@@ -343,6 +295,67 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
343
295
|
isDisabled: disableScrollLocking || !isOpen,
|
|
344
296
|
});
|
|
345
297
|
|
|
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
|
+
|
|
346
359
|
const state = useSheetState({
|
|
347
360
|
isOpen,
|
|
348
361
|
onOpen: async () => {
|
|
@@ -394,6 +407,7 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
394
407
|
dragProps,
|
|
395
408
|
indicatorRotation,
|
|
396
409
|
avoidKeyboard,
|
|
410
|
+
prefersReducedMotion,
|
|
397
411
|
sheetBoundsRef,
|
|
398
412
|
sheetRef,
|
|
399
413
|
unstyled,
|
package/src/types.tsx
CHANGED
|
@@ -111,6 +111,7 @@ export interface SheetContextType {
|
|
|
111
111
|
dragProps?: SheetDragProps;
|
|
112
112
|
indicatorRotation: MotionValue<number>;
|
|
113
113
|
avoidKeyboard: boolean;
|
|
114
|
+
prefersReducedMotion: boolean;
|
|
114
115
|
sheetBoundsRef: (node: HTMLDivElement | null) => void;
|
|
115
116
|
sheetRef: RefObject<any>;
|
|
116
117
|
unstyled: boolean;
|