react-native-keyboard-toolkit 0.0.0-stage → 1.0.1

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +256 -2
  4. package/dist/components/KeyboardAwareScrollView.d.ts +6 -0
  5. package/dist/components/KeyboardAwareScrollView.js +137 -0
  6. package/dist/components/KeyboardAwareView.d.ts +6 -0
  7. package/dist/components/KeyboardAwareView.js +112 -0
  8. package/dist/components/KeyboardDismissButton.d.ts +5 -0
  9. package/dist/components/KeyboardDismissButton.js +16 -0
  10. package/dist/components/KeyboardToolbar.d.ts +5 -0
  11. package/dist/components/KeyboardToolbar.js +109 -0
  12. package/dist/components/index.d.ts +4 -0
  13. package/dist/components/index.js +11 -0
  14. package/dist/context/KeyboardContext.d.ts +6 -0
  15. package/dist/context/KeyboardContext.js +27 -0
  16. package/dist/context/index.d.ts +1 -0
  17. package/dist/context/index.js +5 -0
  18. package/dist/hooks/index.d.ts +4 -0
  19. package/dist/hooks/index.js +11 -0
  20. package/dist/hooks/useFocusedInput.d.ts +6 -0
  21. package/dist/hooks/useFocusedInput.js +42 -0
  22. package/dist/hooks/useKeyboard.d.ts +1 -0
  23. package/dist/hooks/useKeyboard.js +5 -0
  24. package/dist/hooks/useKeyboardAnimation.d.ts +6 -0
  25. package/dist/hooks/useKeyboardAnimation.js +43 -0
  26. package/dist/hooks/useKeyboardState.d.ts +2 -0
  27. package/dist/hooks/useKeyboardState.js +9 -0
  28. package/dist/index.d.ts +5 -0
  29. package/dist/index.js +23 -0
  30. package/dist/types/index.d.ts +69 -0
  31. package/dist/types/index.js +2 -0
  32. package/dist/utils/constants.d.ts +2 -0
  33. package/dist/utils/constants.js +5 -0
  34. package/dist/utils/focusManager.d.ts +10 -0
  35. package/dist/utils/focusManager.js +34 -0
  36. package/dist/utils/index.d.ts +3 -0
  37. package/dist/utils/index.js +13 -0
  38. package/dist/utils/keyboardEvents.d.ts +7 -0
  39. package/dist/utils/keyboardEvents.js +88 -0
  40. package/dist/utils/platform.d.ts +11 -0
  41. package/dist/utils/platform.js +19 -0
  42. package/dist/utils/scrollManager.d.ts +21 -0
  43. package/dist/utils/scrollManager.js +37 -0
  44. package/docs/android.md +31 -0
  45. package/docs/api.md +78 -0
  46. package/docs/assets/demo-preview.svg +24 -0
  47. package/docs/installation.md +22 -0
  48. package/docs/ios.md +27 -0
  49. package/docs/troubleshooting.md +44 -0
  50. package/package.json +90 -4
  51. package/src/components/KeyboardAwareScrollView.tsx +240 -0
  52. package/src/components/KeyboardAwareView.tsx +172 -0
  53. package/src/components/KeyboardDismissButton.tsx +32 -0
  54. package/src/components/KeyboardToolbar.tsx +200 -0
  55. package/src/components/index.ts +4 -0
  56. package/src/context/KeyboardContext.tsx +35 -0
  57. package/src/context/index.ts +1 -0
  58. package/src/hooks/index.ts +4 -0
  59. package/src/hooks/useFocusedInput.ts +52 -0
  60. package/src/hooks/useKeyboard.ts +1 -0
  61. package/src/hooks/useKeyboardAnimation.ts +50 -0
  62. package/src/hooks/useKeyboardState.ts +14 -0
  63. package/src/index.ts +35 -0
  64. package/src/types/index.ts +101 -0
  65. package/src/utils/constants.ts +2 -0
  66. package/src/utils/focusManager.ts +40 -0
  67. package/src/utils/index.ts +10 -0
  68. package/src/utils/keyboardEvents.ts +110 -0
  69. package/src/utils/platform.ts +23 -0
  70. package/src/utils/scrollManager.ts +71 -0
@@ -0,0 +1,172 @@
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import { Animated, StyleSheet } from 'react-native';
10
+ import type { KeyboardAwareViewProps } from '../types';
11
+ import type { KeyboardAwareViewRef } from '../types';
12
+ import { DEFAULT_KEYBOARD_SPACING } from '../utils/constants';
13
+ import { useKeyboardState } from '../hooks/useKeyboardState';
14
+ import {
15
+ calculateKeyboardDisplacement,
16
+ calculateKeyboardOverlap,
17
+ } from '../utils/scrollManager';
18
+
19
+ /** Adds keyboard spacing to in-flow content and keeps absolute footers visible. */
20
+ export const KeyboardAwareView: React.ForwardRefExoticComponent<
21
+ KeyboardAwareViewProps & React.RefAttributes<KeyboardAwareViewRef>
22
+ > = forwardRef<KeyboardAwareViewRef, KeyboardAwareViewProps>(
23
+ function KeyboardAwareViewComponent(
24
+ {
25
+ behavior = 'smart',
26
+ keyboardSpacing = DEFAULT_KEYBOARD_SPACING,
27
+ enabled = true,
28
+ style,
29
+ onLayout,
30
+ ...props
31
+ },
32
+ forwardedRef,
33
+ ): React.JSX.Element {
34
+ const keyboard = useKeyboardState();
35
+ const viewRef = useRef<KeyboardAwareViewRef>(null);
36
+ useImperativeHandle(forwardedRef, () => viewRef.current!, []);
37
+ const measurementSequence = useRef(0);
38
+ const [keyboardOverlap, setKeyboardOverlap] = useState(0);
39
+ const [keyboardDisplacement, setKeyboardDisplacement] = useState(0);
40
+ const animatedDisplacement = useRef(new Animated.Value(0)).current;
41
+ const flattenedStyle = StyleSheet.flatten(style);
42
+ const isAbsolutelyPositioned = flattenedStyle?.position === 'absolute';
43
+ const spacing = Number.isFinite(keyboardSpacing)
44
+ ? Math.max(0, keyboardSpacing)
45
+ : 0;
46
+ const basePadding =
47
+ typeof flattenedStyle?.paddingBottom === 'number'
48
+ ? flattenedStyle.paddingBottom
49
+ : 0;
50
+ const measureOverlap = useCallback(() => {
51
+ const view = viewRef.current;
52
+ if (!view?.measureInWindow || !keyboard.isVisible) {
53
+ return;
54
+ }
55
+
56
+ const sequence = ++measurementSequence.current;
57
+ view.measureInWindow((_x, y, _width, height) => {
58
+ if (sequence !== measurementSequence.current || !keyboard.isVisible) {
59
+ return;
60
+ }
61
+ if (!isAbsolutelyPositioned) {
62
+ setKeyboardOverlap(
63
+ calculateKeyboardOverlap(
64
+ { y, height },
65
+ keyboard.height,
66
+ keyboard.screenY,
67
+ ),
68
+ );
69
+ }
70
+ setKeyboardDisplacement(
71
+ isAbsolutelyPositioned && typeof keyboard.screenY === 'number'
72
+ ? calculateKeyboardDisplacement(
73
+ { y, height },
74
+ keyboard.screenY,
75
+ spacing,
76
+ )
77
+ : 0,
78
+ );
79
+ });
80
+ }, [
81
+ isAbsolutelyPositioned,
82
+ keyboard.height,
83
+ keyboard.isVisible,
84
+ keyboard.screenY,
85
+ spacing,
86
+ ]);
87
+
88
+ useEffect(() => {
89
+ if (!keyboard.isVisible) {
90
+ measurementSequence.current += 1;
91
+ setKeyboardOverlap(0);
92
+ setKeyboardDisplacement(0);
93
+ return;
94
+ }
95
+ const frame = requestAnimationFrame(measureOverlap);
96
+ return () => cancelAnimationFrame(frame);
97
+ }, [keyboard.isVisible, keyboard.height, keyboard.screenY, measureOverlap]);
98
+
99
+ const shouldAdjust =
100
+ enabled &&
101
+ keyboard.isVisible &&
102
+ behavior !== 'none' &&
103
+ behavior !== 'resize';
104
+ useEffect(() => {
105
+ if (!isAbsolutelyPositioned) {
106
+ animatedDisplacement.setValue(0);
107
+ return;
108
+ }
109
+ const animation = Animated.timing(animatedDisplacement, {
110
+ toValue: shouldAdjust ? -keyboardDisplacement : 0,
111
+ duration: keyboard.animationDuration,
112
+ useNativeDriver: true,
113
+ });
114
+ animation.start();
115
+ return () => animation.stop();
116
+ }, [
117
+ animatedDisplacement,
118
+ isAbsolutelyPositioned,
119
+ keyboard.animationDuration,
120
+ keyboardDisplacement,
121
+ shouldAdjust,
122
+ ]);
123
+
124
+ const handleLayout = useCallback(
125
+ (event: Parameters<NonNullable<typeof onLayout>>[0]) => {
126
+ onLayout?.(event);
127
+ measureOverlap();
128
+ },
129
+ [measureOverlap, onLayout],
130
+ );
131
+ const adjustPadding =
132
+ shouldAdjust &&
133
+ !isAbsolutelyPositioned &&
134
+ keyboardOverlap > 0 &&
135
+ typeof flattenedStyle?.paddingBottom !== 'string';
136
+ const stickyTransform = isAbsolutelyPositioned
137
+ ? [
138
+ ...(Array.isArray(flattenedStyle?.transform)
139
+ ? flattenedStyle.transform
140
+ : []),
141
+ { translateY: animatedDisplacement },
142
+ ]
143
+ : undefined;
144
+
145
+ return (
146
+ <Animated.View
147
+ {...props}
148
+ ref={viewRef}
149
+ onLayout={handleLayout}
150
+ style={
151
+ adjustPadding
152
+ ? [
153
+ style,
154
+ {
155
+ paddingBottom: Math.max(
156
+ basePadding,
157
+ keyboardOverlap + spacing,
158
+ ),
159
+ },
160
+ ]
161
+ : isAbsolutelyPositioned
162
+ ? [style, { transform: stickyTransform }]
163
+ : style
164
+ }
165
+ />
166
+ );
167
+ },
168
+ );
169
+
170
+ KeyboardAwareView.displayName = 'KeyboardAwareView';
171
+
172
+ export default KeyboardAwareView;
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { Keyboard, Pressable } from 'react-native';
3
+ import type { KeyboardDismissButtonProps } from '../types';
4
+
5
+ /** A pressable that dismisses the software keyboard by default. */
6
+ export function KeyboardDismissButton({
7
+ children,
8
+ onPress,
9
+ dismissKeyboard = true,
10
+ accessibilityLabel = 'Dismiss keyboard',
11
+ ...props
12
+ }: KeyboardDismissButtonProps): React.JSX.Element {
13
+ const handlePress = () => {
14
+ onPress?.();
15
+ if (dismissKeyboard) {
16
+ Keyboard.dismiss();
17
+ }
18
+ };
19
+
20
+ return (
21
+ <Pressable
22
+ {...props}
23
+ accessibilityLabel={accessibilityLabel}
24
+ accessibilityRole="button"
25
+ onPress={handlePress}
26
+ >
27
+ {children}
28
+ </Pressable>
29
+ );
30
+ }
31
+
32
+ export default KeyboardDismissButton;
@@ -0,0 +1,200 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Keyboard,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ TextInput,
8
+ View,
9
+ } from 'react-native';
10
+ import type { KeyboardToolbarProps } from '../types';
11
+ import { useKeyboardState } from '../hooks/useKeyboardState';
12
+
13
+ function findFocusedInputIndex(
14
+ inputRefs: NonNullable<KeyboardToolbarProps['inputRefs']>,
15
+ ): number {
16
+ return inputRefs.findIndex(ref => ref.current?.isFocused() === true);
17
+ }
18
+
19
+ function isMountedInputAt(
20
+ inputRefs: NonNullable<KeyboardToolbarProps['inputRefs']>,
21
+ index: number,
22
+ ): boolean {
23
+ return (
24
+ index >= 0 && index < inputRefs.length && inputRefs[index]?.current != null
25
+ );
26
+ }
27
+
28
+ function hasMountedInputBefore(
29
+ inputRefs: NonNullable<KeyboardToolbarProps['inputRefs']>,
30
+ index: number,
31
+ ): boolean {
32
+ return inputRefs.some(
33
+ (input, inputIndex) => inputIndex < index && input.current != null,
34
+ );
35
+ }
36
+
37
+ function hasMountedInputAfter(
38
+ inputRefs: NonNullable<KeyboardToolbarProps['inputRefs']>,
39
+ index: number,
40
+ ): boolean {
41
+ return inputRefs.some(
42
+ (input, inputIndex) => inputIndex > index && input.current != null,
43
+ );
44
+ }
45
+
46
+ /** An optional in-layout toolbar for input navigation and keyboard dismissal. */
47
+ export function KeyboardToolbar({
48
+ inputRefs = [],
49
+ focusedInputIndex,
50
+ onFocusedInputChange,
51
+ onDone,
52
+ doneText = 'Done',
53
+ children,
54
+ alwaysVisible = false,
55
+ style,
56
+ ...props
57
+ }: KeyboardToolbarProps): React.JSX.Element | null {
58
+ const keyboard = useKeyboardState();
59
+ const [trackedIndex, setTrackedIndex] = useState(-1);
60
+ const lastReportedIndex = useRef(-1);
61
+ const trackFocus = useCallback(() => {
62
+ const index = findFocusedInputIndex(inputRefs);
63
+ setTrackedIndex(current => (current === index ? current : index));
64
+ if (lastReportedIndex.current !== index) {
65
+ lastReportedIndex.current = index;
66
+ onFocusedInputChange?.(index);
67
+ }
68
+ }, [inputRefs, onFocusedInputChange]);
69
+
70
+ useEffect(() => {
71
+ trackFocus();
72
+ if (!keyboard.isVisible && !alwaysVisible) {
73
+ return;
74
+ }
75
+
76
+ const interval = setInterval(trackFocus, 100);
77
+ return () => clearInterval(interval);
78
+ }, [alwaysVisible, keyboard.isVisible, trackFocus]);
79
+
80
+ if (!alwaysVisible && !keyboard.isVisible) {
81
+ return null;
82
+ }
83
+
84
+ const actualFocusedIndex = findFocusedInputIndex(inputRefs);
85
+ const currentIndex =
86
+ actualFocusedIndex >= 0
87
+ ? actualFocusedIndex
88
+ : isMountedInputAt(inputRefs, trackedIndex)
89
+ ? trackedIndex
90
+ : isMountedInputAt(inputRefs, focusedInputIndex ?? -1)
91
+ ? focusedInputIndex!
92
+ : -1;
93
+ const canNavigateBackward =
94
+ currentIndex >= 0 && hasMountedInputBefore(inputRefs, currentIndex);
95
+ const canNavigateForward =
96
+ currentIndex >= 0 && hasMountedInputAfter(inputRefs, currentIndex);
97
+
98
+ const focusInDirection = (direction: -1 | 1) => {
99
+ const actualIndex = findFocusedInputIndex(inputRefs);
100
+ const origin = actualIndex >= 0 ? actualIndex : currentIndex;
101
+ if (origin < 0) {
102
+ return;
103
+ }
104
+
105
+ for (
106
+ let index = origin + direction;
107
+ index >= 0 && index < inputRefs.length;
108
+ index += direction
109
+ ) {
110
+ const target = inputRefs[index]?.current;
111
+ if (target) {
112
+ target.focus();
113
+ lastReportedIndex.current = index;
114
+ setTrackedIndex(index);
115
+ onFocusedInputChange?.(index);
116
+ return;
117
+ }
118
+ }
119
+ };
120
+
121
+ const done = () => {
122
+ const focusedInput = TextInput.State.currentlyFocusedInput?.();
123
+ if (focusedInput) {
124
+ TextInput.State.blurTextInput(focusedInput);
125
+ }
126
+ Keyboard.dismiss();
127
+ onDone?.();
128
+ };
129
+
130
+ return (
131
+ <View {...props} style={[styles.toolbar, style]}>
132
+ {children ?? (
133
+ <>
134
+ <Pressable
135
+ accessibilityRole="button"
136
+ accessibilityLabel="Previous input"
137
+ disabled={!canNavigateBackward}
138
+ onPress={() => focusInDirection(-1)}
139
+ style={styles.action}
140
+ >
141
+ <Text style={canNavigateBackward ? styles.label : styles.disabled}>
142
+ Previous
143
+ </Text>
144
+ </Pressable>
145
+ <Pressable
146
+ accessibilityRole="button"
147
+ accessibilityLabel="Next input"
148
+ disabled={!canNavigateForward}
149
+ onPress={() => focusInDirection(1)}
150
+ style={styles.action}
151
+ >
152
+ <Text style={canNavigateForward ? styles.label : styles.disabled}>
153
+ Next
154
+ </Text>
155
+ </Pressable>
156
+ <View style={styles.spacer} />
157
+ <Pressable
158
+ accessibilityRole="button"
159
+ accessibilityLabel="Done"
160
+ onPress={done}
161
+ style={styles.action}
162
+ >
163
+ <Text style={styles.label}>{doneText}</Text>
164
+ </Pressable>
165
+ </>
166
+ )}
167
+ </View>
168
+ );
169
+ }
170
+
171
+ const styles = StyleSheet.create({
172
+ toolbar: {
173
+ alignItems: 'center',
174
+ backgroundColor: '#f6f7f9',
175
+ borderTopColor: '#d8dce2',
176
+ borderTopWidth: StyleSheet.hairlineWidth,
177
+ flexDirection: 'row',
178
+ minHeight: 48,
179
+ paddingHorizontal: 12,
180
+ },
181
+ action: {
182
+ minHeight: 44,
183
+ justifyContent: 'center',
184
+ paddingHorizontal: 10,
185
+ },
186
+ label: {
187
+ color: '#155eef',
188
+ fontSize: 15,
189
+ fontWeight: '600',
190
+ },
191
+ disabled: {
192
+ color: '#98a2b3',
193
+ fontSize: 15,
194
+ },
195
+ spacer: {
196
+ flex: 1,
197
+ },
198
+ });
199
+
200
+ export default KeyboardToolbar;
@@ -0,0 +1,4 @@
1
+ export { KeyboardAwareScrollView } from './KeyboardAwareScrollView';
2
+ export { KeyboardAwareView } from './KeyboardAwareView';
3
+ export { KeyboardToolbar } from './KeyboardToolbar';
4
+ export { KeyboardDismissButton } from './KeyboardDismissButton';
@@ -0,0 +1,35 @@
1
+ import React, { createContext, useContext, useEffect, useRef } from 'react';
2
+ import { useKeyboardState } from '../hooks/useKeyboardState';
3
+ import type { KeyboardProviderProps } from '../types';
4
+
5
+ const DebugContext = createContext(false);
6
+
7
+ /** Optional provider for development diagnostics; keyboard subscriptions are shared globally. */
8
+ export function KeyboardProvider({
9
+ children,
10
+ debug = false,
11
+ }: KeyboardProviderProps): React.JSX.Element {
12
+ const keyboard = useKeyboardState();
13
+ const previous = useRef(keyboard);
14
+
15
+ useEffect(() => {
16
+ if (
17
+ debug &&
18
+ __DEV__ &&
19
+ (previous.current.isVisible !== keyboard.isVisible ||
20
+ previous.current.height !== keyboard.height)
21
+ ) {
22
+ console.debug('[keyboard-toolkit]', keyboard);
23
+ }
24
+ previous.current = keyboard;
25
+ }, [debug, keyboard]);
26
+
27
+ return (
28
+ <DebugContext.Provider value={debug}>{children}</DebugContext.Provider>
29
+ );
30
+ }
31
+
32
+ /** Read whether development diagnostics are enabled on the nearest provider. */
33
+ export function useKeyboardDebug(): boolean {
34
+ return useContext(DebugContext);
35
+ }
@@ -0,0 +1 @@
1
+ export { KeyboardProvider } from './KeyboardContext';
@@ -0,0 +1,4 @@
1
+ export { useKeyboard } from './useKeyboard';
2
+ export { useKeyboardState } from './useKeyboardState';
3
+ export { useKeyboardAnimation } from './useKeyboardAnimation';
4
+ export { useFocusedInput } from './useFocusedInput';
@@ -0,0 +1,52 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import type { FocusedInputLayout } from '../types';
3
+ import { getFocusedInput, measureFocusedInput } from '../utils/focusManager';
4
+ import { useKeyboardState } from './useKeyboardState';
5
+
6
+ /** Measure the focused TextInput when the keyboard opens; call `refresh` after layout changes. */
7
+ export function useFocusedInput(): {
8
+ focusedInput: FocusedInputLayout | null;
9
+ refresh: () => Promise<FocusedInputLayout | null>;
10
+ } {
11
+ const keyboard = useKeyboardState();
12
+ const [focusedInput, setFocusedInput] = useState<FocusedInputLayout | null>(
13
+ null,
14
+ );
15
+ const measureSequence = useRef(0);
16
+
17
+ const refresh = useCallback(async () => {
18
+ const sequence = ++measureSequence.current;
19
+ const layout = await measureFocusedInput(getFocusedInput());
20
+ if (sequence === measureSequence.current) {
21
+ setFocusedInput(current => {
22
+ if (
23
+ current?.x === layout?.x &&
24
+ current?.y === layout?.y &&
25
+ current?.width === layout?.width &&
26
+ current?.height === layout?.height
27
+ ) {
28
+ return current;
29
+ }
30
+ return layout;
31
+ });
32
+ }
33
+ return layout;
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ if (!keyboard.isVisible) {
38
+ measureSequence.current += 1;
39
+ setFocusedInput(null);
40
+ return;
41
+ }
42
+
43
+ refresh();
44
+ const interval = setInterval(refresh, 120);
45
+ return () => {
46
+ measureSequence.current += 1;
47
+ clearInterval(interval);
48
+ };
49
+ }, [keyboard.isVisible, keyboard.height, refresh]);
50
+
51
+ return { focusedInput, refresh };
52
+ }
@@ -0,0 +1 @@
1
+ export { useKeyboardState as useKeyboard } from './useKeyboardState';
@@ -0,0 +1,50 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { AccessibilityInfo, Animated } from 'react-native';
3
+ import { useKeyboardState } from './useKeyboardState';
4
+
5
+ /** Provides an animated 0-to-1 visibility value and the native event duration. */
6
+ export function useKeyboardAnimation(): {
7
+ progress: Animated.Value;
8
+ animationDuration: number;
9
+ } {
10
+ const keyboard = useKeyboardState();
11
+ const progressRef = useRef<Animated.Value | null>(null);
12
+ if (!progressRef.current) {
13
+ progressRef.current = new Animated.Value(keyboard.progress);
14
+ }
15
+ const progress = progressRef.current;
16
+ const [reduceMotion, setReduceMotion] = useState(false);
17
+
18
+ useEffect(() => {
19
+ let active = true;
20
+ AccessibilityInfo.isReduceMotionEnabled()
21
+ .then(enabled => {
22
+ if (active) {
23
+ setReduceMotion(enabled);
24
+ }
25
+ })
26
+ .catch(() => {
27
+ // Keep animations enabled when the platform preference cannot be read.
28
+ });
29
+ const subscription = AccessibilityInfo.addEventListener(
30
+ 'reduceMotionChanged',
31
+ setReduceMotion,
32
+ );
33
+ return () => {
34
+ active = false;
35
+ subscription.remove();
36
+ };
37
+ }, []);
38
+
39
+ useEffect(() => {
40
+ const animation = Animated.timing(progress, {
41
+ toValue: keyboard.progress,
42
+ duration: reduceMotion ? 0 : keyboard.animationDuration,
43
+ useNativeDriver: true,
44
+ });
45
+ animation.start();
46
+ return () => animation.stop();
47
+ }, [keyboard.animationDuration, keyboard.progress, progress, reduceMotion]);
48
+
49
+ return { progress, animationDuration: keyboard.animationDuration };
50
+ }
@@ -0,0 +1,14 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import {
3
+ getKeyboardState,
4
+ subscribeKeyboardState,
5
+ } from '../utils/keyboardEvents';
6
+
7
+ /** Subscribe to the shared, normalized keyboard state. A provider is optional. */
8
+ export function useKeyboardState() {
9
+ return useSyncExternalStore(
10
+ subscribeKeyboardState,
11
+ getKeyboardState,
12
+ getKeyboardState,
13
+ );
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ export {
2
+ KeyboardAwareScrollView,
3
+ KeyboardAwareView,
4
+ KeyboardDismissButton,
5
+ KeyboardToolbar,
6
+ } from './components';
7
+ export { KeyboardProvider } from './context';
8
+ export {
9
+ useFocusedInput,
10
+ useKeyboard,
11
+ useKeyboardAnimation,
12
+ useKeyboardState,
13
+ } from './hooks';
14
+ export {
15
+ calculateScrollOffset,
16
+ calculateKeyboardOverlap,
17
+ focusInput,
18
+ getFocusedInput,
19
+ isAndroid,
20
+ isIOS,
21
+ measureFocusedInput,
22
+ } from './utils';
23
+ export type {
24
+ FocusedInputLayout,
25
+ KeyboardAwareScrollViewProps,
26
+ KeyboardAwareScrollViewRef,
27
+ KeyboardAwareViewProps,
28
+ KeyboardAwareViewRef,
29
+ KeyboardBehavior,
30
+ KeyboardDismissButtonProps,
31
+ KeyboardProviderProps,
32
+ KeyboardState,
33
+ KeyboardToolbarProps,
34
+ TextInputRef,
35
+ } from './types';