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/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' = '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
- debugElement.textContent = JSON.stringify(payload, null, 2);
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;