react-native-dodge-keyboard 1.0.2 → 1.0.4

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 (4) hide show
  1. package/index.d.ts +15 -3
  2. package/index.js +418 -254
  3. package/package.json +1 -1
  4. package/TODO +0 -1
package/index.d.ts CHANGED
@@ -63,12 +63,15 @@ export interface DodgeKeyboardProps {
63
63
  /**
64
64
  * an handler used internally for checking if a view is focused
65
65
  *
66
+ * @param {View} ref - the reference of the view to check if it is currently focused
67
+ * @param {View} refList - list of all views references that were marked as `focusable`
68
+ *
66
69
  * @default
67
70
  * ```js
68
71
  * r => r?.isFocused?.()
69
72
  * ```
70
73
  */
71
- checkIfElementIsFocused?: (ref: View) => boolean;
74
+ checkIfElementIsFocused?: (ref: View, refList: View[]) => boolean;
72
75
 
73
76
  /**
74
77
  * Child element(s) wrapped by the dodge container.
@@ -97,11 +100,20 @@ interface doHijackResult {
97
100
 
98
101
  interface ReactHijackerProps {
99
102
  doHijack: (node: React.ReactNode, path: Array<any> | undefined) => doHijackResult,
100
- path?: Array<any> | undefined;
103
+ enableLocator?: boolean | undefined;
101
104
  children?: React.ReactNode;
102
105
  }
103
106
 
104
107
  export function ReactHijacker(props: ReactHijackerProps): React.ReactElement | null;
108
+ export function __HijackNode(props: { children: () => React.ReactElement | null }): React.ReactElement | null;
109
+
110
+ export function createHijackedElement(element?: React.ReactElement | null): { __element: React.ReactElement | null };
105
111
 
106
112
  export function isDodgeScrollable(element: React.ReactNode, disableTagCheck?: boolean): boolean;
107
- export function isDodgeInput(element: React.ReactNode, disableTagCheck?: boolean): boolean;
113
+ export function isDodgeInput(element: React.ReactNode, disableTagCheck?: boolean): boolean;
114
+
115
+ interface KeyboardPlaceholderProps {
116
+ doHeight: (keyboardheight: number) => number;
117
+ }
118
+
119
+ export function KeyboardPlaceholderView(props: KeyboardPlaceholderProps): React.ReactElement | null;
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Children, cloneElement, createElement, forwardRef, isValidElement, memo, useEffect, useMemo, useRef, useState } from "react";
2
- import { Dimensions, findNodeHandle, Keyboard, StyleSheet, UIManager } from "react-native";
2
+ import { Animated, Dimensions, findNodeHandle, Keyboard, Platform, StyleSheet, UIManager, useAnimatedValue } from "react-native";
3
3
 
4
4
  export default function ({ children, offset = 10, disabled, onHandleDodging, disableTagCheck, checkIfElementIsFocused }) {
5
5
  if (checkIfElementIsFocused !== undefined) {
@@ -7,7 +7,7 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
7
7
  throw 'checkIfElementIsFocused should be a function';
8
8
 
9
9
  checkIfElementIsFocused = niceFunction(checkIfElementIsFocused, 'checkIfElementIsFocused');
10
- } else checkIfElementIsFocused = r => r?.isFocused?.();
10
+ }
11
11
 
12
12
  if (onHandleDodging !== undefined) {
13
13
  if (typeof onHandleDodging !== 'function')
@@ -24,91 +24,156 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
24
24
  * @type {import("react").RefObject<{[key: string]: { __is_standalone: boolean, _standalone_props: { dodge_keyboard_offset?: number, dodge_keyboard_lift?: boolean }, scrollRef: import("react-native").ScrollView, inputRef: {[key: string]: { ref: import("react-native").TextInput, props: { dodge_keyboard_offset?: number, dodge_keyboard_lift?: boolean } }} }}>}
25
25
  */
26
26
  const viewRefsMap = useRef({});
27
- const isKeyboardVisible = useRef();
28
27
  const doDodgeKeyboard = useRef();
29
28
  const previousLift = useRef();
29
+ const wasVisible = useRef();
30
+ const pendingIdleTask = useRef();
31
+ const lastKeyboardEvent = useRef();
30
32
 
31
33
  const clearPreviousDodge = (scrollId) => {
32
34
  if (previousLift.current && previousLift.current !== scrollId) {
33
35
  const viewRef = viewRefsMap.current[previousLift.current]?.scrollRef;
34
- onHandleDodging?.({ liftUp: 0, viewRef: viewRef || null });
36
+ onHandleDodging?.({
37
+ liftUp: 0,
38
+ viewRef: viewRef || null,
39
+ keyboardEvent: lastKeyboardEvent.current
40
+ });
35
41
  previousLift.current = undefined;
36
42
  }
37
43
  }
38
44
 
39
- doDodgeKeyboard.current = () => {
45
+ /**
46
+ * @param {import('react-native').KeyboardEvent | undefined} event
47
+ * @param {boolean} visible
48
+ * @param {boolean} fromIdle
49
+ */
50
+ doDodgeKeyboard.current = (event, visible, fromIdle) => {
51
+ if (Platform.OS === 'ios' && event && !event?.isEventFromThisApp) return;
52
+
53
+ if (typeof visible !== 'boolean') {
54
+ if (typeof wasVisible.current === 'boolean') {
55
+ visible = wasVisible.current;
56
+ } else return;
57
+ }
58
+
59
+ wasVisible.current = visible;
60
+ if (event) lastKeyboardEvent.current = event;
61
+
40
62
  try {
41
- const keyboardInfo = Keyboard.metrics();
42
- const { width: windowWidth, height: windowHeight } = Dimensions.get('window');
63
+ const keyboardInfo = event?.endCoordinates || Keyboard.metrics();
43
64
 
44
65
  // console.log('doDodgeKeyboard');
66
+
67
+ if (pendingIdleTask.current !== undefined)
68
+ cancelIdleCallback(pendingIdleTask.current);
69
+ pendingIdleTask.current = undefined;
70
+
45
71
  if (
46
- isKeyboardVisible.current &&
72
+ visible &&
47
73
  keyboardInfo &&
48
74
  !disabled &&
49
- (keyboardInfo.width === windowWidth ||
50
- keyboardInfo.height + keyboardInfo.screenY === windowHeight) &&
75
+ (!isOffScreenY(keyboardInfo) && !isOffScreenX(keyboardInfo)) &&
51
76
  keyboardInfo.screenY
52
77
  ) {
53
78
  // console.log('doDodgeKeyboard 1 entries:', Object.keys(viewRefsMap.current).length);
54
- for (const [scrollId, obj] of Object.entries(viewRefsMap.current)) {
79
+ const itemIteList = Object.entries(viewRefsMap.current);
80
+ const allInputList = checkIfElementIsFocused && itemIteList.map(v =>
81
+ v[1].__is_standalone
82
+ ? [v[1].scrollRef]
83
+ : Object.values(v[1].inputRef).map(v => v.ref)
84
+ ).flat();
85
+
86
+ const initIdleTask = () => {
87
+ if (!fromIdle)
88
+ pendingIdleTask.current = requestIdleCallback(() => {
89
+ doDodgeKeyboard.current(undefined, undefined, true);
90
+ });
91
+ }
92
+
93
+ const checkFocused = checkIfElementIsFocused || (r => r?.isFocused?.());
94
+
95
+ for (const [scrollId, obj] of itemIteList) {
55
96
  const { scrollRef, inputRef, __is_standalone, _standalone_props } = obj;
56
97
  if (scrollRef) {
57
98
  if (__is_standalone) {
58
- if (checkIfElementIsFocused(scrollRef)) {
59
- if (scrollRef.measureInWindow)
60
- UIManager.measureInWindow(findNodeHandle(scrollRef), (x, y, w, h) => {
61
- const { dodge_keyboard_offset } = _standalone_props || {};
62
- const thisOffset = isNumber(dodge_keyboard_offset) ? dodge_keyboard_offset : offset;
63
-
64
- const liftUp = Math.max(0, (y - keyboardInfo.screenY) + Math.min(h + thisOffset, keyboardInfo.screenY));
65
- clearPreviousDodge(scrollId);
66
- if (liftUp) {
67
- previousLift.current = scrollId;
68
- onHandleDodging?.({ liftUp, viewRef: scrollRef });
69
- }
70
- });
99
+ if (checkFocused(scrollRef, allInputList)) {
100
+ UIManager.measure(findNodeHandle(scrollRef), (_x, _y, w, h, x, y) => {
101
+ const { dodge_keyboard_offset } = _standalone_props || {};
102
+ const thisOffset = isNumber(dodge_keyboard_offset) ? dodge_keyboard_offset : offset;
103
+
104
+ const liftUp = Math.max(0, (y - keyboardInfo.screenY) + Math.min(h + thisOffset, keyboardInfo.screenY));
105
+ clearPreviousDodge(scrollId);
106
+ if (liftUp) {
107
+ previousLift.current = scrollId;
108
+ onHandleDodging?.({
109
+ liftUp,
110
+ viewRef: scrollRef,
111
+ keyboardEvent: lastKeyboardEvent.current
112
+ });
113
+ }
114
+ initIdleTask();
115
+ });
71
116
  return;
72
117
  }
73
118
  } else {
74
119
  for (const { ref: inputObj, props } of Object.values(inputRef)) {
75
- if (checkIfElementIsFocused(inputObj)) {
76
- UIManager.measureInWindow(findNodeHandle(scrollRef), ((sx, sy, sw, sh) => {
77
- inputObj.measureInWindow((x, y) => { // y is dynamic
120
+ if (checkFocused(inputObj, allInputList)) {
121
+ Promise.all([
122
+ new Promise(resolve => {
123
+ UIManager.measure(findNodeHandle(scrollRef), (x, y, w, h, px, py) => {
124
+ resolve({ h, py });
125
+ });
126
+ }),
127
+ new Promise(resolve => {
128
+ inputObj.measure((x, y, w, h, px, py) => { // y is dynamic
129
+ resolve({ py });
130
+ });
131
+ }),
132
+ new Promise((resolve, reject) => {
78
133
  inputObj.measureLayout(scrollRef, (l, t, w, h) => { // t is fixed
79
- const { dodge_keyboard_offset } = props || {};
80
- const thisOffset = isNumber(dodge_keyboard_offset) ? dodge_keyboard_offset : offset;
81
-
82
- const scrollInputY = y - sy;
83
-
84
- if (scrollInputY >= 0 && scrollInputY <= sh) { // is input visible in viewport
85
- const clampedLift = Math.min(h + thisOffset, keyboardInfo.screenY);
86
-
87
- if (y + clampedLift >= keyboardInfo.screenY) { // is below keyboard
88
- const requiredScrollY = (t - (keyboardInfo.screenY - sy)) + clampedLift;
89
- // for lifting up the scroll-view
90
- const liftUp = Math.max(0, requiredScrollY - t);
91
- clearPreviousDodge(scrollId);
92
- if (liftUp) {
93
- previousLift.current = scrollId;
94
- onHandleDodging?.({ liftUp, viewRef: scrollRef });
95
- }
96
-
97
- const scrollLift = Math.max(0, (sy + sh + (thisOffset >= 0 ? thisOffset : 0)) - keyboardInfo.screenY);
98
- const newScrollY = Math.min(requiredScrollY, t);
99
-
100
- // console.log('scrolling-to:', requiredScrollY, ' scrollLift:', scrollLift);
101
- if (scrollLift) {
102
- setCurrentPaddedScroller([scrollId, scrollLift, newScrollY]);
103
- } else {
104
- scrollRef.scrollTo({ y: newScrollY, animated: true });
105
- setCurrentPaddedScroller();
106
- }
107
- }
134
+ resolve({ t, h })
135
+ }, reject);
136
+ })
137
+ ]).then(([{ h: sh, py: sy }, { py: y }, { t, h }]) => {
138
+
139
+ const { dodge_keyboard_offset } = props || {};
140
+ const thisOffset = isNumber(dodge_keyboard_offset) ? dodge_keyboard_offset : offset;
141
+
142
+ const scrollInputY = y - sy;
143
+
144
+ if (scrollInputY >= 0 && scrollInputY <= sh) { // is input visible in viewport
145
+ const clampedLift = Math.min(h + thisOffset, keyboardInfo.screenY);
146
+
147
+ if (y + clampedLift >= keyboardInfo.screenY) { // is below keyboard
148
+ const requiredScrollY = (t - (keyboardInfo.screenY - sy)) + clampedLift;
149
+ // for lifting up the scroll-view
150
+ const liftUp = Math.max(0, requiredScrollY - t);
151
+ clearPreviousDodge(scrollId);
152
+ if (liftUp) {
153
+ previousLift.current = scrollId;
154
+ onHandleDodging?.({
155
+ liftUp,
156
+ viewRef: scrollRef,
157
+ keyboardEvent: lastKeyboardEvent.current
158
+ });
108
159
  }
109
- });
110
- });
111
- }));
160
+
161
+ const scrollLift = Math.max(0, (sy + sh + (thisOffset >= 0 ? thisOffset : 0)) - keyboardInfo.screenY);
162
+ const newScrollY = Math.min(requiredScrollY, t);
163
+
164
+ // console.log('scrolling-to:', requiredScrollY, ' scrollLift:', scrollLift);
165
+ if (scrollLift) {
166
+ setCurrentPaddedScroller([scrollId, scrollLift, newScrollY]);
167
+ } else {
168
+ tryPerformScroll(scrollRef, newScrollY, true);
169
+ setCurrentPaddedScroller();
170
+ }
171
+ initIdleTask();
172
+ }
173
+ }
174
+ }).catch(e => {
175
+ console.error('frame calculation error:', e);
176
+ });
112
177
  return;
113
178
  }
114
179
  }
@@ -124,20 +189,24 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
124
189
  }
125
190
  }
126
191
 
192
+ const tryPerformScroll = (ref, y, animated = true) => {
193
+ if (!ref) return;
194
+
195
+ if (ref.scrollTo) {
196
+ ref.scrollTo?.({ y, animated });
197
+ } else if (ref.scrollToOffset) {
198
+ ref.scrollToOffset?.({ offset: y, animated });
199
+ } else {
200
+ ref.getScrollResponder?.()?.scrollTo?.({ y, animated });
201
+ }
202
+ }
203
+
127
204
  const [paddedId, paddedSize, paddedScroll] = currentPaddedScroller || [];
128
205
 
129
206
  useEffect(() => {
130
207
  if (currentPaddedScroller) {
131
208
  const ref = viewRefsMap.current[paddedId]?.scrollRef;
132
- if (ref) {
133
- if (ref.scrollTo) {
134
- ref.scrollTo?.({ y: paddedScroll, animated: true });
135
- } else if (ref.scrollToOffset) {
136
- ref.scrollToOffset?.({ offset: paddedScroll, animated: true });
137
- } else {
138
- ref.getScrollResponder?.()?.scrollTo?.({ y: paddedScroll, animated: true });
139
- }
140
- }
209
+ tryPerformScroll(ref, paddedScroll, false);
141
210
  }
142
211
  }, [currentPaddedScroller]);
143
212
 
@@ -147,19 +216,15 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
147
216
 
148
217
  useEffect(() => {
149
218
  if (disabled) return;
150
- const frameListener = Keyboard.addListener('keyboardDidChangeFrame', e => {
151
- doDodgeKeyboard.current();
152
- });
153
-
154
- const showListener = Keyboard.addListener('keyboardDidShow', e => {
155
- isKeyboardVisible.current = true;
156
- doDodgeKeyboard.current();
157
- });
158
-
159
- const hiddenListener = Keyboard.addListener('keyboardDidHide', e => {
160
- isKeyboardVisible.current = false;
161
- doDodgeKeyboard.current();
162
- });
219
+ const frameListener = Keyboard.addListener('keyboardDidChangeFrame', e => doDodgeKeyboard.current(e));
220
+ const showListener = Keyboard.addListener(
221
+ Platform.OS === 'android' ? 'keyboardDidShow' : 'keyboardWillShow',
222
+ e => doDodgeKeyboard.current(e, true)
223
+ );
224
+ const hiddenListener = Keyboard.addListener(
225
+ Platform.OS === 'android' ? 'keyboardDidHide' : 'keyboardWillHide',
226
+ e => doDodgeKeyboard.current(e, false)
227
+ );
163
228
 
164
229
  return () => {
165
230
  frameListener.remove();
@@ -168,145 +233,164 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
168
233
  }
169
234
  }, [!disabled]);
170
235
 
171
- return (
172
- <ReactHijacker
173
- doHijack={(node, path) => {
174
- if (node?.props?.['dodge_keyboard_scan_off']) return { element: node };
175
-
176
- const isStandalone = isDodgeInput(node);
177
-
178
- if (isStandalone || isDodgeScrollable(node, disableTagCheck)) {
179
- const scrollId = path.join('=>');
180
- const initNode = () => {
181
- if (!viewRefsMap.current[scrollId])
182
- viewRefsMap.current[scrollId] = { inputRef: {} };
183
-
184
- if (isStandalone) {
185
- viewRefsMap.current[scrollId].__is_standalone = true;
186
- viewRefsMap.current[scrollId]._standalone_props = {
187
- dodge_keyboard_offset: node.props?.dodge_keyboard_offset,
188
- dodge_keyboard_lift: node.props?.dodge_keyboard_lift
189
- };
190
- }
191
- }
192
- const shouldPad = scrollId === paddedId;
193
- const contentStyle = shouldPad && StyleSheet.flatten(node.props?.contentContainerStyle);
194
- const rootRenderItem = node.prop?.renderItem;
195
- const rootKeyExtractor = node.prop?.keyExtractor;
196
- const hasInternalList = !isStandalone && (typeof rootRenderItem === 'function' && !node.props?.children);
197
-
198
- const doRefCleanup = () => {
199
- if (
200
- viewRefsMap.current[scrollId]?.scrollRef ||
201
- Object.keys(viewRefsMap.current[scrollId]?.inputRef || {}).length
202
- ) return;
203
- delete viewRefsMap.current[scrollId];
204
- }
236
+ const nodeIdIte = useRef(0);
205
237
 
206
- const injectChild = (children, childPath) =>
207
- ReactHijacker({
208
- children,
209
- path: childPath,
210
- doHijack: (inputNode, path) => {
211
- if (isDodgeInput(inputNode, disableTagCheck)) {
212
- const inputId = path.join('=>');
213
- const initInputNode = () => {
214
- initNode();
215
- if (!viewRefsMap.current[scrollId].inputRef[inputId])
216
- viewRefsMap.current[scrollId].inputRef[inputId] = {};
217
- viewRefsMap.current[scrollId].inputRef[inputId].props = {
218
- dodge_keyboard_offset: inputNode.props?.dodge_keyboard_offset,
219
- dodge_keyboard_lift: inputNode.props?.dodge_keyboard_lift
220
- };
221
- }
238
+ const onHijackNode = node => {
239
+ if (offDodgeScan(node)) return createHijackedElement(node);
222
240
 
223
- initInputNode();
224
-
225
- return {
226
- props: {
227
- ...inputNode.props,
228
- onFocus: (...args) => {
229
- doDodgeKeyboard.current();
230
- return inputNode.props?.onFocus?.(...args);
231
- },
232
- onLayout: (...args) => {
233
- doDodgeKeyboard.current();
234
- return inputNode.props?.onLayout?.(...args);
235
- },
236
- ref: r => {
237
- if (r) {
238
- initInputNode();
239
-
240
- viewRefsMap.current[scrollId].inputRef[inputId].ref = r;
241
- } else if (viewRefsMap.current[scrollId]?.inputRef?.[inputId]) {
242
- delete viewRefsMap.current[scrollId].inputRef[inputId];
243
- doRefCleanup();
244
- }
241
+ const isStandalone = isDodgeInput(node);
242
+ if (!isStandalone && !isDodgeScrollable(node, disableTagCheck)) return;
245
243
 
246
- const thatRef = inputNode.props?.ref;
247
- if (typeof thatRef === 'function') {
248
- thatRef(r);
249
- } else if (thatRef) thatRef.current = r;
250
- }
251
- }
252
- };
253
- }
254
- }
255
- });
244
+ const renderer = () => {
245
+ const scrollId = useMemo(() => `${++nodeIdIte.current}`, []);
256
246
 
257
- return {
258
- props: {
259
- ...node.props,
260
- ...shouldPad ? {
261
- contentContainerStyle: {
262
- ...contentStyle,
263
- paddingBottom: paddedSize + (isNumber(contentStyle?.paddingBottom) ? contentStyle.paddingBottom : 0)
264
- }
265
- } : {},
266
- ref: r => {
267
- if (r) {
268
- initNode();
269
- viewRefsMap.current[scrollId].scrollRef = r;
270
- } else if (viewRefsMap.current[scrollId]) {
271
- viewRefsMap.current[scrollId].scrollRef = undefined;
272
- doRefCleanup();
273
- }
247
+ const initNode = () => {
248
+ if (!viewRefsMap.current[scrollId])
249
+ viewRefsMap.current[scrollId] = { inputRef: {} };
274
250
 
275
- const thatRef = node.props?.ref;
276
- if (typeof thatRef === 'function') {
277
- thatRef(r);
278
- } else if (thatRef) thatRef.current = r;
279
- },
280
- ...isStandalone ? {
281
- onFocus: (...args) => {
282
- doDodgeKeyboard.current();
283
- return node.props?.onFocus?.(...args);
284
- }
285
- } : {},
286
- onLayout: (...args) => {
287
- doDodgeKeyboard.current();
288
- return node.props?.onLayout?.(...args);
289
- },
290
- ...isStandalone ? {} :
291
- hasInternalList ? {
292
- renderItem: (...args) => {
293
- const { item, index } = args[0] || {};
294
-
295
- return injectChild(
296
- rootRenderItem(...args),
297
- [
298
- ...path,
299
- index,
300
- ...typeof rootKeyExtractor === 'function' ?
301
- [rootKeyExtractor?.(item, index)] : []
302
- ]
303
- );
304
- }
305
- } : { children: injectChild(node.props?.children, path) }
251
+ if (isStandalone) {
252
+ viewRefsMap.current[scrollId].__is_standalone = true;
253
+ viewRefsMap.current[scrollId]._standalone_props = {
254
+ dodge_keyboard_offset: node.props?.dodge_keyboard_offset,
255
+ dodge_keyboard_lift: node.props?.dodge_keyboard_lift
256
+ };
257
+ }
258
+ }
259
+ const shouldPad = !isStandalone && scrollId === paddedId;
260
+ const contentStyle = shouldPad && StyleSheet.flatten(node.props?.contentContainerStyle);
261
+ const rootRenderItem = node.props?.renderItem;
262
+ const hasInternalList = !isStandalone && (typeof rootRenderItem === 'function' && !node.props?.children);
263
+
264
+ const doRefCleanup = () => {
265
+ if (
266
+ viewRefsMap.current[scrollId]?.scrollRef ||
267
+ Object.keys(viewRefsMap.current[scrollId]?.inputRef || {}).length
268
+ ) return;
269
+ delete viewRefsMap.current[scrollId];
270
+ }
271
+
272
+ const injectChild = inputNode => {
273
+ if (offDodgeScan(inputNode)) return createHijackedElement(inputNode);
274
+
275
+ if (!isDodgeInput(inputNode, disableTagCheck)) return;
276
+
277
+ const inputRenderer = () => {
278
+ const inputId = useMemo(() => `${++nodeIdIte.current}`, []);
279
+ const initInputNode = () => {
280
+ initNode();
281
+ if (!viewRefsMap.current[scrollId].inputRef[inputId])
282
+ viewRefsMap.current[scrollId].inputRef[inputId] = {};
283
+ viewRefsMap.current[scrollId].inputRef[inputId].props = {
284
+ dodge_keyboard_offset: inputNode.props?.dodge_keyboard_offset,
285
+ dodge_keyboard_lift: inputNode.props?.dodge_keyboard_lift
286
+ };
287
+ }
288
+
289
+ initInputNode();
290
+
291
+ const newProps = {
292
+ ...inputNode.props,
293
+ __dodging_keyboard: true,
294
+ onFocus: (...args) => {
295
+ doDodgeKeyboard.current();
296
+ return inputNode.props?.onFocus?.(...args);
297
+ },
298
+ onLayout: (...args) => {
299
+ doDodgeKeyboard.current();
300
+ return inputNode.props?.onLayout?.(...args);
301
+ },
302
+ ref: r => {
303
+ if (r) {
304
+ initInputNode();
305
+
306
+ viewRefsMap.current[scrollId].inputRef[inputId].ref = r;
307
+ } else if (viewRefsMap.current[scrollId]?.inputRef?.[inputId]) {
308
+ delete viewRefsMap.current[scrollId].inputRef[inputId];
309
+ doRefCleanup();
310
+ }
311
+
312
+ const thatRef = inputNode.props?.ref;
313
+ if (typeof thatRef === 'function') {
314
+ thatRef(r);
315
+ } else if (thatRef) thatRef.current = r;
306
316
  }
307
317
  };
318
+
319
+ return cloneElement(inputNode, newProps);
308
320
  }
309
- }}>
321
+
322
+ return createHijackedElement(
323
+ <__HijackNode>
324
+ {inputRenderer}
325
+ </__HijackNode>
326
+ );
327
+ }
328
+
329
+ const newProps = {
330
+ ...node.props,
331
+ __dodging_keyboard: true,
332
+ ...shouldPad ? {
333
+ contentContainerStyle: {
334
+ ...contentStyle,
335
+ paddingBottom: paddedSize + (isNumber(contentStyle?.paddingBottom) ? contentStyle.paddingBottom : 0)
336
+ }
337
+ } : {},
338
+ ref: r => {
339
+ if (r) {
340
+ initNode();
341
+ viewRefsMap.current[scrollId].scrollRef = r;
342
+ } else if (viewRefsMap.current[scrollId]) {
343
+ viewRefsMap.current[scrollId].scrollRef = undefined;
344
+ doRefCleanup();
345
+ }
346
+
347
+ const thatRef = node.props?.ref;
348
+ if (typeof thatRef === 'function') {
349
+ thatRef(r);
350
+ } else if (thatRef) thatRef.current = r;
351
+ },
352
+ ...isStandalone ? {
353
+ onFocus: (...args) => {
354
+ doDodgeKeyboard.current();
355
+ return node.props?.onFocus?.(...args);
356
+ }
357
+ } : {},
358
+ onLayout: (...args) => {
359
+ doDodgeKeyboard.current();
360
+ return node.props?.onLayout?.(...args);
361
+ },
362
+ ...isStandalone ? {} :
363
+ hasInternalList ? {
364
+ renderItem: (...args) => {
365
+ return (
366
+ <ReactHijacker
367
+ doHijack={injectChild}>
368
+ {rootRenderItem(...args)}
369
+ </ReactHijacker>
370
+ );
371
+ }
372
+ } : {
373
+ children:
374
+ ReactHijacker({
375
+ children: node.props?.children,
376
+ doHijack: injectChild
377
+ })
378
+ }
379
+ };
380
+
381
+ return cloneElement(node, newProps);
382
+ }
383
+
384
+ return createHijackedElement(
385
+ <__HijackNode>
386
+ {renderer}
387
+ </__HijackNode>
388
+ );
389
+ };
390
+
391
+ return (
392
+ <ReactHijacker
393
+ doHijack={onHijackNode}>
310
394
  {children}
311
395
  </ReactHijacker>
312
396
  );
@@ -329,62 +413,43 @@ const REACT_SYMBOLS = {
329
413
  memo: Symbol.for('react.memo')
330
414
  };
331
415
 
332
- export function ReactHijacker({ children, doHijack, path }) {
333
- const renderRefs = useMemo(() => new Map(), []);
334
-
416
+ export function ReactHijacker({ children, doHijack, enableLocator }) {
335
417
  const instantDoHijack = useRef();
336
418
  instantDoHijack.current = doHijack;
337
419
 
338
- const injectIntoTree = (node, path = [], arrayIndex) => {
420
+ const injectIntoTree = (node, path) => {
339
421
  if (!node) return node;
340
422
  if (Array.isArray(node)) {
341
- path = [...path, ...arrayIndex === undefined ? [0] : [arrayIndex]];
342
- return Children.map(node, (v, i) => injectIntoTree(v, path, i));
423
+ return Children.map(node, (v, i) => injectIntoTree(v, path && [...path, i]));
343
424
  }
344
425
  if (!isValidElement(node)) return node;
345
426
 
346
- path = [...path, ...arrayIndex === undefined ? [0] : [arrayIndex], getNodeId(node)];
427
+ if (path) path = [...path, getNodeId(node)];
347
428
 
348
429
  let thisObj;
349
- if (thisObj = instantDoHijack.current?.(node, path)) {
350
- const { element, props } = thisObj;
351
-
352
- if (Object.hasOwn(thisObj, 'element')) return element;
353
- if (props) return cloneElement(node, props);
354
- return node;
430
+ if (Object.hasOwn((thisObj = instantDoHijack.current?.(node, path)) || {}, '__element')) {
431
+ return thisObj.__element;
355
432
  }
356
433
 
357
434
  if (!isHostElement(node)) {
358
435
  const wrapNodeType = (nodeType, pathway, pathKey) => {
359
- pathway = [...pathway, getNodeId(undefined, nodeType, pathKey)];
360
- const path_id = pathway.join(',');
361
- let renderRefStore = renderRefs.get(nodeType);
362
-
363
- if (renderRefStore?.[path_id]) return renderRefStore[path_id];
436
+ if (pathway) pathway = [...pathway, getNodeId(undefined, nodeType, pathKey)];
364
437
 
365
438
  // if (doLogging) console.log('wrapNodeType path:', pathway, ' node:', nodeType);
366
439
  const render = (renderedNode) => {
367
440
  // if (doLogging) console.log('deep path:', pathway, ' node:', renderedNode);
368
- return injectIntoTree(renderedNode, pathway);
441
+ return injectIntoTree(renderedNode, pathway && [...pathway, 0]);
369
442
  }
370
443
 
371
- let newType;
372
-
373
444
  if (typeof nodeType === 'function') { // check self closed tag
374
- newType = hijackRender(nodeType, render);
445
+ return hijackRender(nodeType, render);
375
446
  } else if (nodeType?.$$typeof === REACT_SYMBOLS.forwardRef) {
376
- newType = forwardRef(hijackRender(nodeType.render, render));
447
+ return forwardRef(hijackRender(nodeType.render, render));
377
448
  } else if (nodeType?.$$typeof === REACT_SYMBOLS.memo) {
378
- newType = memo(wrapNodeType(nodeType.type, pathway), nodeType.compare);
449
+ const newType = memo(wrapNodeType(nodeType.type, pathway), nodeType.compare);
379
450
  newType.displayName = nodeType.displayName || nodeType.name;
380
- }
381
-
382
- if (newType) {
383
- if (!renderRefStore) renderRefs.set(nodeType, renderRefStore = {});
384
- renderRefStore[path_id] = newType;
385
451
  return newType;
386
452
  }
387
-
388
453
  return nodeType;
389
454
  }
390
455
 
@@ -394,15 +459,24 @@ export function ReactHijacker({ children, doHijack, path }) {
394
459
  node.type?.$$typeof === REACT_SYMBOLS.memo // check memo
395
460
  ) {
396
461
  // if (doLogging) console.log('doLog path:', path, ' node:', node);
397
- const injectedType = wrapNodeType(node.type, path.slice(0, -1), node.key);
398
- return createElement(
399
- injectedType,
400
- {
401
- ...node.props,
402
- key: node.key,
403
- // ...isForwardRef ? { ref: node.ref } : {}
404
- },
405
- node.props?.children
462
+ return (
463
+ <__HijackNodePath>
464
+ {() => {
465
+ const hijackType = useMemo(() =>
466
+ wrapNodeType(node.type, path && path.slice(0, -1), node.key),
467
+ [node.type]
468
+ );
469
+
470
+ return createElement(
471
+ hijackType,
472
+ {
473
+ ...node.props,
474
+ key: node.key
475
+ },
476
+ node.props?.children
477
+ );
478
+ }}
479
+ </__HijackNodePath>
406
480
  );
407
481
  }
408
482
  }
@@ -410,15 +484,24 @@ export function ReactHijacker({ children, doHijack, path }) {
410
484
  const children = node.props?.children;
411
485
  if (children)
412
486
  return cloneElement(node, {
413
- children: injectIntoTree(children, path)
487
+ children: injectIntoTree(children, path && [...path, 0])
414
488
  });
415
489
 
416
490
  return node;
417
491
  };
418
492
 
419
- return injectIntoTree(children, path);
493
+ return injectIntoTree(children, enableLocator ? [] : undefined);
420
494
  };
421
495
 
496
+ export const createHijackedElement = (element) => ({ __element: element });
497
+ export function __HijackNode({ children }) {
498
+ return children?.();
499
+ }
500
+
501
+ function __HijackNodePath({ children }) {
502
+ return children?.();
503
+ }
504
+
422
505
  const hijackRender = (type, doHijack) =>
423
506
  new Proxy(type, {
424
507
  apply(target, thisArg, args) {
@@ -474,6 +557,8 @@ export function isHostElement(node) {
474
557
  );
475
558
  }
476
559
 
560
+ const offDodgeScan = (node) => node?.props?.dodge_keyboard_scan_off || node?.props?.__dodging_keyboard;
561
+
477
562
  export const isDodgeScrollable = (element, disableTagCheck) => {
478
563
  if (element?.props?.['dodge_keyboard_scrollable']) return true;
479
564
  if (!element?.type || element?.props?.horizontal || disableTagCheck) return false;
@@ -492,4 +577,83 @@ export const isDodgeInput = (element, disableTagCheck) => {
492
577
 
493
578
  return inputTypes.includes(element.type?.displayName)
494
579
  || inputTypes.includes(element.type?.name);
495
- };
580
+ };
581
+
582
+ const isOffScreenY = (keyboardInfo, minimumThreshold = .27) =>
583
+ !keyboardInfo ||
584
+ keyboardInfo.height <= 0 ||
585
+ ((keyboardInfo.screenY / (keyboardInfo.screenY + keyboardInfo.height)) < .40) ||
586
+ (keyboardInfo.screenY / Dimensions.get('window').height) < minimumThreshold;
587
+
588
+ const isOffScreenX = (keyboardInfo, minimumThreshold = .7) => {
589
+ const vw = Dimensions.get('window').width;
590
+
591
+ return !keyboardInfo ||
592
+ (Math.min(keyboardInfo.width, vw) / Math.max(keyboardInfo.width, vw)) < minimumThreshold;
593
+ }
594
+
595
+ export const KeyboardPlaceholderView = ({ doHeight }) => {
596
+ const height = useAnimatedValue(0);
597
+
598
+ const instantDoHeight = useRef();
599
+ instantDoHeight.current = doHeight;
600
+
601
+ useEffect(() => {
602
+ let wasVisible;
603
+ /**
604
+ * @param {import('react-native').KeyboardEvent} event
605
+ * @param {boolean} visible
606
+ */
607
+ const updateKeyboardHeight = (event, visible) => {
608
+ if (typeof visible !== 'boolean') {
609
+ if (typeof wasVisible === 'boolean') {
610
+ visible = wasVisible;
611
+ } else return;
612
+ }
613
+
614
+ wasVisible = visible;
615
+
616
+ const { endCoordinates, isEventFromThisApp, duration } = event;
617
+ if (Platform.OS === 'ios' && !isEventFromThisApp) return;
618
+
619
+ const kh = (visible && !isOffScreenX(endCoordinates) && !isOffScreenY(endCoordinates, .3))
620
+ ? Math.max(Dimensions.get('window').height - endCoordinates.screenY, 0)
621
+ : 0;
622
+
623
+ const newHeight = Math.max(0, instantDoHeight.current ? instantDoHeight.current(kh) : kh);
624
+ const newDuration = (Math.abs(height._value - newHeight) * duration) / Math.max(0, endCoordinates.height);
625
+
626
+ Animated.timing(height, {
627
+ duration: newDuration || 0,
628
+ toValue: newHeight,
629
+ useNativeDriver: false
630
+ }).start();
631
+ }
632
+
633
+ const initialMetric = Keyboard.metrics();
634
+ if (initialMetric)
635
+ updateKeyboardHeight({
636
+ endCoordinates: initialMetric,
637
+ isEventFromThisApp: true,
638
+ duration: 0
639
+ }, true);
640
+
641
+ const frameListener = Keyboard.addListener('keyboardDidChangeFrame', e => updateKeyboardHeight(e));
642
+ const showListener = Keyboard.addListener(
643
+ Platform.OS === 'android' ? 'keyboardDidShow' : 'keyboardWillShow',
644
+ e => updateKeyboardHeight(e, true)
645
+ );
646
+ const hiddenListener = Keyboard.addListener(
647
+ Platform.OS === 'android' ? 'keyboardDidHide' : 'keyboardWillHide',
648
+ e => updateKeyboardHeight(e, false)
649
+ );
650
+
651
+ return () => {
652
+ frameListener.remove();
653
+ showListener.remove();
654
+ hiddenListener.remove();
655
+ }
656
+ }, []);
657
+
658
+ return <Animated.View style={{ height }} />;
659
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-dodge-keyboard",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "react-native-dodge-keyboard is a tiny library designed to flawlessly move your UI out of the way of the keyboard",
5
5
  "keywords": [
6
6
  "react-native",
package/TODO DELETED
@@ -1 +0,0 @@
1
- keyboard not dodging in state changes