react-native-keyboard-controller 1.21.5 → 1.21.7
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/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt +11 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/index.js +12 -2
- package/lib/commonjs/components/KeyboardChatScrollView/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/types.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +3 -3
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js +4 -4
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/useEndVisible.js +40 -0
- package/lib/commonjs/components/KeyboardChatScrollView/useEndVisible.js.map +1 -0
- package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js +2 -2
- package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardStickyView/index.js +10 -14
- package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardToolbar/index.js +12 -3
- package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -1
- package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js +35 -23
- package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js.map +1 -1
- package/lib/commonjs/hooks/useKeyboardState/index.js +1 -7
- package/lib/commonjs/hooks/useKeyboardState/index.js.map +1 -1
- package/lib/commonjs/module.js +1 -1
- package/lib/commonjs/module.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/index.js +12 -2
- package/lib/module/components/KeyboardChatScrollView/index.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/types.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +3 -3
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js +4 -4
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/useEndVisible.js +33 -0
- package/lib/module/components/KeyboardChatScrollView/useEndVisible.js.map +1 -0
- package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js +2 -2
- package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -1
- package/lib/module/components/KeyboardStickyView/index.js +11 -15
- package/lib/module/components/KeyboardStickyView/index.js.map +1 -1
- package/lib/module/components/KeyboardToolbar/index.js +12 -3
- package/lib/module/components/KeyboardToolbar/index.js.map +1 -1
- package/lib/module/components/ScrollViewWithBottomPadding/index.js +37 -25
- package/lib/module/components/ScrollViewWithBottomPadding/index.js.map +1 -1
- package/lib/module/hooks/useKeyboardState/index.js +1 -7
- package/lib/module/hooks/useKeyboardState/index.js.map +1 -1
- package/lib/module/module.js +1 -1
- package/lib/module/module.js.map +1 -1
- package/lib/typescript/components/KeyboardChatScrollView/index.d.ts +3 -1
- package/lib/typescript/components/KeyboardChatScrollView/types.d.ts +26 -2
- package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts +1 -1
- package/lib/typescript/components/KeyboardChatScrollView/useEndVisible.d.ts +17 -0
- package/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts +1 -1
- package/lib/typescript/components/ScrollViewWithBottomPadding/index.d.ts +12 -0
- package/package.json +1 -1
- package/src/components/KeyboardChatScrollView/index.tsx +15 -2
- package/src/components/KeyboardChatScrollView/types.ts +29 -2
- package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts +3 -3
- package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts +4 -4
- package/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts +1 -1
- package/src/components/KeyboardChatScrollView/useEndVisible.ts +66 -0
- package/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts +3 -3
- package/src/components/KeyboardStickyView/index.tsx +18 -19
- package/src/components/KeyboardToolbar/index.tsx +13 -4
- package/src/components/ScrollViewWithBottomPadding/index.tsx +67 -28
- package/src/hooks/useKeyboardState/index.ts +1 -8
- package/src/module.ts +1 -1
package/lib/module/module.js
CHANGED
package/lib/module/module.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["KeyboardControllerNative","KeyboardEvents","isClosed","lastState","height","duration","timestamp","Date","getTime","target","type","appearance","addListener","e","dismiss","options","keepFocus","animated","Promise","resolve","subscription","undefined","remove","isVisible","state","KeyboardController","setDefaultMode","setInputMode","setFocusTo","preload"],"sources":["module.ts"],"sourcesContent":["import { KeyboardControllerNative, KeyboardEvents } from \"./bindings\";\n\nimport type {\n DismissOptions,\n KeyboardControllerModule,\n KeyboardEventData,\n} from \"./types\";\n\nlet isClosed = true;\nlet lastState: KeyboardEventData = {\n height: 0,\n duration: 0,\n timestamp: new Date().getTime(),\n target: -1,\n type: \"default\",\n appearance: \"light\",\n};\n\nKeyboardEvents.addListener(\"keyboardDidHide\", (e) => {\n isClosed = true;\n lastState = e;\n});\n\nKeyboardEvents.addListener(\"
|
|
1
|
+
{"version":3,"names":["KeyboardControllerNative","KeyboardEvents","isClosed","lastState","height","duration","timestamp","Date","getTime","target","type","appearance","addListener","e","dismiss","options","keepFocus","animated","Promise","resolve","subscription","undefined","remove","isVisible","state","KeyboardController","setDefaultMode","setInputMode","setFocusTo","preload"],"sources":["module.ts"],"sourcesContent":["import { KeyboardControllerNative, KeyboardEvents } from \"./bindings\";\n\nimport type {\n DismissOptions,\n KeyboardControllerModule,\n KeyboardEventData,\n} from \"./types\";\n\nlet isClosed = true;\nlet lastState: KeyboardEventData = {\n height: 0,\n duration: 0,\n timestamp: new Date().getTime(),\n target: -1,\n type: \"default\",\n appearance: \"light\",\n};\n\nKeyboardEvents.addListener(\"keyboardDidHide\", (e) => {\n isClosed = true;\n lastState = e;\n});\n\nKeyboardEvents.addListener(\"keyboardWillShow\", (e) => {\n isClosed = false;\n lastState = e;\n});\n\nconst dismiss = async (options?: Partial<DismissOptions>): Promise<void> => {\n const keepFocus = options?.keepFocus ?? false;\n const animated = options?.animated ?? true;\n\n return new Promise((resolve) => {\n if (isClosed) {\n resolve();\n\n return;\n }\n\n const subscription = KeyboardEvents.addListener(\"keyboardDidHide\", () => {\n resolve(undefined);\n subscription.remove();\n });\n\n KeyboardControllerNative.dismiss(keepFocus, animated);\n });\n};\nconst isVisible = () => !isClosed;\nconst state = () => lastState;\n\n/**\n * KeyboardController module. Helps to perform imperative actions/checks with keyboard.\n */\nexport const KeyboardController: KeyboardControllerModule = {\n setDefaultMode: KeyboardControllerNative.setDefaultMode,\n setInputMode: KeyboardControllerNative.setInputMode,\n setFocusTo: KeyboardControllerNative.setFocusTo,\n preload: KeyboardControllerNative.preload,\n dismiss,\n isVisible,\n state,\n};\n"],"mappings":"AAAA,SAASA,wBAAwB,EAAEC,cAAc,QAAQ,YAAY;AAQrE,IAAIC,QAAQ,GAAG,IAAI;AACnB,IAAIC,SAA4B,GAAG;EACjCC,MAAM,EAAE,CAAC;EACTC,QAAQ,EAAE,CAAC;EACXC,SAAS,EAAE,IAAIC,IAAI,CAAC,CAAC,CAACC,OAAO,CAAC,CAAC;EAC/BC,MAAM,EAAE,CAAC,CAAC;EACVC,IAAI,EAAE,SAAS;EACfC,UAAU,EAAE;AACd,CAAC;AAEDV,cAAc,CAACW,WAAW,CAAC,iBAAiB,EAAGC,CAAC,IAAK;EACnDX,QAAQ,GAAG,IAAI;EACfC,SAAS,GAAGU,CAAC;AACf,CAAC,CAAC;AAEFZ,cAAc,CAACW,WAAW,CAAC,kBAAkB,EAAGC,CAAC,IAAK;EACpDX,QAAQ,GAAG,KAAK;EAChBC,SAAS,GAAGU,CAAC;AACf,CAAC,CAAC;AAEF,MAAMC,OAAO,GAAG,MAAOC,OAAiC,IAAoB;EAC1E,MAAMC,SAAS,GAAG,CAAAD,OAAO,aAAPA,OAAO,uBAAPA,OAAO,CAAEC,SAAS,KAAI,KAAK;EAC7C,MAAMC,QAAQ,GAAG,CAAAF,OAAO,aAAPA,OAAO,uBAAPA,OAAO,CAAEE,QAAQ,KAAI,IAAI;EAE1C,OAAO,IAAIC,OAAO,CAAEC,OAAO,IAAK;IAC9B,IAAIjB,QAAQ,EAAE;MACZiB,OAAO,CAAC,CAAC;MAET;IACF;IAEA,MAAMC,YAAY,GAAGnB,cAAc,CAACW,WAAW,CAAC,iBAAiB,EAAE,MAAM;MACvEO,OAAO,CAACE,SAAS,CAAC;MAClBD,YAAY,CAACE,MAAM,CAAC,CAAC;IACvB,CAAC,CAAC;IAEFtB,wBAAwB,CAACc,OAAO,CAACE,SAAS,EAAEC,QAAQ,CAAC;EACvD,CAAC,CAAC;AACJ,CAAC;AACD,MAAMM,SAAS,GAAGA,CAAA,KAAM,CAACrB,QAAQ;AACjC,MAAMsB,KAAK,GAAGA,CAAA,KAAMrB,SAAS;;AAE7B;AACA;AACA;AACA,OAAO,MAAMsB,kBAA4C,GAAG;EAC1DC,cAAc,EAAE1B,wBAAwB,CAAC0B,cAAc;EACvDC,YAAY,EAAE3B,wBAAwB,CAAC2B,YAAY;EACnDC,UAAU,EAAE5B,wBAAwB,CAAC4B,UAAU;EAC/CC,OAAO,EAAE7B,wBAAwB,CAAC6B,OAAO;EACzCf,OAAO;EACPS,SAAS;EACTC;AACF,CAAC","ignoreList":[]}
|
|
@@ -5,10 +5,12 @@ declare const KeyboardChatScrollView: React.ForwardRefExoticComponent<{
|
|
|
5
5
|
inverted?: boolean;
|
|
6
6
|
offset?: number;
|
|
7
7
|
keyboardLiftBehavior?: import("./hooks").KeyboardLiftBehavior;
|
|
8
|
-
freeze?: boolean
|
|
8
|
+
freeze?: boolean | import("react-native-reanimated").SharedValue<boolean>;
|
|
9
9
|
extraContentPadding?: import("react-native-reanimated").SharedValue<number>;
|
|
10
10
|
applyWorkaroundForContentInsetHitTestBug?: boolean;
|
|
11
11
|
blankSpace?: import("react-native-reanimated").SharedValue<number>;
|
|
12
|
+
onContentInsetChange?: (insets: import("../ScrollViewWithBottomPadding").ScrollViewContentInsets) => void;
|
|
13
|
+
onEndVisible?: (visible: boolean) => void;
|
|
12
14
|
} & import("react-native").ScrollViewProps & {
|
|
13
15
|
children?: React.ReactNode | undefined;
|
|
14
16
|
} & React.RefAttributes<Reanimated.ScrollView>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnimatedScrollViewComponent } from "../ScrollViewWithBottomPadding";
|
|
1
|
+
import type { AnimatedScrollViewComponent, ScrollViewContentInsets } from "../ScrollViewWithBottomPadding";
|
|
2
2
|
import type { KeyboardLiftBehavior } from "./useChatKeyboard/types";
|
|
3
3
|
import type { ScrollViewProps } from "react-native";
|
|
4
4
|
import type { SharedValue } from "react-native-reanimated";
|
|
@@ -45,7 +45,7 @@ export type KeyboardChatScrollViewProps = {
|
|
|
45
45
|
*
|
|
46
46
|
* Default is `false`.
|
|
47
47
|
*/
|
|
48
|
-
freeze?: boolean
|
|
48
|
+
freeze?: boolean | SharedValue<boolean>;
|
|
49
49
|
/**
|
|
50
50
|
* A shared value representing additional padding from external elements
|
|
51
51
|
* (e.g., a growing multiline `TextInput` in a `KeyboardStickyView`).
|
|
@@ -86,4 +86,28 @@ export type KeyboardChatScrollViewProps = {
|
|
|
86
86
|
* Default is `undefined` (equivalent to `0` — no minimum floor).
|
|
87
87
|
*/
|
|
88
88
|
blankSpace?: SharedValue<number>;
|
|
89
|
+
/**
|
|
90
|
+
* Fires whenever the effective content inset changes — the static `contentInset`
|
|
91
|
+
* prop combined with the dynamic keyboard-driven padding.
|
|
92
|
+
*
|
|
93
|
+
* Useful on Android, where the synthetic content inset is not reflected in the native
|
|
94
|
+
* `onScroll` event payload. Consumers such as virtualized lists computing their own
|
|
95
|
+
* `scrollToEnd` target can use this to track the current inset alongside scroll offsets.
|
|
96
|
+
*/
|
|
97
|
+
onContentInsetChange?: (insets: ScrollViewContentInsets) => void;
|
|
98
|
+
/**
|
|
99
|
+
* Fires whenever the visibility of the content "end" changes — both when the user
|
|
100
|
+
* arrives at the end and when they leave it. The boolean parameter reflects the new state.
|
|
101
|
+
*
|
|
102
|
+
* For non-inverted lists, the "end" is the bottom of the content. For inverted lists
|
|
103
|
+
* (`inverted={true}`), the "end" is the top of the scroll view, where the latest messages
|
|
104
|
+
* are rendered. The same internal detection drives `keyboardLiftBehavior="whenAtEnd"`.
|
|
105
|
+
*
|
|
106
|
+
* The callback can be either a plain JS function or a Reanimated worklet — the type is
|
|
107
|
+
* detected automatically. Worklets run on the UI thread; plain functions are dispatched
|
|
108
|
+
* via `runOnJS`.
|
|
109
|
+
*
|
|
110
|
+
* Fires once on mount with the initial state (after the scroll view has been measured).
|
|
111
|
+
*/
|
|
112
|
+
onEndVisible?: (visible: boolean) => void;
|
|
89
113
|
} & ScrollViewProps;
|
|
@@ -4,7 +4,7 @@ type KeyboardLiftBehavior = "always" | "whenAtEnd" | "persistent" | "never";
|
|
|
4
4
|
type UseChatKeyboardOptions = {
|
|
5
5
|
inverted: boolean;
|
|
6
6
|
keyboardLiftBehavior: KeyboardLiftBehavior;
|
|
7
|
-
freeze: boolean
|
|
7
|
+
freeze: SharedValue<boolean>;
|
|
8
8
|
offset: number;
|
|
9
9
|
blankSpace: SharedValue<number>;
|
|
10
10
|
/** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
2
|
+
type EndVisibleCallback = (visible: boolean) => void;
|
|
3
|
+
type Options = {
|
|
4
|
+
scroll: SharedValue<number>;
|
|
5
|
+
layout: SharedValue<{
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}>;
|
|
9
|
+
size: SharedValue<{
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}>;
|
|
13
|
+
inverted: boolean;
|
|
14
|
+
onEndVisible?: EndVisibleCallback;
|
|
15
|
+
};
|
|
16
|
+
export declare const useEndVisible: ({ scroll, layout, size, inverted, onEndVisible, }: Options) => void;
|
|
17
|
+
export {};
|
|
@@ -24,7 +24,7 @@ type UseExtraContentPaddingOptions = {
|
|
|
24
24
|
contentOffsetY?: SharedValue<number>;
|
|
25
25
|
inverted: boolean;
|
|
26
26
|
keyboardLiftBehavior: KeyboardLiftBehavior;
|
|
27
|
-
freeze: boolean
|
|
27
|
+
freeze: SharedValue<boolean>;
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
30
|
* Hook that reacts to `extraContentPadding` changes and conditionally
|
|
@@ -4,6 +4,12 @@ import type { ScrollViewProps } from "react-native";
|
|
|
4
4
|
import type { SharedValue } from "react-native-reanimated";
|
|
5
5
|
type AnimatedScrollViewProps = React.ComponentProps<typeof Reanimated.ScrollView>;
|
|
6
6
|
export type AnimatedScrollViewComponent = React.ForwardRefExoticComponent<AnimatedScrollViewProps & React.RefAttributes<Reanimated.ScrollView>>;
|
|
7
|
+
export type ScrollViewContentInsets = {
|
|
8
|
+
top: number;
|
|
9
|
+
bottom: number;
|
|
10
|
+
left: number;
|
|
11
|
+
right: number;
|
|
12
|
+
};
|
|
7
13
|
declare const ScrollViewWithBottomPadding: React.ForwardRefExoticComponent<{
|
|
8
14
|
ScrollViewComponent: AnimatedScrollViewComponent;
|
|
9
15
|
children?: React.ReactNode;
|
|
@@ -14,5 +20,11 @@ declare const ScrollViewWithBottomPadding: React.ForwardRefExoticComponent<{
|
|
|
14
20
|
/** Absolute Y content offset (iOS only, for KeyboardChatScrollView). */
|
|
15
21
|
contentOffsetY?: SharedValue<number>;
|
|
16
22
|
applyWorkaroundForContentInsetHitTestBug?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Fires whenever the effective content inset changes (combines the static `contentInset`
|
|
25
|
+
* prop with the dynamic keyboard-driven padding). Useful on Android where the synthetic
|
|
26
|
+
* inset is not reflected in `onScroll` events.
|
|
27
|
+
*/
|
|
28
|
+
onContentInsetChange?: (insets: ScrollViewContentInsets) => void;
|
|
17
29
|
} & ScrollViewProps & React.RefAttributes<Reanimated.ScrollView>>;
|
|
18
30
|
export default ScrollViewWithBottomPadding;
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ import useCombinedRef from "../hooks/useCombinedRef";
|
|
|
12
12
|
import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding";
|
|
13
13
|
|
|
14
14
|
import { useChatKeyboard } from "./useChatKeyboard";
|
|
15
|
+
import { useEndVisible } from "./useEndVisible";
|
|
15
16
|
import { useExtraContentPadding } from "./useExtraContentPadding";
|
|
16
17
|
|
|
17
18
|
import type { KeyboardChatScrollViewProps } from "./types";
|
|
@@ -37,12 +38,16 @@ const KeyboardChatScrollView = forwardRef<
|
|
|
37
38
|
applyWorkaroundForContentInsetHitTestBug = false,
|
|
38
39
|
onLayout: onLayoutProp,
|
|
39
40
|
onContentSizeChange: onContentSizeChangeProp,
|
|
41
|
+
onEndVisible,
|
|
40
42
|
...rest
|
|
41
43
|
},
|
|
42
44
|
ref,
|
|
43
45
|
) => {
|
|
44
46
|
const scrollViewRef = useAnimatedRef<Reanimated.ScrollView>();
|
|
45
47
|
const onRef = useCombinedRef(ref, scrollViewRef);
|
|
48
|
+
const freezeSV = useDerivedValue(() =>
|
|
49
|
+
typeof freeze === "boolean" ? freeze : freeze.value,
|
|
50
|
+
);
|
|
46
51
|
const {
|
|
47
52
|
padding,
|
|
48
53
|
currentHeight,
|
|
@@ -55,7 +60,7 @@ const KeyboardChatScrollView = forwardRef<
|
|
|
55
60
|
} = useChatKeyboard(scrollViewRef, {
|
|
56
61
|
inverted,
|
|
57
62
|
keyboardLiftBehavior,
|
|
58
|
-
freeze,
|
|
63
|
+
freeze: freezeSV,
|
|
59
64
|
offset,
|
|
60
65
|
blankSpace,
|
|
61
66
|
extraContentPadding,
|
|
@@ -72,7 +77,15 @@ const KeyboardChatScrollView = forwardRef<
|
|
|
72
77
|
contentOffsetY,
|
|
73
78
|
inverted,
|
|
74
79
|
keyboardLiftBehavior,
|
|
75
|
-
freeze,
|
|
80
|
+
freeze: freezeSV,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
useEndVisible({
|
|
84
|
+
scroll,
|
|
85
|
+
layout,
|
|
86
|
+
size,
|
|
87
|
+
inverted,
|
|
88
|
+
onEndVisible,
|
|
76
89
|
});
|
|
77
90
|
|
|
78
91
|
const totalPadding = useDerivedValue(() =>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AnimatedScrollViewComponent,
|
|
3
|
+
ScrollViewContentInsets,
|
|
4
|
+
} from "../ScrollViewWithBottomPadding";
|
|
2
5
|
import type { KeyboardLiftBehavior } from "./useChatKeyboard/types";
|
|
3
6
|
import type { ScrollViewProps } from "react-native";
|
|
4
7
|
import type { SharedValue } from "react-native-reanimated";
|
|
@@ -46,7 +49,7 @@ export type KeyboardChatScrollViewProps = {
|
|
|
46
49
|
*
|
|
47
50
|
* Default is `false`.
|
|
48
51
|
*/
|
|
49
|
-
freeze?: boolean
|
|
52
|
+
freeze?: boolean | SharedValue<boolean>;
|
|
50
53
|
/**
|
|
51
54
|
* A shared value representing additional padding from external elements
|
|
52
55
|
* (e.g., a growing multiline `TextInput` in a `KeyboardStickyView`).
|
|
@@ -87,4 +90,28 @@ export type KeyboardChatScrollViewProps = {
|
|
|
87
90
|
* Default is `undefined` (equivalent to `0` — no minimum floor).
|
|
88
91
|
*/
|
|
89
92
|
blankSpace?: SharedValue<number>;
|
|
93
|
+
/**
|
|
94
|
+
* Fires whenever the effective content inset changes — the static `contentInset`
|
|
95
|
+
* prop combined with the dynamic keyboard-driven padding.
|
|
96
|
+
*
|
|
97
|
+
* Useful on Android, where the synthetic content inset is not reflected in the native
|
|
98
|
+
* `onScroll` event payload. Consumers such as virtualized lists computing their own
|
|
99
|
+
* `scrollToEnd` target can use this to track the current inset alongside scroll offsets.
|
|
100
|
+
*/
|
|
101
|
+
onContentInsetChange?: (insets: ScrollViewContentInsets) => void;
|
|
102
|
+
/**
|
|
103
|
+
* Fires whenever the visibility of the content "end" changes — both when the user
|
|
104
|
+
* arrives at the end and when they leave it. The boolean parameter reflects the new state.
|
|
105
|
+
*
|
|
106
|
+
* For non-inverted lists, the "end" is the bottom of the content. For inverted lists
|
|
107
|
+
* (`inverted={true}`), the "end" is the top of the scroll view, where the latest messages
|
|
108
|
+
* are rendered. The same internal detection drives `keyboardLiftBehavior="whenAtEnd"`.
|
|
109
|
+
*
|
|
110
|
+
* The callback can be either a plain JS function or a Reanimated worklet — the type is
|
|
111
|
+
* detected automatically. Worklets run on the UI thread; plain functions are dispatched
|
|
112
|
+
* via `runOnJS`.
|
|
113
|
+
*
|
|
114
|
+
* Fires once on mount with the initial state (after the scroll view has been measured).
|
|
115
|
+
*/
|
|
116
|
+
onEndVisible?: (visible: boolean) => void;
|
|
90
117
|
} & ScrollViewProps;
|
|
@@ -64,7 +64,7 @@ function useChatKeyboard(
|
|
|
64
64
|
onStart: (e) => {
|
|
65
65
|
"worklet";
|
|
66
66
|
|
|
67
|
-
if (freeze) {
|
|
67
|
+
if (freeze.value) {
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -229,7 +229,7 @@ function useChatKeyboard(
|
|
|
229
229
|
onEnd: (e) => {
|
|
230
230
|
"worklet";
|
|
231
231
|
|
|
232
|
-
if (freeze) {
|
|
232
|
+
if (freeze.value) {
|
|
233
233
|
return;
|
|
234
234
|
}
|
|
235
235
|
|
|
@@ -242,7 +242,7 @@ function useChatKeyboard(
|
|
|
242
242
|
padding.value = effective;
|
|
243
243
|
},
|
|
244
244
|
},
|
|
245
|
-
[inverted, keyboardLiftBehavior,
|
|
245
|
+
[inverted, keyboardLiftBehavior, offset, extraContentPadding],
|
|
246
246
|
);
|
|
247
247
|
|
|
248
248
|
return {
|
|
@@ -82,7 +82,7 @@ function useChatKeyboard(
|
|
|
82
82
|
onStart: (e) => {
|
|
83
83
|
"worklet";
|
|
84
84
|
|
|
85
|
-
if (freeze) {
|
|
85
|
+
if (freeze.value) {
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -166,7 +166,7 @@ function useChatKeyboard(
|
|
|
166
166
|
onMove: (e) => {
|
|
167
167
|
"worklet";
|
|
168
168
|
|
|
169
|
-
if (freeze) {
|
|
169
|
+
if (freeze.value) {
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -340,7 +340,7 @@ function useChatKeyboard(
|
|
|
340
340
|
onEnd: (e) => {
|
|
341
341
|
"worklet";
|
|
342
342
|
|
|
343
|
-
if (freeze) {
|
|
343
|
+
if (freeze.value) {
|
|
344
344
|
return;
|
|
345
345
|
}
|
|
346
346
|
|
|
@@ -358,7 +358,7 @@ function useChatKeyboard(
|
|
|
358
358
|
}
|
|
359
359
|
},
|
|
360
360
|
},
|
|
361
|
-
[inverted, keyboardLiftBehavior,
|
|
361
|
+
[inverted, keyboardLiftBehavior, offset],
|
|
362
362
|
);
|
|
363
363
|
|
|
364
364
|
return {
|
|
@@ -6,7 +6,7 @@ type KeyboardLiftBehavior = "always" | "whenAtEnd" | "persistent" | "never";
|
|
|
6
6
|
type UseChatKeyboardOptions = {
|
|
7
7
|
inverted: boolean;
|
|
8
8
|
keyboardLiftBehavior: KeyboardLiftBehavior;
|
|
9
|
-
freeze: boolean
|
|
9
|
+
freeze: SharedValue<boolean>;
|
|
10
10
|
offset: number;
|
|
11
11
|
blankSpace: SharedValue<number>;
|
|
12
12
|
/** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
runOnJS,
|
|
4
|
+
useAnimatedReaction,
|
|
5
|
+
useDerivedValue,
|
|
6
|
+
} from "react-native-reanimated";
|
|
7
|
+
|
|
8
|
+
import { isScrollAtEnd } from "./useChatKeyboard/helpers";
|
|
9
|
+
|
|
10
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
11
|
+
|
|
12
|
+
type EndVisibleCallback = (visible: boolean) => void;
|
|
13
|
+
|
|
14
|
+
type Options = {
|
|
15
|
+
scroll: SharedValue<number>;
|
|
16
|
+
layout: SharedValue<{ width: number; height: number }>;
|
|
17
|
+
size: SharedValue<{ width: number; height: number }>;
|
|
18
|
+
inverted: boolean;
|
|
19
|
+
onEndVisible?: EndVisibleCallback;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const hasWorkletHash = (value: unknown): boolean =>
|
|
23
|
+
typeof value === "function" &&
|
|
24
|
+
!!(value as unknown as Record<string, unknown>).__workletHash;
|
|
25
|
+
|
|
26
|
+
export const useEndVisible = ({
|
|
27
|
+
scroll,
|
|
28
|
+
layout,
|
|
29
|
+
size,
|
|
30
|
+
inverted,
|
|
31
|
+
onEndVisible,
|
|
32
|
+
}: Options) => {
|
|
33
|
+
const isWorklet = useMemo(() => hasWorkletHash(onEndVisible), [onEndVisible]);
|
|
34
|
+
|
|
35
|
+
const isAtEnd = useDerivedValue(() => {
|
|
36
|
+
// Wait until the scroll view has been measured to avoid a spurious initial
|
|
37
|
+
// `true` on a (0,0,0) layout (the helper would otherwise treat unmeasured
|
|
38
|
+
// state as "at end" because 0 + 0 >= 0 - threshold).
|
|
39
|
+
if (layout.value.height === 0 || size.value.height === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return isScrollAtEnd(
|
|
44
|
+
scroll.value,
|
|
45
|
+
layout.value.height,
|
|
46
|
+
size.value.height,
|
|
47
|
+
inverted,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
useAnimatedReaction(
|
|
52
|
+
() => isAtEnd.value,
|
|
53
|
+
(current, previous) => {
|
|
54
|
+
if (current === null || current === previous || !onEndVisible) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (isWorklet) {
|
|
59
|
+
onEndVisible(current);
|
|
60
|
+
} else {
|
|
61
|
+
runOnJS(onEndVisible)(current);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[onEndVisible, isWorklet, inverted],
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -26,7 +26,7 @@ type UseExtraContentPaddingOptions = {
|
|
|
26
26
|
contentOffsetY?: SharedValue<number>;
|
|
27
27
|
inverted: boolean;
|
|
28
28
|
keyboardLiftBehavior: KeyboardLiftBehavior;
|
|
29
|
-
freeze: boolean
|
|
29
|
+
freeze: SharedValue<boolean>;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -81,7 +81,7 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void {
|
|
|
81
81
|
useAnimatedReaction(
|
|
82
82
|
() => extraContentPadding.value,
|
|
83
83
|
(current, previous) => {
|
|
84
|
-
if (freeze || previous === null) {
|
|
84
|
+
if (freeze.value || previous === null) {
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -141,7 +141,7 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void {
|
|
|
141
141
|
scrollToTarget(target);
|
|
142
142
|
}
|
|
143
143
|
},
|
|
144
|
-
[inverted, keyboardLiftBehavior
|
|
144
|
+
[inverted, keyboardLiftBehavior],
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import React, { forwardRef, useMemo } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import Reanimated, {
|
|
3
|
+
interpolate,
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
} from "react-native-reanimated";
|
|
3
6
|
|
|
4
|
-
import {
|
|
7
|
+
import { useReanimatedKeyboardAnimation } from "../../hooks";
|
|
5
8
|
|
|
6
9
|
import type { View, ViewProps } from "react-native";
|
|
7
10
|
|
|
@@ -51,29 +54,25 @@ const KeyboardStickyView = forwardRef<
|
|
|
51
54
|
},
|
|
52
55
|
ref,
|
|
53
56
|
) => {
|
|
54
|
-
const { height, progress } =
|
|
57
|
+
const { height, progress } = useReanimatedKeyboardAnimation();
|
|
55
58
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
outputRange: [closed, opened],
|
|
59
|
-
});
|
|
59
|
+
const stickyViewStyle = useAnimatedStyle(() => {
|
|
60
|
+
const offset = interpolate(progress.value, [0, 1], [closed, opened]);
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
return {
|
|
63
|
+
transform: [{ translateY: enabled ? height.value + offset : closed }],
|
|
64
|
+
};
|
|
65
|
+
}, [closed, opened, enabled]);
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
style,
|
|
70
|
-
];
|
|
71
|
-
}, [closed, enabled, height, offset, style]);
|
|
67
|
+
const styles = useMemo(
|
|
68
|
+
() => [style, stickyViewStyle],
|
|
69
|
+
[style, stickyViewStyle],
|
|
70
|
+
);
|
|
72
71
|
|
|
73
72
|
return (
|
|
74
|
-
<
|
|
73
|
+
<Reanimated.View ref={ref} style={styles} {...props}>
|
|
75
74
|
{children}
|
|
76
|
-
</
|
|
75
|
+
</Reanimated.View>
|
|
77
76
|
);
|
|
78
77
|
},
|
|
79
78
|
);
|
|
@@ -99,11 +99,13 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> & {
|
|
|
99
99
|
);
|
|
100
100
|
const containerStyle = useMemo(
|
|
101
101
|
() => [
|
|
102
|
+
// CRUCIAL: gives the native view real bounds
|
|
103
|
+
styles.sticky,
|
|
102
104
|
KEYBOARD_HAS_ROUNDED_CORNERS
|
|
103
|
-
? {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
105
|
+
? ({
|
|
106
|
+
left: (insets?.left ?? 0) + 16,
|
|
107
|
+
right: (insets?.right ?? 0) + 16,
|
|
108
|
+
} as const)
|
|
107
109
|
: null,
|
|
108
110
|
],
|
|
109
111
|
[insets],
|
|
@@ -207,6 +209,13 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> & {
|
|
|
207
209
|
};
|
|
208
210
|
|
|
209
211
|
const styles = StyleSheet.create({
|
|
212
|
+
sticky: {
|
|
213
|
+
position: "absolute",
|
|
214
|
+
left: 0,
|
|
215
|
+
right: 0,
|
|
216
|
+
bottom: 0,
|
|
217
|
+
height: KEYBOARD_TOOLBAR_HEIGHT,
|
|
218
|
+
},
|
|
210
219
|
toolbar: {
|
|
211
220
|
position: "absolute",
|
|
212
221
|
bottom: 0,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import React, { forwardRef } from "react";
|
|
2
|
-
import { Platform
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
3
|
import Reanimated, {
|
|
4
|
+
runOnJS,
|
|
4
5
|
useAnimatedProps,
|
|
6
|
+
useAnimatedReaction,
|
|
7
|
+
useDerivedValue,
|
|
5
8
|
useSharedValue,
|
|
6
9
|
} from "react-native-reanimated";
|
|
7
10
|
|
|
@@ -26,6 +29,13 @@ export type AnimatedScrollViewComponent = React.ForwardRefExoticComponent<
|
|
|
26
29
|
AnimatedScrollViewProps & React.RefAttributes<Reanimated.ScrollView>
|
|
27
30
|
>;
|
|
28
31
|
|
|
32
|
+
export type ScrollViewContentInsets = {
|
|
33
|
+
top: number;
|
|
34
|
+
bottom: number;
|
|
35
|
+
left: number;
|
|
36
|
+
right: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
29
39
|
type ScrollViewWithBottomPaddingProps = {
|
|
30
40
|
ScrollViewComponent: AnimatedScrollViewComponent;
|
|
31
41
|
children?: React.ReactNode;
|
|
@@ -36,6 +46,12 @@ type ScrollViewWithBottomPaddingProps = {
|
|
|
36
46
|
/** Absolute Y content offset (iOS only, for KeyboardChatScrollView). */
|
|
37
47
|
contentOffsetY?: SharedValue<number>;
|
|
38
48
|
applyWorkaroundForContentInsetHitTestBug?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Fires whenever the effective content inset changes (combines the static `contentInset`
|
|
51
|
+
* prop with the dynamic keyboard-driven padding). Useful on Android where the synthetic
|
|
52
|
+
* inset is not reflected in `onScroll` events.
|
|
53
|
+
*/
|
|
54
|
+
onContentInsetChange?: (insets: ScrollViewContentInsets) => void;
|
|
39
55
|
} & ScrollViewProps;
|
|
40
56
|
|
|
41
57
|
const ScrollViewWithBottomPadding = forwardRef<
|
|
@@ -52,6 +68,7 @@ const ScrollViewWithBottomPadding = forwardRef<
|
|
|
52
68
|
inverted,
|
|
53
69
|
contentOffsetY,
|
|
54
70
|
applyWorkaroundForContentInsetHitTestBug,
|
|
71
|
+
onContentInsetChange,
|
|
55
72
|
children,
|
|
56
73
|
...rest
|
|
57
74
|
},
|
|
@@ -59,11 +76,52 @@ const ScrollViewWithBottomPadding = forwardRef<
|
|
|
59
76
|
) => {
|
|
60
77
|
const prevContentOffsetY = useSharedValue<number | null>(null);
|
|
61
78
|
|
|
79
|
+
const insets = useDerivedValue(() => {
|
|
80
|
+
const dynamicTop = inverted ? bottomPadding.value : 0;
|
|
81
|
+
const dynamicBottom = !inverted ? bottomPadding.value : 0;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
dynamic: {
|
|
85
|
+
top: dynamicTop,
|
|
86
|
+
bottom: dynamicBottom,
|
|
87
|
+
},
|
|
88
|
+
effective: {
|
|
89
|
+
top: dynamicTop + (contentInset?.top || 0),
|
|
90
|
+
bottom: dynamicBottom + (contentInset?.bottom || 0),
|
|
91
|
+
left: contentInset?.left || 0,
|
|
92
|
+
right: contentInset?.right || 0,
|
|
93
|
+
} as ScrollViewContentInsets,
|
|
94
|
+
};
|
|
95
|
+
}, [
|
|
96
|
+
inverted,
|
|
97
|
+
contentInset?.top,
|
|
98
|
+
contentInset?.bottom,
|
|
99
|
+
contentInset?.left,
|
|
100
|
+
contentInset?.right,
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
useAnimatedReaction(
|
|
104
|
+
() => insets.value.effective,
|
|
105
|
+
(current, previous) => {
|
|
106
|
+
if (!onContentInsetChange) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
previous &&
|
|
111
|
+
current.top === previous.top &&
|
|
112
|
+
current.bottom === previous.bottom &&
|
|
113
|
+
current.left === previous.left &&
|
|
114
|
+
current.right === previous.right
|
|
115
|
+
) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
runOnJS(onContentInsetChange)(current);
|
|
119
|
+
},
|
|
120
|
+
[onContentInsetChange],
|
|
121
|
+
);
|
|
122
|
+
|
|
62
123
|
const animatedProps = useAnimatedProps(() => {
|
|
63
|
-
const
|
|
64
|
-
const insetBottom = !inverted ? bottomPadding.value : 0;
|
|
65
|
-
const bottom = insetBottom + (contentInset?.bottom || 0);
|
|
66
|
-
const top = insetTop + (contentInset?.top || 0);
|
|
124
|
+
const { dynamic, effective } = insets.value;
|
|
67
125
|
|
|
68
126
|
const indicatorPadding = scrollIndicatorPadding ?? bottomPadding;
|
|
69
127
|
const indicatorTop =
|
|
@@ -75,12 +133,7 @@ const ScrollViewWithBottomPadding = forwardRef<
|
|
|
75
133
|
|
|
76
134
|
const result: Record<string, unknown> = {
|
|
77
135
|
// iOS prop
|
|
78
|
-
contentInset:
|
|
79
|
-
bottom: bottom,
|
|
80
|
-
top: top,
|
|
81
|
-
right: contentInset?.right,
|
|
82
|
-
left: contentInset?.left,
|
|
83
|
-
},
|
|
136
|
+
contentInset: effective,
|
|
84
137
|
scrollIndicatorInsets: {
|
|
85
138
|
bottom: indicatorBottom,
|
|
86
139
|
top: indicatorTop,
|
|
@@ -88,8 +141,8 @@ const ScrollViewWithBottomPadding = forwardRef<
|
|
|
88
141
|
left: scrollIndicatorInsets?.left,
|
|
89
142
|
},
|
|
90
143
|
// Android prop
|
|
91
|
-
contentInsetBottom:
|
|
92
|
-
contentInsetTop:
|
|
144
|
+
contentInsetBottom: dynamic.bottom,
|
|
145
|
+
contentInsetTop: dynamic.top,
|
|
93
146
|
};
|
|
94
147
|
|
|
95
148
|
if (contentOffsetY) {
|
|
@@ -104,10 +157,6 @@ const ScrollViewWithBottomPadding = forwardRef<
|
|
|
104
157
|
|
|
105
158
|
return result;
|
|
106
159
|
}, [
|
|
107
|
-
contentInset?.bottom,
|
|
108
|
-
contentInset?.top,
|
|
109
|
-
contentInset?.right,
|
|
110
|
-
contentInset?.left,
|
|
111
160
|
scrollIndicatorInsets?.bottom,
|
|
112
161
|
scrollIndicatorInsets?.top,
|
|
113
162
|
scrollIndicatorInsets?.right,
|
|
@@ -125,17 +174,7 @@ const ScrollViewWithBottomPadding = forwardRef<
|
|
|
125
174
|
style={styles.container}
|
|
126
175
|
>
|
|
127
176
|
<ScrollViewComponent ref={ref} animatedProps={animatedProps} {...rest}>
|
|
128
|
-
{
|
|
129
|
-
// The only thing it can break is `StickyHeader`, but it's already broken in FlatList and other lists
|
|
130
|
-
// don't support this functionality, so we can add additional view here
|
|
131
|
-
// The correct fix would be to add a new prop in ScrollView that allows
|
|
132
|
-
// to customize children extraction logic and skip custom view
|
|
133
|
-
<View collapsable={false} nativeID="container">
|
|
134
|
-
{children}
|
|
135
|
-
</View>
|
|
136
|
-
) : (
|
|
137
|
-
children
|
|
138
|
-
)}
|
|
177
|
+
{children}
|
|
139
178
|
</ScrollViewComponent>
|
|
140
179
|
</ReanimatedClippingScrollView>
|
|
141
180
|
);
|