react-native-keyboard-toolkit 0.0.0-stage → 1.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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +1014 -2
- package/dist/components/KeyboardAwareScrollView.d.ts +6 -0
- package/dist/components/KeyboardAwareScrollView.js +137 -0
- package/dist/components/KeyboardAwareView.d.ts +6 -0
- package/dist/components/KeyboardAwareView.js +63 -0
- package/dist/components/KeyboardDismissButton.d.ts +5 -0
- package/dist/components/KeyboardDismissButton.js +16 -0
- package/dist/components/KeyboardToolbar.d.ts +5 -0
- package/dist/components/KeyboardToolbar.js +109 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +11 -0
- package/dist/context/KeyboardContext.d.ts +6 -0
- package/dist/context/KeyboardContext.js +27 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +5 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +11 -0
- package/dist/hooks/useFocusedInput.d.ts +6 -0
- package/dist/hooks/useFocusedInput.js +42 -0
- package/dist/hooks/useKeyboard.d.ts +1 -0
- package/dist/hooks/useKeyboard.js +5 -0
- package/dist/hooks/useKeyboardAnimation.d.ts +6 -0
- package/dist/hooks/useKeyboardAnimation.js +43 -0
- package/dist/hooks/useKeyboardState.d.ts +2 -0
- package/dist/hooks/useKeyboardState.js +9 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +23 -0
- package/dist/types/index.d.ts +69 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/constants.d.ts +2 -0
- package/dist/utils/constants.js +5 -0
- package/dist/utils/focusManager.d.ts +10 -0
- package/dist/utils/focusManager.js +34 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +13 -0
- package/dist/utils/keyboardEvents.d.ts +7 -0
- package/dist/utils/keyboardEvents.js +88 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +19 -0
- package/dist/utils/scrollManager.d.ts +19 -0
- package/dist/utils/scrollManager.js +32 -0
- package/docs/android.md +26 -0
- package/docs/api.md +75 -0
- package/docs/assets/demo-preview.svg +24 -0
- package/docs/installation.md +22 -0
- package/docs/ios.md +22 -0
- package/docs/troubleshooting.md +36 -0
- package/package.json +90 -4
- package/src/components/KeyboardAwareScrollView.tsx +240 -0
- package/src/components/KeyboardAwareView.tsx +112 -0
- package/src/components/KeyboardDismissButton.tsx +32 -0
- package/src/components/KeyboardToolbar.tsx +200 -0
- package/src/components/index.ts +4 -0
- package/src/context/KeyboardContext.tsx +35 -0
- package/src/context/index.ts +1 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useFocusedInput.ts +52 -0
- package/src/hooks/useKeyboard.ts +1 -0
- package/src/hooks/useKeyboardAnimation.ts +50 -0
- package/src/hooks/useKeyboardState.ts +14 -0
- package/src/index.ts +35 -0
- package/src/types/index.ts +101 -0
- package/src/utils/constants.ts +2 -0
- package/src/utils/focusManager.ts +40 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/keyboardEvents.ts +110 -0
- package/src/utils/platform.ts +23 -0
- package/src/utils/scrollManager.ts +59 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { KeyboardAwareScrollViewProps } from '../types';
|
|
3
|
+
import type { KeyboardAwareScrollViewRef } from '../types';
|
|
4
|
+
/** A ScrollView that adds keyboard space and scrolls a covered focused input into view. */
|
|
5
|
+
export declare const KeyboardAwareScrollView: React.ForwardRefExoticComponent<KeyboardAwareScrollViewProps & React.RefAttributes<KeyboardAwareScrollViewRef>>;
|
|
6
|
+
export default KeyboardAwareScrollView;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyboardAwareScrollView = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_native_1 = require("react-native");
|
|
7
|
+
const focusManager_1 = require("../utils/focusManager");
|
|
8
|
+
const scrollManager_1 = require("../utils/scrollManager");
|
|
9
|
+
const KeyboardContext_1 = require("../context/KeyboardContext");
|
|
10
|
+
const useKeyboardState_1 = require("../hooks/useKeyboardState");
|
|
11
|
+
const constants_1 = require("../utils/constants");
|
|
12
|
+
const platform_1 = require("../utils/platform");
|
|
13
|
+
/** A ScrollView that adds keyboard space and scrolls a covered focused input into view. */
|
|
14
|
+
exports.KeyboardAwareScrollView = (0, react_1.forwardRef)(function KeyboardAwareScrollViewComponent({ behavior = 'smart', keyboardSpacing = constants_1.DEFAULT_KEYBOARD_SPACING, enabled = true, contentContainerStyle, onScroll, onScrollBeginDrag, onContentSizeChange, onLayout, onFocusCapture, scrollEventThrottle = 16, ...props }, forwardedRef) {
|
|
15
|
+
const keyboard = (0, useKeyboardState_1.useKeyboardState)();
|
|
16
|
+
const debug = (0, KeyboardContext_1.useKeyboardDebug)();
|
|
17
|
+
const scrollRef = (0, react_1.useRef)(null);
|
|
18
|
+
(0, react_1.useImperativeHandle)(forwardedRef, () => scrollRef.current, []);
|
|
19
|
+
const scrollOffset = (0, react_1.useRef)(0);
|
|
20
|
+
const lastScrollTarget = (0, react_1.useRef)(null);
|
|
21
|
+
const measurementSequence = (0, react_1.useRef)(0);
|
|
22
|
+
const [keyboardOverlap, setKeyboardOverlap] = (0, react_1.useState)(0);
|
|
23
|
+
const spacing = Math.max(0, keyboardSpacing);
|
|
24
|
+
const scrollFocusedInput = (0, react_1.useCallback)(async () => {
|
|
25
|
+
const sequence = ++measurementSequence.current;
|
|
26
|
+
if (!enabled || !keyboard.isVisible || behavior === 'resize') {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const scrollView = scrollRef.current;
|
|
30
|
+
const input = (0, focusManager_1.getFocusedInput)();
|
|
31
|
+
if (!scrollView?.measureInWindow) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const viewport = await new Promise(resolve => {
|
|
35
|
+
scrollView.measureInWindow((_x, y, _width, height) => resolve({ y, height }));
|
|
36
|
+
});
|
|
37
|
+
if (sequence !== measurementSequence.current || !keyboard.isVisible) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const overlap = (0, scrollManager_1.calculateKeyboardOverlap)(viewport, keyboard.height, keyboard.screenY);
|
|
41
|
+
if (Math.abs(overlap - keyboardOverlap) > 1) {
|
|
42
|
+
setKeyboardOverlap(overlap);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!input?.measureInWindow ||
|
|
46
|
+
behavior === 'none' ||
|
|
47
|
+
behavior === 'padding') {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const inputLayout = await (0, focusManager_1.measureFocusedInput)(input);
|
|
51
|
+
if (sequence !== measurementSequence.current || !inputLayout) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const calculation = (0, scrollManager_1.calculateScrollOffset)({ y: inputLayout.y, height: inputLayout.height }, viewport, keyboard.height, spacing, scrollOffset.current, keyboard.screenY);
|
|
55
|
+
if (debug && __DEV__) {
|
|
56
|
+
console.debug('[keyboard-toolkit] scroll calculation', calculation);
|
|
57
|
+
}
|
|
58
|
+
if (Math.abs(calculation.delta) > 1 &&
|
|
59
|
+
(lastScrollTarget.current === null ||
|
|
60
|
+
Math.abs(lastScrollTarget.current - calculation.offset) > 1)) {
|
|
61
|
+
lastScrollTarget.current = calculation.offset;
|
|
62
|
+
scrollView.scrollTo({ y: calculation.offset, animated: true });
|
|
63
|
+
}
|
|
64
|
+
}, [
|
|
65
|
+
behavior,
|
|
66
|
+
debug,
|
|
67
|
+
enabled,
|
|
68
|
+
keyboard.height,
|
|
69
|
+
keyboard.isVisible,
|
|
70
|
+
keyboard.screenY,
|
|
71
|
+
keyboardOverlap,
|
|
72
|
+
spacing,
|
|
73
|
+
]);
|
|
74
|
+
(0, react_1.useEffect)(() => {
|
|
75
|
+
if (!keyboard.isVisible) {
|
|
76
|
+
lastScrollTarget.current = null;
|
|
77
|
+
measurementSequence.current += 1;
|
|
78
|
+
setKeyboardOverlap(0);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const frame = requestAnimationFrame(() => {
|
|
82
|
+
scrollFocusedInput();
|
|
83
|
+
});
|
|
84
|
+
return () => cancelAnimationFrame(frame);
|
|
85
|
+
}, [keyboard.height, keyboard.isVisible, scrollFocusedInput]);
|
|
86
|
+
const handleScroll = (0, react_1.useCallback)((event) => {
|
|
87
|
+
scrollOffset.current = event.nativeEvent.contentOffset.y;
|
|
88
|
+
if (lastScrollTarget.current !== null &&
|
|
89
|
+
Math.abs(scrollOffset.current - lastScrollTarget.current) <= 1) {
|
|
90
|
+
lastScrollTarget.current = null;
|
|
91
|
+
}
|
|
92
|
+
onScroll?.(event);
|
|
93
|
+
}, [onScroll]);
|
|
94
|
+
const handleScrollBeginDrag = (0, react_1.useCallback)((event) => {
|
|
95
|
+
lastScrollTarget.current = null;
|
|
96
|
+
onScrollBeginDrag?.(event);
|
|
97
|
+
}, [onScrollBeginDrag]);
|
|
98
|
+
const handleContentSizeChange = (0, react_1.useCallback)((width, height) => {
|
|
99
|
+
onContentSizeChange?.(width, height);
|
|
100
|
+
if (keyboard.isVisible) {
|
|
101
|
+
requestAnimationFrame(() => scrollFocusedInput());
|
|
102
|
+
}
|
|
103
|
+
}, [keyboard.isVisible, onContentSizeChange, scrollFocusedInput]);
|
|
104
|
+
const handleLayout = (0, react_1.useCallback)((event) => {
|
|
105
|
+
onLayout?.(event);
|
|
106
|
+
if (keyboard.isVisible) {
|
|
107
|
+
requestAnimationFrame(() => scrollFocusedInput());
|
|
108
|
+
}
|
|
109
|
+
}, [keyboard.isVisible, onLayout, scrollFocusedInput]);
|
|
110
|
+
const handleFocusCapture = (0, react_1.useCallback)((event) => {
|
|
111
|
+
onFocusCapture?.(event);
|
|
112
|
+
if (keyboard.isVisible) {
|
|
113
|
+
requestAnimationFrame(() => scrollFocusedInput());
|
|
114
|
+
}
|
|
115
|
+
}, [keyboard.isVisible, onFocusCapture, scrollFocusedInput]);
|
|
116
|
+
const flattenedContentStyle = react_native_1.StyleSheet.flatten(contentContainerStyle);
|
|
117
|
+
const existingBottomPadding = typeof flattenedContentStyle?.paddingBottom === 'number'
|
|
118
|
+
? flattenedContentStyle.paddingBottom
|
|
119
|
+
: 0;
|
|
120
|
+
const shouldPad = enabled &&
|
|
121
|
+
keyboard.isVisible &&
|
|
122
|
+
behavior !== 'none' &&
|
|
123
|
+
behavior !== 'resize' &&
|
|
124
|
+
keyboardOverlap > 0;
|
|
125
|
+
const adjustedContentStyle = shouldPad && typeof flattenedContentStyle?.paddingBottom !== 'string'
|
|
126
|
+
? [
|
|
127
|
+
contentContainerStyle,
|
|
128
|
+
{
|
|
129
|
+
paddingBottom: Math.max(existingBottomPadding, keyboardOverlap + spacing),
|
|
130
|
+
},
|
|
131
|
+
]
|
|
132
|
+
: contentContainerStyle;
|
|
133
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { ...props, automaticallyAdjustKeyboardInsets: props.automaticallyAdjustKeyboardInsets ??
|
|
134
|
+
(behavior === 'resize' && platform_1.isIOS), ref: scrollRef, contentContainerStyle: adjustedContentStyle, onContentSizeChange: handleContentSizeChange, onLayout: handleLayout, onFocusCapture: handleFocusCapture, onScroll: handleScroll, onScrollBeginDrag: handleScrollBeginDrag, scrollEventThrottle: scrollEventThrottle }));
|
|
135
|
+
});
|
|
136
|
+
exports.KeyboardAwareScrollView.displayName = 'KeyboardAwareScrollView';
|
|
137
|
+
exports.default = exports.KeyboardAwareScrollView;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { KeyboardAwareViewProps } from '../types';
|
|
3
|
+
import type { KeyboardAwareViewRef } from '../types';
|
|
4
|
+
/** A lightweight view that adds bottom space while the keyboard is visible. */
|
|
5
|
+
export declare const KeyboardAwareView: React.ForwardRefExoticComponent<KeyboardAwareViewProps & React.RefAttributes<KeyboardAwareViewRef>>;
|
|
6
|
+
export default KeyboardAwareView;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyboardAwareView = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_native_1 = require("react-native");
|
|
7
|
+
const constants_1 = require("../utils/constants");
|
|
8
|
+
const useKeyboardState_1 = require("../hooks/useKeyboardState");
|
|
9
|
+
const scrollManager_1 = require("../utils/scrollManager");
|
|
10
|
+
/** A lightweight view that adds bottom space while the keyboard is visible. */
|
|
11
|
+
exports.KeyboardAwareView = (0, react_1.forwardRef)(function KeyboardAwareViewComponent({ behavior = 'smart', keyboardSpacing = constants_1.DEFAULT_KEYBOARD_SPACING, enabled = true, style, onLayout, ...props }, forwardedRef) {
|
|
12
|
+
const keyboard = (0, useKeyboardState_1.useKeyboardState)();
|
|
13
|
+
const viewRef = (0, react_1.useRef)(null);
|
|
14
|
+
(0, react_1.useImperativeHandle)(forwardedRef, () => viewRef.current, []);
|
|
15
|
+
const measurementSequence = (0, react_1.useRef)(0);
|
|
16
|
+
const [keyboardOverlap, setKeyboardOverlap] = (0, react_1.useState)(0);
|
|
17
|
+
const flattenedStyle = react_native_1.StyleSheet.flatten(style);
|
|
18
|
+
const basePadding = typeof flattenedStyle?.paddingBottom === 'number'
|
|
19
|
+
? flattenedStyle.paddingBottom
|
|
20
|
+
: 0;
|
|
21
|
+
const measureOverlap = (0, react_1.useCallback)(() => {
|
|
22
|
+
const view = viewRef.current;
|
|
23
|
+
if (!view?.measureInWindow || !keyboard.isVisible) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const sequence = ++measurementSequence.current;
|
|
27
|
+
view.measureInWindow((_x, y, _width, height) => {
|
|
28
|
+
if (sequence !== measurementSequence.current || !keyboard.isVisible) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
setKeyboardOverlap((0, scrollManager_1.calculateKeyboardOverlap)({ y, height }, keyboard.height, keyboard.screenY));
|
|
32
|
+
});
|
|
33
|
+
}, [keyboard.height, keyboard.isVisible, keyboard.screenY]);
|
|
34
|
+
(0, react_1.useEffect)(() => {
|
|
35
|
+
if (!keyboard.isVisible) {
|
|
36
|
+
measurementSequence.current += 1;
|
|
37
|
+
setKeyboardOverlap(0);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const frame = requestAnimationFrame(measureOverlap);
|
|
41
|
+
return () => cancelAnimationFrame(frame);
|
|
42
|
+
}, [keyboard.isVisible, keyboard.height, keyboard.screenY, measureOverlap]);
|
|
43
|
+
const handleLayout = (0, react_1.useCallback)((event) => {
|
|
44
|
+
onLayout?.(event);
|
|
45
|
+
measureOverlap();
|
|
46
|
+
}, [measureOverlap, onLayout]);
|
|
47
|
+
const adjust = enabled &&
|
|
48
|
+
keyboard.isVisible &&
|
|
49
|
+
behavior !== 'none' &&
|
|
50
|
+
behavior !== 'resize' &&
|
|
51
|
+
keyboardOverlap > 0 &&
|
|
52
|
+
typeof flattenedStyle?.paddingBottom !== 'string';
|
|
53
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { ...props, ref: viewRef, onLayout: handleLayout, style: adjust
|
|
54
|
+
? [
|
|
55
|
+
style,
|
|
56
|
+
{
|
|
57
|
+
paddingBottom: Math.max(basePadding, keyboardOverlap + Math.max(0, keyboardSpacing)),
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
: style }));
|
|
61
|
+
});
|
|
62
|
+
exports.KeyboardAwareView.displayName = 'KeyboardAwareView';
|
|
63
|
+
exports.default = exports.KeyboardAwareView;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { KeyboardDismissButtonProps } from '../types';
|
|
3
|
+
/** A pressable that dismisses the software keyboard by default. */
|
|
4
|
+
export declare function KeyboardDismissButton({ children, onPress, dismissKeyboard, accessibilityLabel, ...props }: KeyboardDismissButtonProps): React.JSX.Element;
|
|
5
|
+
export default KeyboardDismissButton;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyboardDismissButton = KeyboardDismissButton;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
/** A pressable that dismisses the software keyboard by default. */
|
|
7
|
+
function KeyboardDismissButton({ children, onPress, dismissKeyboard = true, accessibilityLabel = 'Dismiss keyboard', ...props }) {
|
|
8
|
+
const handlePress = () => {
|
|
9
|
+
onPress?.();
|
|
10
|
+
if (dismissKeyboard) {
|
|
11
|
+
react_native_1.Keyboard.dismiss();
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { ...props, accessibilityLabel: accessibilityLabel, accessibilityRole: "button", onPress: handlePress, children: children }));
|
|
15
|
+
}
|
|
16
|
+
exports.default = KeyboardDismissButton;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { KeyboardToolbarProps } from '../types';
|
|
3
|
+
/** An optional in-layout toolbar for input navigation and keyboard dismissal. */
|
|
4
|
+
export declare function KeyboardToolbar({ inputRefs, focusedInputIndex, onFocusedInputChange, onDone, doneText, children, alwaysVisible, style, ...props }: KeyboardToolbarProps): React.JSX.Element | null;
|
|
5
|
+
export default KeyboardToolbar;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyboardToolbar = KeyboardToolbar;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_native_1 = require("react-native");
|
|
7
|
+
const useKeyboardState_1 = require("../hooks/useKeyboardState");
|
|
8
|
+
function findFocusedInputIndex(inputRefs) {
|
|
9
|
+
return inputRefs.findIndex(ref => ref.current?.isFocused() === true);
|
|
10
|
+
}
|
|
11
|
+
function isMountedInputAt(inputRefs, index) {
|
|
12
|
+
return (index >= 0 && index < inputRefs.length && inputRefs[index]?.current != null);
|
|
13
|
+
}
|
|
14
|
+
function hasMountedInputBefore(inputRefs, index) {
|
|
15
|
+
return inputRefs.some((input, inputIndex) => inputIndex < index && input.current != null);
|
|
16
|
+
}
|
|
17
|
+
function hasMountedInputAfter(inputRefs, index) {
|
|
18
|
+
return inputRefs.some((input, inputIndex) => inputIndex > index && input.current != null);
|
|
19
|
+
}
|
|
20
|
+
/** An optional in-layout toolbar for input navigation and keyboard dismissal. */
|
|
21
|
+
function KeyboardToolbar({ inputRefs = [], focusedInputIndex, onFocusedInputChange, onDone, doneText = 'Done', children, alwaysVisible = false, style, ...props }) {
|
|
22
|
+
const keyboard = (0, useKeyboardState_1.useKeyboardState)();
|
|
23
|
+
const [trackedIndex, setTrackedIndex] = (0, react_1.useState)(-1);
|
|
24
|
+
const lastReportedIndex = (0, react_1.useRef)(-1);
|
|
25
|
+
const trackFocus = (0, react_1.useCallback)(() => {
|
|
26
|
+
const index = findFocusedInputIndex(inputRefs);
|
|
27
|
+
setTrackedIndex(current => (current === index ? current : index));
|
|
28
|
+
if (lastReportedIndex.current !== index) {
|
|
29
|
+
lastReportedIndex.current = index;
|
|
30
|
+
onFocusedInputChange?.(index);
|
|
31
|
+
}
|
|
32
|
+
}, [inputRefs, onFocusedInputChange]);
|
|
33
|
+
(0, react_1.useEffect)(() => {
|
|
34
|
+
trackFocus();
|
|
35
|
+
if (!keyboard.isVisible && !alwaysVisible) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const interval = setInterval(trackFocus, 100);
|
|
39
|
+
return () => clearInterval(interval);
|
|
40
|
+
}, [alwaysVisible, keyboard.isVisible, trackFocus]);
|
|
41
|
+
if (!alwaysVisible && !keyboard.isVisible) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const actualFocusedIndex = findFocusedInputIndex(inputRefs);
|
|
45
|
+
const currentIndex = actualFocusedIndex >= 0
|
|
46
|
+
? actualFocusedIndex
|
|
47
|
+
: isMountedInputAt(inputRefs, trackedIndex)
|
|
48
|
+
? trackedIndex
|
|
49
|
+
: isMountedInputAt(inputRefs, focusedInputIndex ?? -1)
|
|
50
|
+
? focusedInputIndex
|
|
51
|
+
: -1;
|
|
52
|
+
const canNavigateBackward = currentIndex >= 0 && hasMountedInputBefore(inputRefs, currentIndex);
|
|
53
|
+
const canNavigateForward = currentIndex >= 0 && hasMountedInputAfter(inputRefs, currentIndex);
|
|
54
|
+
const focusInDirection = (direction) => {
|
|
55
|
+
const actualIndex = findFocusedInputIndex(inputRefs);
|
|
56
|
+
const origin = actualIndex >= 0 ? actualIndex : currentIndex;
|
|
57
|
+
if (origin < 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (let index = origin + direction; index >= 0 && index < inputRefs.length; index += direction) {
|
|
61
|
+
const target = inputRefs[index]?.current;
|
|
62
|
+
if (target) {
|
|
63
|
+
target.focus();
|
|
64
|
+
lastReportedIndex.current = index;
|
|
65
|
+
setTrackedIndex(index);
|
|
66
|
+
onFocusedInputChange?.(index);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const done = () => {
|
|
72
|
+
const focusedInput = react_native_1.TextInput.State.currentlyFocusedInput?.();
|
|
73
|
+
if (focusedInput) {
|
|
74
|
+
react_native_1.TextInput.State.blurTextInput(focusedInput);
|
|
75
|
+
}
|
|
76
|
+
react_native_1.Keyboard.dismiss();
|
|
77
|
+
onDone?.();
|
|
78
|
+
};
|
|
79
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { ...props, style: [styles.toolbar, style], children: children ?? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: "Previous input", disabled: !canNavigateBackward, onPress: () => focusInDirection(-1), style: styles.action, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: canNavigateBackward ? styles.label : styles.disabled, children: "Previous" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: "Next input", disabled: !canNavigateForward, onPress: () => focusInDirection(1), style: styles.action, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: canNavigateForward ? styles.label : styles.disabled, children: "Next" }) }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.spacer }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: "Done", onPress: done, style: styles.action, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: doneText }) })] })) }));
|
|
80
|
+
}
|
|
81
|
+
const styles = react_native_1.StyleSheet.create({
|
|
82
|
+
toolbar: {
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
backgroundColor: '#f6f7f9',
|
|
85
|
+
borderTopColor: '#d8dce2',
|
|
86
|
+
borderTopWidth: react_native_1.StyleSheet.hairlineWidth,
|
|
87
|
+
flexDirection: 'row',
|
|
88
|
+
minHeight: 48,
|
|
89
|
+
paddingHorizontal: 12,
|
|
90
|
+
},
|
|
91
|
+
action: {
|
|
92
|
+
minHeight: 44,
|
|
93
|
+
justifyContent: 'center',
|
|
94
|
+
paddingHorizontal: 10,
|
|
95
|
+
},
|
|
96
|
+
label: {
|
|
97
|
+
color: '#155eef',
|
|
98
|
+
fontSize: 15,
|
|
99
|
+
fontWeight: '600',
|
|
100
|
+
},
|
|
101
|
+
disabled: {
|
|
102
|
+
color: '#98a2b3',
|
|
103
|
+
fontSize: 15,
|
|
104
|
+
},
|
|
105
|
+
spacer: {
|
|
106
|
+
flex: 1,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
exports.default = KeyboardToolbar;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyboardDismissButton = exports.KeyboardToolbar = exports.KeyboardAwareView = exports.KeyboardAwareScrollView = void 0;
|
|
4
|
+
var KeyboardAwareScrollView_1 = require("./KeyboardAwareScrollView");
|
|
5
|
+
Object.defineProperty(exports, "KeyboardAwareScrollView", { enumerable: true, get: function () { return KeyboardAwareScrollView_1.KeyboardAwareScrollView; } });
|
|
6
|
+
var KeyboardAwareView_1 = require("./KeyboardAwareView");
|
|
7
|
+
Object.defineProperty(exports, "KeyboardAwareView", { enumerable: true, get: function () { return KeyboardAwareView_1.KeyboardAwareView; } });
|
|
8
|
+
var KeyboardToolbar_1 = require("./KeyboardToolbar");
|
|
9
|
+
Object.defineProperty(exports, "KeyboardToolbar", { enumerable: true, get: function () { return KeyboardToolbar_1.KeyboardToolbar; } });
|
|
10
|
+
var KeyboardDismissButton_1 = require("./KeyboardDismissButton");
|
|
11
|
+
Object.defineProperty(exports, "KeyboardDismissButton", { enumerable: true, get: function () { return KeyboardDismissButton_1.KeyboardDismissButton; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { KeyboardProviderProps } from '../types';
|
|
3
|
+
/** Optional provider for development diagnostics; keyboard subscriptions are shared globally. */
|
|
4
|
+
export declare function KeyboardProvider({ children, debug, }: KeyboardProviderProps): React.JSX.Element;
|
|
5
|
+
/** Read whether development diagnostics are enabled on the nearest provider. */
|
|
6
|
+
export declare function useKeyboardDebug(): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyboardProvider = KeyboardProvider;
|
|
4
|
+
exports.useKeyboardDebug = useKeyboardDebug;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const useKeyboardState_1 = require("../hooks/useKeyboardState");
|
|
8
|
+
const DebugContext = (0, react_1.createContext)(false);
|
|
9
|
+
/** Optional provider for development diagnostics; keyboard subscriptions are shared globally. */
|
|
10
|
+
function KeyboardProvider({ children, debug = false, }) {
|
|
11
|
+
const keyboard = (0, useKeyboardState_1.useKeyboardState)();
|
|
12
|
+
const previous = (0, react_1.useRef)(keyboard);
|
|
13
|
+
(0, react_1.useEffect)(() => {
|
|
14
|
+
if (debug &&
|
|
15
|
+
__DEV__ &&
|
|
16
|
+
(previous.current.isVisible !== keyboard.isVisible ||
|
|
17
|
+
previous.current.height !== keyboard.height)) {
|
|
18
|
+
console.debug('[keyboard-toolkit]', keyboard);
|
|
19
|
+
}
|
|
20
|
+
previous.current = keyboard;
|
|
21
|
+
}, [debug, keyboard]);
|
|
22
|
+
return ((0, jsx_runtime_1.jsx)(DebugContext.Provider, { value: debug, children: children }));
|
|
23
|
+
}
|
|
24
|
+
/** Read whether development diagnostics are enabled on the nearest provider. */
|
|
25
|
+
function useKeyboardDebug() {
|
|
26
|
+
return (0, react_1.useContext)(DebugContext);
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { KeyboardProvider } from './KeyboardContext';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyboardProvider = void 0;
|
|
4
|
+
var KeyboardContext_1 = require("./KeyboardContext");
|
|
5
|
+
Object.defineProperty(exports, "KeyboardProvider", { enumerable: true, get: function () { return KeyboardContext_1.KeyboardProvider; } });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useFocusedInput = exports.useKeyboardAnimation = exports.useKeyboardState = exports.useKeyboard = void 0;
|
|
4
|
+
var useKeyboard_1 = require("./useKeyboard");
|
|
5
|
+
Object.defineProperty(exports, "useKeyboard", { enumerable: true, get: function () { return useKeyboard_1.useKeyboard; } });
|
|
6
|
+
var useKeyboardState_1 = require("./useKeyboardState");
|
|
7
|
+
Object.defineProperty(exports, "useKeyboardState", { enumerable: true, get: function () { return useKeyboardState_1.useKeyboardState; } });
|
|
8
|
+
var useKeyboardAnimation_1 = require("./useKeyboardAnimation");
|
|
9
|
+
Object.defineProperty(exports, "useKeyboardAnimation", { enumerable: true, get: function () { return useKeyboardAnimation_1.useKeyboardAnimation; } });
|
|
10
|
+
var useFocusedInput_1 = require("./useFocusedInput");
|
|
11
|
+
Object.defineProperty(exports, "useFocusedInput", { enumerable: true, get: function () { return useFocusedInput_1.useFocusedInput; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FocusedInputLayout } from '../types';
|
|
2
|
+
/** Measure the focused TextInput when the keyboard opens; call `refresh` after layout changes. */
|
|
3
|
+
export declare function useFocusedInput(): {
|
|
4
|
+
focusedInput: FocusedInputLayout | null;
|
|
5
|
+
refresh: () => Promise<FocusedInputLayout | null>;
|
|
6
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useFocusedInput = useFocusedInput;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const focusManager_1 = require("../utils/focusManager");
|
|
6
|
+
const useKeyboardState_1 = require("./useKeyboardState");
|
|
7
|
+
/** Measure the focused TextInput when the keyboard opens; call `refresh` after layout changes. */
|
|
8
|
+
function useFocusedInput() {
|
|
9
|
+
const keyboard = (0, useKeyboardState_1.useKeyboardState)();
|
|
10
|
+
const [focusedInput, setFocusedInput] = (0, react_1.useState)(null);
|
|
11
|
+
const measureSequence = (0, react_1.useRef)(0);
|
|
12
|
+
const refresh = (0, react_1.useCallback)(async () => {
|
|
13
|
+
const sequence = ++measureSequence.current;
|
|
14
|
+
const layout = await (0, focusManager_1.measureFocusedInput)((0, focusManager_1.getFocusedInput)());
|
|
15
|
+
if (sequence === measureSequence.current) {
|
|
16
|
+
setFocusedInput(current => {
|
|
17
|
+
if (current?.x === layout?.x &&
|
|
18
|
+
current?.y === layout?.y &&
|
|
19
|
+
current?.width === layout?.width &&
|
|
20
|
+
current?.height === layout?.height) {
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
return layout;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return layout;
|
|
27
|
+
}, []);
|
|
28
|
+
(0, react_1.useEffect)(() => {
|
|
29
|
+
if (!keyboard.isVisible) {
|
|
30
|
+
measureSequence.current += 1;
|
|
31
|
+
setFocusedInput(null);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
refresh();
|
|
35
|
+
const interval = setInterval(refresh, 120);
|
|
36
|
+
return () => {
|
|
37
|
+
measureSequence.current += 1;
|
|
38
|
+
clearInterval(interval);
|
|
39
|
+
};
|
|
40
|
+
}, [keyboard.isVisible, keyboard.height, refresh]);
|
|
41
|
+
return { focusedInput, refresh };
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useKeyboardState as useKeyboard } from './useKeyboardState';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useKeyboard = void 0;
|
|
4
|
+
var useKeyboardState_1 = require("./useKeyboardState");
|
|
5
|
+
Object.defineProperty(exports, "useKeyboard", { enumerable: true, get: function () { return useKeyboardState_1.useKeyboardState; } });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useKeyboardAnimation = useKeyboardAnimation;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
const useKeyboardState_1 = require("./useKeyboardState");
|
|
7
|
+
/** Provides an animated 0-to-1 visibility value and the native event duration. */
|
|
8
|
+
function useKeyboardAnimation() {
|
|
9
|
+
const keyboard = (0, useKeyboardState_1.useKeyboardState)();
|
|
10
|
+
const progressRef = (0, react_1.useRef)(null);
|
|
11
|
+
if (!progressRef.current) {
|
|
12
|
+
progressRef.current = new react_native_1.Animated.Value(keyboard.progress);
|
|
13
|
+
}
|
|
14
|
+
const progress = progressRef.current;
|
|
15
|
+
const [reduceMotion, setReduceMotion] = (0, react_1.useState)(false);
|
|
16
|
+
(0, react_1.useEffect)(() => {
|
|
17
|
+
let active = true;
|
|
18
|
+
react_native_1.AccessibilityInfo.isReduceMotionEnabled()
|
|
19
|
+
.then(enabled => {
|
|
20
|
+
if (active) {
|
|
21
|
+
setReduceMotion(enabled);
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.catch(() => {
|
|
25
|
+
// Keep animations enabled when the platform preference cannot be read.
|
|
26
|
+
});
|
|
27
|
+
const subscription = react_native_1.AccessibilityInfo.addEventListener('reduceMotionChanged', setReduceMotion);
|
|
28
|
+
return () => {
|
|
29
|
+
active = false;
|
|
30
|
+
subscription.remove();
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
(0, react_1.useEffect)(() => {
|
|
34
|
+
const animation = react_native_1.Animated.timing(progress, {
|
|
35
|
+
toValue: keyboard.progress,
|
|
36
|
+
duration: reduceMotion ? 0 : keyboard.animationDuration,
|
|
37
|
+
useNativeDriver: true,
|
|
38
|
+
});
|
|
39
|
+
animation.start();
|
|
40
|
+
return () => animation.stop();
|
|
41
|
+
}, [keyboard.animationDuration, keyboard.progress, progress, reduceMotion]);
|
|
42
|
+
return { progress, animationDuration: keyboard.animationDuration };
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useKeyboardState = useKeyboardState;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const keyboardEvents_1 = require("../utils/keyboardEvents");
|
|
6
|
+
/** Subscribe to the shared, normalized keyboard state. A provider is optional. */
|
|
7
|
+
function useKeyboardState() {
|
|
8
|
+
return (0, react_1.useSyncExternalStore)(keyboardEvents_1.subscribeKeyboardState, keyboardEvents_1.getKeyboardState, keyboardEvents_1.getKeyboardState);
|
|
9
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { KeyboardAwareScrollView, KeyboardAwareView, KeyboardDismissButton, KeyboardToolbar, } from './components';
|
|
2
|
+
export { KeyboardProvider } from './context';
|
|
3
|
+
export { useFocusedInput, useKeyboard, useKeyboardAnimation, useKeyboardState, } from './hooks';
|
|
4
|
+
export { calculateScrollOffset, calculateKeyboardOverlap, focusInput, getFocusedInput, isAndroid, isIOS, measureFocusedInput, } from './utils';
|
|
5
|
+
export type { FocusedInputLayout, KeyboardAwareScrollViewProps, KeyboardAwareScrollViewRef, KeyboardAwareViewProps, KeyboardAwareViewRef, KeyboardBehavior, KeyboardDismissButtonProps, KeyboardProviderProps, KeyboardState, KeyboardToolbarProps, TextInputRef, } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.measureFocusedInput = exports.isIOS = exports.isAndroid = exports.getFocusedInput = exports.focusInput = exports.calculateKeyboardOverlap = exports.calculateScrollOffset = exports.useKeyboardState = exports.useKeyboardAnimation = exports.useKeyboard = exports.useFocusedInput = exports.KeyboardProvider = exports.KeyboardToolbar = exports.KeyboardDismissButton = exports.KeyboardAwareView = exports.KeyboardAwareScrollView = void 0;
|
|
4
|
+
var components_1 = require("./components");
|
|
5
|
+
Object.defineProperty(exports, "KeyboardAwareScrollView", { enumerable: true, get: function () { return components_1.KeyboardAwareScrollView; } });
|
|
6
|
+
Object.defineProperty(exports, "KeyboardAwareView", { enumerable: true, get: function () { return components_1.KeyboardAwareView; } });
|
|
7
|
+
Object.defineProperty(exports, "KeyboardDismissButton", { enumerable: true, get: function () { return components_1.KeyboardDismissButton; } });
|
|
8
|
+
Object.defineProperty(exports, "KeyboardToolbar", { enumerable: true, get: function () { return components_1.KeyboardToolbar; } });
|
|
9
|
+
var context_1 = require("./context");
|
|
10
|
+
Object.defineProperty(exports, "KeyboardProvider", { enumerable: true, get: function () { return context_1.KeyboardProvider; } });
|
|
11
|
+
var hooks_1 = require("./hooks");
|
|
12
|
+
Object.defineProperty(exports, "useFocusedInput", { enumerable: true, get: function () { return hooks_1.useFocusedInput; } });
|
|
13
|
+
Object.defineProperty(exports, "useKeyboard", { enumerable: true, get: function () { return hooks_1.useKeyboard; } });
|
|
14
|
+
Object.defineProperty(exports, "useKeyboardAnimation", { enumerable: true, get: function () { return hooks_1.useKeyboardAnimation; } });
|
|
15
|
+
Object.defineProperty(exports, "useKeyboardState", { enumerable: true, get: function () { return hooks_1.useKeyboardState; } });
|
|
16
|
+
var utils_1 = require("./utils");
|
|
17
|
+
Object.defineProperty(exports, "calculateScrollOffset", { enumerable: true, get: function () { return utils_1.calculateScrollOffset; } });
|
|
18
|
+
Object.defineProperty(exports, "calculateKeyboardOverlap", { enumerable: true, get: function () { return utils_1.calculateKeyboardOverlap; } });
|
|
19
|
+
Object.defineProperty(exports, "focusInput", { enumerable: true, get: function () { return utils_1.focusInput; } });
|
|
20
|
+
Object.defineProperty(exports, "getFocusedInput", { enumerable: true, get: function () { return utils_1.getFocusedInput; } });
|
|
21
|
+
Object.defineProperty(exports, "isAndroid", { enumerable: true, get: function () { return utils_1.isAndroid; } });
|
|
22
|
+
Object.defineProperty(exports, "isIOS", { enumerable: true, get: function () { return utils_1.isIOS; } });
|
|
23
|
+
Object.defineProperty(exports, "measureFocusedInput", { enumerable: true, get: function () { return utils_1.measureFocusedInput; } });
|