react-modal-sheet 5.1.2 → 5.2.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.
@@ -1,18 +1,62 @@
1
- import { useEffect, useRef, useState } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
 
3
- export function useScrollPosition() {
4
- const ref = useRef<HTMLDivElement>(null);
3
+ type UseScrollPositionOptions = {
4
+ /**
5
+ * Debounce delay in ms for scroll event handling.
6
+ * @default 32
7
+ */
8
+ debounceDelay?: number;
9
+ /**
10
+ * Enable or disable the hook entirely.
11
+ * @default true
12
+ */
13
+ isEnabled?: boolean;
14
+ };
15
+
16
+ /**
17
+ * Hook to track the scroll position of an element.
18
+ *
19
+ * The scroll position can be 'top', 'bottom', 'middle', or undefined if the content is not scrollable.
20
+ * The hook provides a `scrollRef` callback to assign to the scrollable element.
21
+ *
22
+ * Note that the scroll position is only updated when the user stops scrolling
23
+ * for a short moment (debounced). You can set `debounceDelay` to `0` to disable debouncing entirely.
24
+ *
25
+ * @param options Configuration options for the hook.
26
+ * @returns An object containing the `scrollRef` callback and the current `scrollPosition`.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * import { useScrollPosition } from 'react-modal-sheet';
31
+ *
32
+ * function MyComponent() {
33
+ * const { scrollRef, scrollPosition } = useScrollPosition();
34
+ *
35
+ * return (
36
+ * <div>
37
+ * <p>Scroll position: {scrollPosition}</p>
38
+ * <div ref={scrollRef} style={{ overflowY: 'auto', maxHeight: '300px' }}>
39
+ * ...long content...
40
+ * </div>
41
+ * </div>
42
+ * );
43
+ * }
44
+ * ```
45
+ */
46
+ export function useScrollPosition(options: UseScrollPositionOptions = {}) {
47
+ const { debounceDelay = 32, isEnabled = true } = options;
48
+
49
+ const [element, setElement] = useState<HTMLElement | null>(null);
5
50
  const [scrollPosition, setScrollPosition] = useState<
6
51
  'top' | 'bottom' | 'middle' | undefined
7
52
  >(undefined);
8
53
 
9
54
  useEffect(() => {
10
- const element = ref.current;
11
- if (!element) return;
55
+ if (!element || !isEnabled) return;
12
56
 
13
57
  let scrollTimeout: number | null = null;
14
58
 
15
- function determineScrollPosition(element: HTMLDivElement) {
59
+ function determineScrollPosition(element: HTMLElement) {
16
60
  const { scrollTop, scrollHeight, clientHeight } = element;
17
61
  const isScrollable = scrollHeight > clientHeight;
18
62
 
@@ -42,20 +86,30 @@ export function useScrollPosition() {
42
86
  }
43
87
 
44
88
  function onScroll(event: Event) {
45
- if (event.currentTarget instanceof HTMLDivElement) {
89
+ if (event.currentTarget instanceof HTMLElement) {
46
90
  const el = event.currentTarget;
91
+
47
92
  if (scrollTimeout) clearTimeout(scrollTimeout);
48
- // Debounce the scroll handler
49
- scrollTimeout = setTimeout(() => determineScrollPosition(el), 32);
93
+
94
+ if (debounceDelay === 0) {
95
+ determineScrollPosition(el);
96
+ } else {
97
+ // Debounce the scroll handler
98
+ scrollTimeout = setTimeout(
99
+ () => determineScrollPosition(el),
100
+ debounceDelay
101
+ );
102
+ }
50
103
  }
51
104
  }
52
105
 
53
106
  function onTouchStart(event: Event) {
54
- if (event.currentTarget instanceof HTMLDivElement) {
107
+ if (event.currentTarget instanceof HTMLElement) {
55
108
  determineScrollPosition(event.currentTarget);
56
109
  }
57
110
  }
58
111
 
112
+ // Determine initial scroll position
59
113
  determineScrollPosition(element);
60
114
 
61
115
  element.addEventListener('scroll', onScroll);
@@ -66,7 +120,10 @@ export function useScrollPosition() {
66
120
  element.removeEventListener('scroll', onScroll);
67
121
  element.removeEventListener('touchstart', onTouchStart);
68
122
  };
69
- }, []);
123
+ }, [element, isEnabled]);
70
124
 
71
- return { ref, scrollPosition };
125
+ return {
126
+ scrollRef: (element: HTMLElement | null) => setElement(element),
127
+ scrollPosition,
128
+ };
72
129
  }
@@ -8,34 +8,68 @@ type VirtualKeyboardState = {
8
8
 
9
9
  type UseVirtualKeyboardOptions = {
10
10
  /**
11
- * Ref to the container element to apply `keyboard-inset-height` CSS variable updates (required)
11
+ * Ref to the container element to apply `keyboard-inset-height` CSS variable updates.
12
+ * @default document.documentElement
12
13
  */
13
- containerRef: RefObject<HTMLDivElement | null>;
14
+ containerRef?: RefObject<HTMLElement | null>;
14
15
  /**
15
- * Enable or disable the hook entirely (default: true)
16
+ * Enable or disable the hook entirely.
17
+ * @default true
16
18
  */
17
19
  isEnabled?: boolean;
18
20
  /**
19
- * Minimum pixel height difference to consider the keyboard visible (default: 100px)
21
+ * Minimum pixel height difference to consider the keyboard visible.
22
+ * @default 100
20
23
  */
21
24
  visualViewportThreshold?: number;
22
25
  /**
23
- * Whether to treat contenteditable elements as text inputs (default: true)
26
+ * Whether to treat contenteditable elements as text inputs.
27
+ * @default true
24
28
  */
25
29
  includeContentEditable?: boolean;
26
30
  /**
27
- * Delay in ms for debouncing viewport changes (default: 100ms)
31
+ * Delay in ms for debouncing viewport changes.
32
+ * @default 100
28
33
  */
29
34
  debounceDelay?: number;
30
35
  };
31
36
 
32
- export function useVirtualKeyboard({
33
- containerRef,
34
- isEnabled = true,
35
- debounceDelay = 100,
36
- includeContentEditable = true,
37
- visualViewportThreshold = 100,
38
- }: UseVirtualKeyboardOptions) {
37
+ /**
38
+ * A hook that detects virtual keyboard visibility and height.
39
+ * It listens to focus events and visual viewport changes to determine
40
+ * if a text input is focused and the keyboard is likely visible.
41
+ *
42
+ * It also sets the `--keyboard-inset-height` CSS variable on the specified container
43
+ * (or `:root` by default) to allow for easy styling adjustments when the keyboard is open.
44
+ *
45
+ * @param options Configuration options for the hook.
46
+ * @returns An object containing `isKeyboardOpen` and `keyboardHeight`.
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * import { useVirtualKeyboard } from 'react-modal-sheet';
51
+ *
52
+ * function MyComponent() {
53
+ * const { isKeyboardOpen, keyboardHeight } = useVirtualKeyboard();
54
+ *
55
+ * return (
56
+ * <div>
57
+ * <p>Keyboard is {isKeyboardOpen ? 'open' : 'closed'}</p>
58
+ * <p>Keyboard height: {keyboardHeight}px</p>
59
+ * </div>
60
+ * );
61
+ * }
62
+ * ```
63
+ */
64
+ export function useVirtualKeyboard(options: UseVirtualKeyboardOptions = {}) {
65
+ const {
66
+ containerRef,
67
+ isEnabled = true,
68
+ debounceDelay = 100,
69
+ includeContentEditable = true,
70
+ visualViewportThreshold = 100,
71
+ } = options;
72
+
39
73
  const [state, setState] = useState<VirtualKeyboardState>({
40
74
  isVisible: false,
41
75
  height: 0,
@@ -61,10 +95,17 @@ export function useVirtualKeyboard({
61
95
  const vk = (navigator as any).virtualKeyboard;
62
96
 
63
97
  function setKeyboardInsetHeightEnv(height: number) {
64
- containerRef.current?.style.setProperty(
65
- '--keyboard-inset-height',
66
- `${height}px`
67
- );
98
+ const element = containerRef?.current || document.documentElement;
99
+
100
+ // Virtual Keyboard API is only available in secure context
101
+ if (window.isSecureContext) {
102
+ element.style.setProperty(
103
+ '--keyboard-inset-height',
104
+ `env(keyboard-inset-height, ${height}px)`
105
+ );
106
+ } else {
107
+ element.style.setProperty('--keyboard-inset-height', `${height}px`);
108
+ }
68
109
  }
69
110
 
70
111
  function handleFocusIn(e: FocusEvent) {
package/src/index.tsx CHANGED
@@ -23,6 +23,9 @@ export const Sheet: SheetCompound = Object.assign(SheetBase, {
23
23
  Backdrop: SheetBackdrop,
24
24
  });
25
25
 
26
+ export { useScrollPosition } from './hooks/use-scroll-position';
27
+ export { useVirtualKeyboard } from './hooks/use-virtual-keyboard';
28
+
26
29
  // Export types
27
30
  export type {
28
31
  SheetBackdropProps,
package/src/types.tsx CHANGED
@@ -1,6 +1,13 @@
1
1
  import {
2
- type CSSProperties,
2
+ type DragHandler,
3
+ type EasingDefinition,
4
+ type MotionProps,
5
+ type MotionValue,
6
+ type motion,
7
+ } from 'motion/react';
8
+ import {
3
9
  type ComponentPropsWithoutRef,
10
+ type CSSProperties,
4
11
  type ForwardRefExoticComponent,
5
12
  type FunctionComponent,
6
13
  type HTMLAttributes,
@@ -9,14 +16,6 @@ import {
9
16
  type RefObject,
10
17
  } from 'react';
11
18
 
12
- import {
13
- type DragHandler,
14
- type EasingDefinition,
15
- type MotionProps,
16
- type MotionValue,
17
- type motion,
18
- } from 'motion/react';
19
-
20
19
  export type SheetDetent = 'default' | 'full' | 'content';
21
20
 
22
21
  type CommonProps = {
@@ -76,6 +75,8 @@ export type SheetContentProps = MotionCommonProps &
76
75
  disableDrag?: boolean | ((args: SheetStateInfo) => boolean);
77
76
  disableScroll?: boolean | ((args: SheetStateInfo) => boolean);
78
77
  scrollRef?: RefObject<HTMLDivElement | null>;
78
+ scrollClassName?: string;
79
+ scrollStyle?: CSSProperties;
79
80
  };
80
81
 
81
82
  export type SheetBackdropProps = MotionCommonProps & CommonProps;