react-modal-sheet 4.4.0 → 5.0.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,34 +1,96 @@
1
+ import { type MotionStyle, motion } from 'motion/react';
1
2
  import React, { forwardRef } from 'react';
2
- import { motion } from 'motion/react';
3
3
 
4
- import { type SheetDraggableProps } from './types';
5
- import { useSheetScrollerContext, useSheetContext } from './context';
4
+ import { useSheetContext } from './context';
6
5
  import { useDragConstraints } from './hooks/use-drag-constraints';
7
- import { mergeRefs } from './utils';
6
+ import { useScrollPosition } from './hooks/use-scroll-position';
8
7
  import { styles } from './styles';
8
+ import { type SheetContentScrollableProps } from './types';
9
+ import { mergeRefs } from './utils';
9
10
 
10
- export const SheetContent = forwardRef<any, SheetDraggableProps>(
11
- ({ children, style, disableDrag, className = '', ...rest }, ref) => {
11
+ export const SheetContent = forwardRef<any, SheetContentScrollableProps>(
12
+ (
13
+ {
14
+ disableScroll: disableScrollProp,
15
+ disableDrag: disableDragProp,
16
+ children,
17
+ style: styleProp,
18
+ className = '',
19
+ scrollRef: scrollRefProp = null,
20
+ ...rest
21
+ },
22
+ ref
23
+ ) => {
12
24
  const sheetContext = useSheetContext();
13
- const sheetScrollerContext = useSheetScrollerContext();
14
- const { constraintsRef, onMeasureDragConstraints } = useDragConstraints();
25
+ const dragConstraints = useDragConstraints();
26
+ const scroll = useScrollPosition();
27
+
28
+ const disableScroll =
29
+ typeof disableScrollProp === 'function'
30
+ ? disableScrollProp({
31
+ scrollPosition: scroll.scrollPosition,
32
+ currentSnap: sheetContext.currentSnap,
33
+ })
34
+ : Boolean(disableScrollProp);
35
+
36
+ const disableDragDueToScroll =
37
+ !disableScroll &&
38
+ scroll.scrollPosition &&
39
+ scroll.scrollPosition !== 'top';
40
+
41
+ const disableDragDueToProp =
42
+ typeof disableDragProp === 'function'
43
+ ? disableDragProp({
44
+ scrollPosition: scroll.scrollPosition,
45
+ currentSnap: sheetContext.currentSnap,
46
+ })
47
+ : Boolean(disableDragProp);
48
+
49
+ const disableDrag =
50
+ disableDragDueToProp ||
51
+ disableDragDueToScroll ||
52
+ sheetContext.disableDrag;
15
53
 
16
54
  const dragProps =
17
- disableDrag || sheetScrollerContext.disableDrag
55
+ disableDrag || sheetContext.disableDrag
18
56
  ? undefined
19
57
  : sheetContext.dragProps;
20
58
 
59
+ const contentStyle: MotionStyle = {
60
+ ...styles.content,
61
+ ...styleProp,
62
+ };
63
+
64
+ const scrollStyle: MotionStyle = {
65
+ ...styles.scroller,
66
+ };
67
+
68
+ if (sheetContext.avoidKeyboard) {
69
+ scrollStyle.paddingBottom =
70
+ 'env(keyboard-inset-height, var(--keyboard-inset-height, 0px))';
71
+ }
72
+
73
+ if (disableScroll) {
74
+ scrollStyle.overflowY = 'hidden';
75
+ }
76
+
21
77
  return (
22
78
  <motion.div
23
79
  {...rest}
24
- ref={mergeRefs([ref, constraintsRef])}
80
+ ref={mergeRefs([ref, dragConstraints.ref])}
25
81
  className={`react-modal-sheet-content ${className}`}
26
- style={{ ...styles.content, ...style }}
82
+ style={contentStyle}
27
83
  {...dragProps}
28
- dragConstraints={constraintsRef}
29
- onMeasureDragConstraints={onMeasureDragConstraints}
84
+ dragConstraints={dragConstraints.ref}
85
+ onMeasureDragConstraints={dragConstraints.onMeasure}
30
86
  >
31
- {children}
87
+ <motion.div
88
+ ref={mergeRefs([scroll.ref, scrollRefProp])}
89
+ style={scrollStyle}
90
+ className="react-modal-sheet-content-scroller"
91
+ >
92
+ {children}
93
+ </motion.div>
32
94
  </motion.div>
33
95
  );
34
96
  }
@@ -1,36 +1,39 @@
1
- import React, { forwardRef } from 'react';
2
1
  import { motion, useTransform } from 'motion/react';
2
+ import React, { forwardRef } from 'react';
3
3
 
4
- import { type SheetDraggableProps } from './types';
5
4
  import { useSheetContext } from './context';
6
5
  import { useDragConstraints } from './hooks/use-drag-constraints';
7
- import { mergeRefs } from './utils';
8
6
  import { styles } from './styles';
7
+ import { type SheetDraggableProps } from './types';
8
+ import { mergeRefs } from './utils';
9
9
 
10
10
  export const SheetHeader = forwardRef<any, SheetDraggableProps>(
11
11
  ({ children, style, disableDrag, ...rest }, ref) => {
12
- const { indicatorRotation, dragProps } = useSheetContext();
13
- const { constraintsRef, onMeasureDragConstraints } = useDragConstraints();
14
- const _dragProps = disableDrag ? undefined : dragProps;
12
+ const sheetContext = useSheetContext();
13
+ const dragConstraints = useDragConstraints();
14
+ const dragProps =
15
+ disableDrag || sheetContext.disableDrag
16
+ ? undefined
17
+ : sheetContext.dragProps;
15
18
 
16
19
  const indicator1Transform = useTransform(
17
- indicatorRotation,
20
+ sheetContext.indicatorRotation,
18
21
  (r) => `translateX(2px) rotate(${r}deg)`
19
22
  );
20
23
 
21
24
  const indicator2Transform = useTransform(
22
- indicatorRotation,
25
+ sheetContext.indicatorRotation,
23
26
  (r) => `translateX(-2px) rotate(${-1 * r}deg)`
24
27
  );
25
28
 
26
29
  return (
27
30
  <motion.div
28
31
  {...rest}
29
- ref={mergeRefs([ref, constraintsRef])}
32
+ ref={mergeRefs([ref, dragConstraints.ref])}
30
33
  style={{ ...styles.headerWrapper, ...style }}
31
- {..._dragProps}
32
- dragConstraints={constraintsRef}
33
- onMeasureDragConstraints={onMeasureDragConstraints}
34
+ {...dragProps}
35
+ dragConstraints={dragConstraints.ref}
36
+ onMeasureDragConstraints={dragConstraints.onMeasure}
34
37
  >
35
38
  {children || (
36
39
  <div className="react-modal-sheet-header" style={styles.header}>
package/src/constants.ts CHANGED
@@ -1,13 +1,15 @@
1
- export const MAX_HEIGHT = 'calc(100% - env(safe-area-inset-top) - 34px)';
1
+ import type { SheetTweenConfig } from './types';
2
+
3
+ export const DEFAULT_HEIGHT = 'calc(100% - env(safe-area-inset-top) - 34px)';
2
4
 
3
5
  export const IS_SSR = typeof window === 'undefined';
4
6
 
5
- export const DEFAULT_TWEEN_CONFIG = {
7
+ export const DEFAULT_TWEEN_CONFIG: SheetTweenConfig = {
6
8
  ease: 'easeOut',
7
9
  duration: 0.2,
8
10
  };
9
11
 
10
- export const REDUCED_MOTION_TWEEN_CONFIG = {
12
+ export const REDUCED_MOTION_TWEEN_CONFIG: SheetTweenConfig = {
11
13
  ease: 'linear',
12
14
  duration: 0.01,
13
15
  };
package/src/context.tsx CHANGED
@@ -1,10 +1,5 @@
1
- import React, {
2
- type ReactNode,
3
- createContext,
4
- useContext,
5
- useState,
6
- } from 'react';
7
- import { type SheetScrollerContextType, type SheetContextType } from './types';
1
+ import { createContext, useContext } from 'react';
2
+ import { type SheetContextType } from './types';
8
3
 
9
4
  export const SheetContext = createContext<SheetContextType | undefined>(
10
5
  undefined
@@ -15,38 +10,3 @@ export function useSheetContext() {
15
10
  if (!context) throw new Error('Sheet context error');
16
11
  return context;
17
12
  }
18
-
19
- export const SheetScrollerContext = createContext<
20
- SheetScrollerContextType | undefined
21
- >(undefined);
22
-
23
- export function SheetScrollerContextProvider({
24
- children,
25
- }: {
26
- children: ReactNode;
27
- }) {
28
- const sheetContext = useSheetContext();
29
- const [disableDrag, setDisableDrag] = useState(!!sheetContext.disableDrag);
30
-
31
- function setDragEnabled() {
32
- if (!sheetContext.disableDrag) setDisableDrag(false);
33
- }
34
-
35
- function setDragDisabled() {
36
- if (!disableDrag) setDisableDrag(true);
37
- }
38
-
39
- return (
40
- <SheetScrollerContext.Provider
41
- value={{ disableDrag, setDragEnabled, setDragDisabled }}
42
- >
43
- {children}
44
- </SheetScrollerContext.Provider>
45
- );
46
- }
47
-
48
- export function useSheetScrollerContext() {
49
- const context = useContext(SheetScrollerContext);
50
- if (!context) throw new Error('Sheet scroller context error');
51
- return context;
52
- }
@@ -5,15 +5,15 @@ import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
5
5
 
6
6
  export function useDimensions() {
7
7
  const [dimensions, setDimensions] = useState(() => ({
8
- height: !IS_SSR ? window.innerHeight : 0,
9
- width: !IS_SSR ? window.innerWidth : 0,
8
+ windowHeight: !IS_SSR ? window.innerHeight : 0,
9
+ windowWidth: !IS_SSR ? window.innerWidth : 0,
10
10
  }));
11
11
 
12
12
  useIsomorphicLayoutEffect(() => {
13
13
  function handler() {
14
14
  setDimensions({
15
- height: window.innerHeight,
16
- width: window.innerWidth,
15
+ windowHeight: window.innerHeight,
16
+ windowWidth: window.innerWidth,
17
17
  });
18
18
  }
19
19
 
@@ -8,7 +8,7 @@ import { type BoundingBox } from 'motion/react';
8
8
  const constraints: BoundingBox = { bottom: 0, top: 0, left: 0, right: 0 };
9
9
 
10
10
  export function useDragConstraints() {
11
- const constraintsRef = useRef<any>(null);
12
- const onMeasureDragConstraints = useCallback(() => constraints, []);
13
- return { constraintsRef, onMeasureDragConstraints };
11
+ const ref = useRef<any>(null);
12
+ const onMeasure = useCallback(() => constraints, []);
13
+ return { ref, onMeasure };
14
14
  }
@@ -1,25 +1,34 @@
1
- import { transform, type MotionValue } from 'motion';
1
+ import { type MotionValue, transform } from 'motion';
2
2
  import { type RefObject } from 'react';
3
3
 
4
+ import type { SheetDetent, SheetSnapPoint } from '../types';
4
5
  import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
5
6
  import { useSafeAreaInsets } from './use-safe-area-insets';
6
- import { getSheetHeight, getSnapPoints } from '../utils';
7
7
 
8
8
  export function useModalEffect({
9
9
  y,
10
- rootId,
11
- sheetRef,
12
- snapPointsProp,
10
+ detent,
11
+ rootId: _rootId,
12
+ sheetHeight,
13
+ snapPoints,
13
14
  startThreshold,
14
15
  }: {
15
16
  y: MotionValue<number>;
17
+ detent: SheetDetent;
16
18
  rootId?: string;
17
- sheetRef: RefObject<HTMLDivElement | null>;
18
- snapPointsProp?: number[];
19
+ sheetHeight: number;
20
+ snapPoints: SheetSnapPoint[];
19
21
  startThreshold?: number;
20
22
  }) {
21
23
  const insetTop = useSafeAreaInsets().top;
22
24
 
25
+ let rootId: string | undefined = _rootId;
26
+
27
+ if (rootId && detent === 'full') {
28
+ console.warn('Using "full" detent with modal effect is not supported.');
29
+ rootId = undefined;
30
+ }
31
+
23
32
  // Cleanup on unmount
24
33
  useIsomorphicLayoutEffect(() => {
25
34
  return () => {
@@ -33,15 +42,7 @@ export function useModalEffect({
33
42
  const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
34
43
  if (!root) return;
35
44
 
36
- let sheetHeight = 0;
37
-
38
45
  const removeStartListener = y.on('animationStart', () => {
39
- /**
40
- * Only calculate the sheet height when the animation starts to avoid
41
- * unnecessary layout calculations when y value changes.
42
- */
43
- sheetHeight = getSheetHeight(sheetRef);
44
-
45
46
  // biome-ignore lint/style/noNonNullAssertion: root is always defined here
46
47
  setupModalEffect(rootId!);
47
48
  });
@@ -55,17 +56,13 @@ export function useModalEffect({
55
56
 
56
57
  let progress = Math.max(0, 1 - yValue / sheetHeight);
57
58
 
58
- const snapPoints = snapPointsProp
59
- ? getSnapPoints({ snapPointsProp, sheetHeight })
60
- : undefined;
61
-
62
59
  /**
63
60
  * Start the effect only if we have dragged over the second snap point
64
61
  * to make the effect more natural as the sheet will reach it's final
65
62
  * position when the user drags it over the second snap point.
66
63
  */
67
64
  const snapThresholdPoint =
68
- snapPoints && snapPoints.length > 1 ? snapPoints[1] : undefined;
65
+ snapPoints.length > 1 ? snapPoints[snapPoints.length - 2] : undefined;
69
66
 
70
67
  /**
71
68
  * If we have snap points, we need to calculate the progress percentage
@@ -74,8 +71,7 @@ export function useModalEffect({
74
71
  * and its end is different.
75
72
  */
76
73
  if (snapThresholdPoint !== undefined) {
77
- const snapThresholdValue =
78
- sheetHeight - Math.min(snapThresholdPoint, sheetHeight);
74
+ const snapThresholdValue = snapThresholdPoint.snapValueY;
79
75
 
80
76
  if (yValue <= snapThresholdValue) {
81
77
  progress = (snapThresholdValue - yValue) / snapThresholdValue;
@@ -132,7 +128,7 @@ export function useModalEffect({
132
128
  removeCompleteListener();
133
129
  removeCancelListener();
134
130
  };
135
- }, [y, rootId, insetTop, startThreshold, snapPointsProp]);
131
+ }, [y, rootId, insetTop, startThreshold, sheetHeight]);
136
132
  }
137
133
 
138
134
  function setupModalEffect(rootId: string) {
@@ -0,0 +1,31 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { useStableCallback } from './use-stable-callback';
3
+
4
+ export function useResizeObserver<T extends Element = HTMLDivElement>(
5
+ callback: ResizeObserverCallback
6
+ ) {
7
+ const observeRef = useRef<T | null>(null);
8
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
9
+
10
+ const debouncedCallback: ResizeObserverCallback = useStableCallback(
11
+ (entries, observer) => {
12
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
13
+ timeoutRef.current = setTimeout(() => callback(entries, observer), 32);
14
+ }
15
+ );
16
+
17
+ useEffect(() => {
18
+ const element = observeRef.current;
19
+ if (!element) return;
20
+
21
+ const observer = new ResizeObserver(debouncedCallback);
22
+ observer.observe(element, { box: 'border-box' });
23
+
24
+ return () => {
25
+ observer.disconnect();
26
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
27
+ };
28
+ }, []);
29
+
30
+ return { observeRef };
31
+ }
@@ -0,0 +1,72 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ export function useScrollPosition() {
4
+ const ref = useRef<HTMLDivElement>(null);
5
+ const [scrollPosition, setScrollPosition] = useState<
6
+ 'top' | 'bottom' | 'middle' | undefined
7
+ >(undefined);
8
+
9
+ useEffect(() => {
10
+ const element = ref.current;
11
+ if (!element) return;
12
+
13
+ let scrollTimeout: number | null = null;
14
+
15
+ function determineScrollPosition(element: HTMLDivElement) {
16
+ const { scrollTop, scrollHeight, clientHeight } = element;
17
+ const isScrollable = scrollHeight > clientHeight;
18
+
19
+ if (!isScrollable) {
20
+ // Reset scroll position if the content is not scrollable anymore
21
+ if (scrollPosition) setScrollPosition(undefined);
22
+ return;
23
+ }
24
+
25
+ const isAtTop = scrollTop <= 0;
26
+ const isAtBottom =
27
+ Math.ceil(scrollHeight) - Math.ceil(scrollTop) ===
28
+ Math.ceil(clientHeight);
29
+
30
+ let position: 'top' | 'bottom' | 'middle';
31
+
32
+ if (isAtTop) {
33
+ position = 'top';
34
+ } else if (isAtBottom) {
35
+ position = 'bottom';
36
+ } else {
37
+ position = 'middle';
38
+ }
39
+
40
+ if (position === scrollPosition) return;
41
+ setScrollPosition(position);
42
+ }
43
+
44
+ function onScroll(event: Event) {
45
+ if (event.currentTarget instanceof HTMLDivElement) {
46
+ const el = event.currentTarget;
47
+ if (scrollTimeout) clearTimeout(scrollTimeout);
48
+ // Debounce the scroll handler
49
+ scrollTimeout = setTimeout(() => determineScrollPosition(el), 32);
50
+ }
51
+ }
52
+
53
+ function onTouchStart(event: Event) {
54
+ if (event.currentTarget instanceof HTMLDivElement) {
55
+ determineScrollPosition(event.currentTarget);
56
+ }
57
+ }
58
+
59
+ determineScrollPosition(element);
60
+
61
+ element.addEventListener('scroll', onScroll);
62
+ element.addEventListener('touchstart', onTouchStart);
63
+
64
+ return () => {
65
+ if (scrollTimeout) clearTimeout(scrollTimeout);
66
+ element.removeEventListener('scroll', onScroll);
67
+ element.removeEventListener('touchstart', onTouchStart);
68
+ };
69
+ }, []);
70
+
71
+ return { ref, scrollPosition };
72
+ }
@@ -0,0 +1,63 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useStableCallback } from './use-stable-callback';
3
+
4
+ type SheetState = 'closed' | 'opening' | 'open' | 'closing';
5
+
6
+ type UseSheetStatesProps = {
7
+ isOpen: boolean;
8
+ onClosed?: () => Promise<void> | void;
9
+ onOpening?: () => Promise<void> | void;
10
+ onOpen?: () => Promise<void> | void;
11
+ onClosing?: () => Promise<void> | void;
12
+ };
13
+
14
+ export function useSheetState({
15
+ isOpen,
16
+ onClosed: _onClosed,
17
+ onOpening: _onOpening,
18
+ onOpen: _onOpen,
19
+ onClosing: _onClosing,
20
+ }: UseSheetStatesProps) {
21
+ const [state, setState] = useState<SheetState>(isOpen ? 'opening' : 'closed');
22
+ const onClosed = useStableCallback(() => _onClosed?.());
23
+ const onOpening = useStableCallback(() => _onOpening?.());
24
+ const onOpen = useStableCallback(() => _onOpen?.());
25
+ const onClosing = useStableCallback(() => _onClosing?.());
26
+
27
+ useEffect(() => {
28
+ if (isOpen && state === 'closed') {
29
+ setState('opening');
30
+ } else if (!isOpen && (state === 'open' || state === 'opening')) {
31
+ setState('closing');
32
+ }
33
+ }, [isOpen, state]);
34
+
35
+ useEffect(() => {
36
+ async function handle() {
37
+ switch (state) {
38
+ case 'closed':
39
+ await onClosed?.();
40
+ break;
41
+
42
+ case 'opening':
43
+ await onOpening?.();
44
+ setState('open');
45
+ break;
46
+
47
+ case 'open':
48
+ await onOpen?.();
49
+ break;
50
+
51
+ case 'closing':
52
+ await onClosing?.();
53
+ setState('closed');
54
+ break;
55
+ }
56
+ }
57
+ handle().catch((error) => {
58
+ console.error('Internal sheet state error:', error);
59
+ });
60
+ }, [state]);
61
+
62
+ return state;
63
+ }
@@ -0,0 +1,153 @@
1
+ import { type RefObject, useEffect, useRef, useState } from 'react';
2
+ import { useStableCallback } from './use-stable-callback';
3
+
4
+ type VirtualKeyboardState = {
5
+ isVisible: boolean;
6
+ height: number;
7
+ };
8
+
9
+ type UseVirtualKeyboardOptions = {
10
+ /**
11
+ * Ref to the container element to apply `keyboard-inset-height` CSS variable updates (required)
12
+ */
13
+ containerRef: RefObject<HTMLDivElement | null>;
14
+ /**
15
+ * Enable or disable the hook entirely (default: true)
16
+ */
17
+ isEnabled?: boolean;
18
+ /**
19
+ * Minimum pixel height difference to consider the keyboard visible (default: 100px)
20
+ */
21
+ visualViewportThreshold?: number;
22
+ /**
23
+ * Whether to treat contenteditable elements as text inputs (default: true)
24
+ */
25
+ includeContentEditable?: boolean;
26
+ /**
27
+ * Delay in ms for debouncing viewport changes (default: 100ms)
28
+ */
29
+ debounceDelay?: number;
30
+ };
31
+
32
+ export function useVirtualKeyboard({
33
+ containerRef,
34
+ isEnabled = true,
35
+ debounceDelay = 100,
36
+ includeContentEditable = true,
37
+ visualViewportThreshold = 100,
38
+ }: UseVirtualKeyboardOptions) {
39
+ const [state, setState] = useState<VirtualKeyboardState>({
40
+ isVisible: false,
41
+ height: 0,
42
+ });
43
+
44
+ const focusedElementRef = useRef<HTMLElement | null>(null);
45
+ const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
46
+
47
+ const isTextInput = useStableCallback((el: Element | null) => {
48
+ return (
49
+ el?.tagName === 'INPUT' ||
50
+ el?.tagName === 'TEXTAREA' ||
51
+ (includeContentEditable &&
52
+ el instanceof HTMLElement &&
53
+ el.isContentEditable)
54
+ );
55
+ });
56
+
57
+ useEffect(() => {
58
+ if (!isEnabled) return;
59
+
60
+ const vv = window.visualViewport;
61
+ const vk = (navigator as any).virtualKeyboard;
62
+
63
+ function setKeyboardInsetHeightEnv(height: number) {
64
+ containerRef.current?.style.setProperty(
65
+ '--keyboard-inset-height',
66
+ `${height}px`
67
+ );
68
+ }
69
+
70
+ function handleFocusIn(e: FocusEvent) {
71
+ if (e.target instanceof HTMLElement && isTextInput(e.target)) {
72
+ focusedElementRef.current = e.target;
73
+ updateKeyboardState();
74
+ }
75
+ }
76
+
77
+ function handleFocusOut() {
78
+ focusedElementRef.current = null;
79
+ updateKeyboardState();
80
+ }
81
+
82
+ function updateKeyboardState() {
83
+ if (debounceTimer.current) {
84
+ clearTimeout(debounceTimer.current);
85
+ }
86
+ debounceTimer.current = setTimeout(() => {
87
+ const active = focusedElementRef.current;
88
+ const inputIsFocused = isTextInput(active);
89
+
90
+ if (!inputIsFocused) {
91
+ setKeyboardInsetHeightEnv(0);
92
+ setState({ isVisible: false, height: 0 });
93
+ return;
94
+ }
95
+
96
+ if (vv) {
97
+ const heightDiff = window.innerHeight - vv.height;
98
+
99
+ if (heightDiff > visualViewportThreshold) {
100
+ setKeyboardInsetHeightEnv(heightDiff);
101
+ setState({ isVisible: true, height: heightDiff });
102
+ } else {
103
+ setKeyboardInsetHeightEnv(0);
104
+ setState({ isVisible: false, height: 0 });
105
+ }
106
+ }
107
+ }, debounceDelay);
108
+ }
109
+
110
+ window.addEventListener('focusin', handleFocusIn);
111
+ window.addEventListener('focusout', handleFocusOut);
112
+
113
+ if (vv) {
114
+ vv.addEventListener('resize', updateKeyboardState);
115
+ vv.addEventListener('scroll', updateKeyboardState);
116
+ }
117
+
118
+ let currentOverlaysContent = false;
119
+
120
+ if (vk) {
121
+ currentOverlaysContent = vk.overlaysContent;
122
+ vk.overlaysContent = true;
123
+ }
124
+
125
+ return () => {
126
+ window.removeEventListener('focusin', handleFocusIn);
127
+ window.removeEventListener('focusout', handleFocusOut);
128
+
129
+ if (vv) {
130
+ vv.removeEventListener('resize', updateKeyboardState);
131
+ vv.removeEventListener('scroll', updateKeyboardState);
132
+ }
133
+
134
+ if (vk) {
135
+ vk.overlaysContent = currentOverlaysContent;
136
+ }
137
+
138
+ if (debounceTimer.current) {
139
+ clearTimeout(debounceTimer.current);
140
+ }
141
+ };
142
+ }, [
143
+ debounceDelay,
144
+ includeContentEditable,
145
+ isEnabled,
146
+ visualViewportThreshold,
147
+ ]);
148
+
149
+ return {
150
+ keyboardHeight: state.height,
151
+ isKeyboardOpen: state.isVisible,
152
+ };
153
+ }