react-native-dodge-keyboard 1.0.0 → 1.0.2

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/TODO +1 -0
  2. package/index.d.ts +22 -15
  3. package/index.js +355 -166
  4. package/package.json +1 -1
package/TODO ADDED
@@ -0,0 +1 @@
1
+ keyboard not dodging in state changes
package/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { ScrollView } from "react-native";
2
+ import { ScrollView, View } from "react-native";
3
3
 
4
4
  export interface LiftUpDodge {
5
5
  /**
@@ -9,10 +9,11 @@ export interface LiftUpDodge {
9
9
  liftUp: number;
10
10
 
11
11
  /**
12
- * A reference to the scrollable view that should be lifted.
13
- * This should be a ref to a ScrollView, FlatList, or any custom scrollable view.
12
+ * A reference to the view that should be lifted.
13
+ *
14
+ * null is returned if the view has been recently removed from the node hierarchy
14
15
  */
15
- viewRef: ScrollView;
16
+ viewRef: ScrollView | View | null;
16
17
  }
17
18
 
18
19
  export interface DodgeKeyboardProps {
@@ -49,22 +50,25 @@ export interface DodgeKeyboardProps {
49
50
  * - "VirtualizedList"
50
51
  *
51
52
  * If you want a custom scrollable element to support dodging,
52
- * add the prop: `dodge-keyboard-scrollview={true}`.
53
+ * add the prop: `dodge_keyboard_scrollable={true}`.
53
54
  *
54
55
  * By default, "TextInput" is the only known input tag.
55
56
  * To enable dodging for a custom input element,
56
- * add the prop: `dodge-keyboard-input={true}`.
57
+ * add the prop: `dodge_keyboard_input={true}`.
58
+ *
59
+ * Input elements or views with dodge_keyboard_input={true} that are not inside a scrollable view must be manually lifted by responding to the `onHandleDodging` callback.
57
60
  */
58
61
  disableTagCheck?: boolean;
59
62
 
60
63
  /**
61
- * If provided, this prevents ALL other input views from dodging the keyboard
62
- * except the one with the matching `dodge-keyboard-focus-id` prop.
63
- *
64
- * This is useful when trying to dodge a non-input view
65
- * or when you want strict control over which view should move.
64
+ * an handler used internally for checking if a view is focused
65
+ *
66
+ * @default
67
+ * ```js
68
+ * r => r?.isFocused?.()
69
+ * ```
66
70
  */
67
- forceDodgeFocusId?: string;
71
+ checkIfElementIsFocused?: (ref: View) => boolean;
68
72
 
69
73
  /**
70
74
  * Child element(s) wrapped by the dodge container.
@@ -82,9 +86,9 @@ export default function DodgeKeyboard(
82
86
 
83
87
  interface doHijackResult {
84
88
  /**
85
- * continue crawling other react node hierarchy.
89
+ * element to be replace with, providing this basically ignores the `props` field
86
90
  */
87
- persist?: boolean;
91
+ element?: boolean;
88
92
  /**
89
93
  * props injected into the hijacked node.
90
94
  */
@@ -97,4 +101,7 @@ interface ReactHijackerProps {
97
101
  children?: React.ReactNode;
98
102
  }
99
103
 
100
- export function ReactHijacker(props: ReactHijackerProps): React.ReactElement | null;
104
+ export function ReactHijacker(props: ReactHijackerProps): React.ReactElement | null;
105
+
106
+ export function isDodgeScrollable(element: React.ReactNode, disableTagCheck?: boolean): boolean;
107
+ export function isDodgeInput(element: React.ReactNode, disableTagCheck?: boolean): boolean;
package/index.js CHANGED
@@ -1,11 +1,19 @@
1
- import React, { cloneElement, useEffect, useRef, useState } from "react";
2
- import { Dimensions, Keyboard, StyleSheet } from "react-native";
1
+ import { Children, cloneElement, createElement, forwardRef, isValidElement, memo, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Dimensions, findNodeHandle, Keyboard, StyleSheet, UIManager } from "react-native";
3
3
 
4
- export default function ({ children, offset = 10, disabled, onHandleDodging, disableTagCheck, forceDodgeFocusId }) {
4
+ export default function ({ children, offset = 10, disabled, onHandleDodging, disableTagCheck, checkIfElementIsFocused }) {
5
+ if (checkIfElementIsFocused !== undefined) {
6
+ if (typeof checkIfElementIsFocused !== 'function')
7
+ throw 'checkIfElementIsFocused should be a function';
5
8
 
6
- if (forceDodgeFocusId !== undefined) {
7
- if (typeof forceDodgeFocusId !== 'string' || !forceDodgeFocusId.trim())
8
- throw `forceDodgeFocusId should be a non-empty string but got ${forceDodgeFocusId}`;
9
+ checkIfElementIsFocused = niceFunction(checkIfElementIsFocused, 'checkIfElementIsFocused');
10
+ } else checkIfElementIsFocused = r => r?.isFocused?.();
11
+
12
+ if (onHandleDodging !== undefined) {
13
+ if (typeof onHandleDodging !== 'function')
14
+ throw 'onHandleDodging should be a function';
15
+
16
+ onHandleDodging = niceFunction(onHandleDodging, 'onHandleDodging');
9
17
  }
10
18
 
11
19
  if (!isNumber(offset)) throw `offset must be a valid number but got ${offset}`;
@@ -13,79 +21,106 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
13
21
  const [currentPaddedScroller, setCurrentPaddedScroller] = useState();
14
22
 
15
23
  /**
16
- * @type {import("react").RefObject<{[key: string]: { scrollRef: import("react-native").ScrollView, inputRef: {[key: string]: import("react-native").TextInput}, focusIdMap: {[key: string]: string} }}>}
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 } }} }}>}
17
25
  */
18
26
  const viewRefsMap = useRef({});
19
27
  const isKeyboardVisible = useRef();
20
- const onKeyboardChanged = useRef();
28
+ const doDodgeKeyboard = useRef();
21
29
  const previousLift = useRef();
22
30
 
23
31
  const clearPreviousDodge = (scrollId) => {
24
32
  if (previousLift.current && previousLift.current !== scrollId) {
25
33
  const viewRef = viewRefsMap.current[previousLift.current]?.scrollRef;
26
- if (viewRef) onHandleDodging?.({ liftUp: 0, viewRef });
34
+ onHandleDodging?.({ liftUp: 0, viewRef: viewRef || null });
27
35
  previousLift.current = undefined;
28
36
  }
29
37
  }
30
38
 
31
- onKeyboardChanged.current = () => {
32
- const keyboardInfo = Keyboard.metrics();
33
- const { width: windowWidth, height: windowHeight } = Dimensions.get('window');
34
-
35
- if (
36
- isKeyboardVisible.current &&
37
- keyboardInfo &&
38
- !disabled &&
39
- (keyboardInfo.width === windowWidth ||
40
- keyboardInfo.height + keyboardInfo.screenY === windowHeight) &&
41
- keyboardInfo.screenY
42
- ) {
43
- for (const [scrollId, obj] of Object.entries(viewRefsMap.current)) {
44
- const { scrollRef, inputRef, focusIdMap } = obj;
45
- if (scrollRef) {
46
- for (const [inputId, inputObj] of Object.entries(inputRef)) {
47
- if (forceDodgeFocusId ? focusIdMap[inputId] === forceDodgeFocusId : inputObj?.isFocused?.()) {
48
- scrollRef?.getNativeScrollRef?.()?.measureInWindow?.((sx, sy, sw, sh) => {
49
- inputObj.measureInWindow((x, y) => { // y is dynamic
50
- inputObj.measureLayout(scrollRef, (l, t, w, h) => { // t is fixed
51
- const scrollInputY = y - sy;
52
-
53
- if (scrollInputY >= 0 && scrollInputY <= sh) { // is input visible in viewport
54
- const clampedLift = Math.min(h, keyboardInfo.screenY);
55
-
56
- if (y + clampedLift >= keyboardInfo.screenY) { // is below keyboard
57
- const requiredScrollY = (t - (keyboardInfo.screenY - sy)) + clampedLift + offset;
58
- // for lifting up the scroll-view
59
- const liftUp = Math.max(0, requiredScrollY - t);
60
- clearPreviousDodge(scrollId);
61
- if (liftUp) {
62
- previousLift.current = scrollId;
63
- onHandleDodging?.({ liftUp, viewRef: scrollRef });
64
- }
39
+ doDodgeKeyboard.current = () => {
40
+ try {
41
+ const keyboardInfo = Keyboard.metrics();
42
+ const { width: windowWidth, height: windowHeight } = Dimensions.get('window');
43
+
44
+ // console.log('doDodgeKeyboard');
45
+ if (
46
+ isKeyboardVisible.current &&
47
+ keyboardInfo &&
48
+ !disabled &&
49
+ (keyboardInfo.width === windowWidth ||
50
+ keyboardInfo.height + keyboardInfo.screenY === windowHeight) &&
51
+ keyboardInfo.screenY
52
+ ) {
53
+ // console.log('doDodgeKeyboard 1 entries:', Object.keys(viewRefsMap.current).length);
54
+ for (const [scrollId, obj] of Object.entries(viewRefsMap.current)) {
55
+ const { scrollRef, inputRef, __is_standalone, _standalone_props } = obj;
56
+ if (scrollRef) {
57
+ 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
+ });
71
+ return;
72
+ }
73
+ } else {
74
+ 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
78
+ 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
+ }
65
96
 
66
- const scrollLift = Math.max(0, (sy + sh + (offset >= 0 ? offset : 0)) - keyboardInfo.screenY);
67
- const newScrollY = Math.min(requiredScrollY, t);
97
+ const scrollLift = Math.max(0, (sy + sh + (thisOffset >= 0 ? thisOffset : 0)) - keyboardInfo.screenY);
98
+ const newScrollY = Math.min(requiredScrollY, t);
68
99
 
69
- console.log('scrolling-to:', requiredScrollY, ' scrollLift:', scrollLift);
70
- if (scrollLift) {
71
- setCurrentPaddedScroller([scrollId, scrollLift, newScrollY]);
72
- } else {
73
- scrollRef.scrollTo({ y: newScrollY, animated: true });
74
- setCurrentPaddedScroller();
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
+ }
75
108
  }
76
- }
77
- }
78
- });
79
- });
80
- });
81
- return;
109
+ });
110
+ });
111
+ }));
112
+ return;
113
+ }
114
+ }
82
115
  }
83
116
  }
84
117
  }
118
+ } else {
119
+ setCurrentPaddedScroller();
120
+ clearPreviousDodge();
85
121
  }
86
- } else {
87
- clearPreviousDodge();
88
- setCurrentPaddedScroller();
122
+ } catch (error) {
123
+ console.error('doDodgeKeyboard err:', error);
89
124
  }
90
125
  }
91
126
 
@@ -93,35 +128,37 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
93
128
 
94
129
  useEffect(() => {
95
130
  if (currentPaddedScroller) {
96
- viewRefsMap.current[paddedId].scrollRef.scrollTo({ y: paddedScroll, animated: true });
131
+ 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
+ }
97
141
  }
98
142
  }, [currentPaddedScroller]);
99
143
 
100
144
  useEffect(() => {
101
- try {
102
- onKeyboardChanged.current();
103
- } catch (error) {
104
- console.error('onDodgeKeyboard err:', error);
105
- }
106
- }, [offset, !disabled, !forceDodgeFocusId]);
145
+ doDodgeKeyboard.current();
146
+ }, [offset, !disabled]);
107
147
 
108
148
  useEffect(() => {
109
- if (disabled) {
110
- isKeyboardVisible.current = false;
111
- return;
112
- }
149
+ if (disabled) return;
113
150
  const frameListener = Keyboard.addListener('keyboardDidChangeFrame', e => {
114
- onKeyboardChanged.current();
151
+ doDodgeKeyboard.current();
115
152
  });
116
153
 
117
154
  const showListener = Keyboard.addListener('keyboardDidShow', e => {
118
155
  isKeyboardVisible.current = true;
119
- onKeyboardChanged.current();
156
+ doDodgeKeyboard.current();
120
157
  });
121
158
 
122
159
  const hiddenListener = Keyboard.addListener('keyboardDidHide', e => {
123
160
  isKeyboardVisible.current = false;
124
- onKeyboardChanged.current();
161
+ doDodgeKeyboard.current();
125
162
  });
126
163
 
127
164
  return () => {
@@ -134,18 +171,90 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
134
171
  return (
135
172
  <ReactHijacker
136
173
  doHijack={(node, path) => {
137
- if (isDodgeScrollable(node, disableTagCheck)) {
174
+ if (node?.props?.['dodge_keyboard_scan_off']) return { element: node };
175
+
176
+ const isStandalone = isDodgeInput(node);
177
+
178
+ if (isStandalone || isDodgeScrollable(node, disableTagCheck)) {
138
179
  const scrollId = path.join('=>');
139
180
  const initNode = () => {
140
181
  if (!viewRefsMap.current[scrollId])
141
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
+ }
142
191
  }
143
192
  const shouldPad = scrollId === paddedId;
144
193
  const contentStyle = shouldPad && StyleSheet.flatten(node.props?.contentContainerStyle);
145
- const dodgeFocusIdDuplicateCheck = {};
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
+ }
205
+
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
+ }
222
+
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
+ }
245
+
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
+ });
146
256
 
147
257
  return {
148
- persist: true,
149
258
  props: {
150
259
  ...node.props,
151
260
  ...shouldPad ? {
@@ -159,7 +268,8 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
159
268
  initNode();
160
269
  viewRefsMap.current[scrollId].scrollRef = r;
161
270
  } else if (viewRefsMap.current[scrollId]) {
162
- delete viewRefsMap.current[scrollId];
271
+ viewRefsMap.current[scrollId].scrollRef = undefined;
272
+ doRefCleanup();
163
273
  }
164
274
 
165
275
  const thatRef = node.props?.ref;
@@ -167,65 +277,32 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
167
277
  thatRef(r);
168
278
  } else if (thatRef) thatRef.current = r;
169
279
  },
280
+ ...isStandalone ? {
281
+ onFocus: (...args) => {
282
+ doDodgeKeyboard.current();
283
+ return node.props?.onFocus?.(...args);
284
+ }
285
+ } : {},
170
286
  onLayout: (...args) => {
171
- onKeyboardChanged.current();
287
+ doDodgeKeyboard.current();
172
288
  return node.props?.onLayout?.(...args);
173
289
  },
174
- children:
175
- <ReactHijacker
176
- path={path}
177
- doHijack={(inputNode, path) => {
178
- if (isDodgeInput(inputNode, disableTagCheck)) {
179
- const inputId = path.join('=>');
180
-
181
- const dodge_focus_id = inputNode.props?.['dodge-keyboard-focus-id'];
182
-
183
- if (dodge_focus_id) {
184
- if (typeof dodge_focus_id !== 'string' || !dodge_focus_id.trim())
185
- throw `dodge-keyboard-focus-id must be a non-empty string but got ${dodge_focus_id}`;
186
-
187
- if (dodgeFocusIdDuplicateCheck[dodge_focus_id])
188
- throw `duplicate dodge-keyboard-focus-id must not exist inside the same <DodgeKeyboard> ancestor component`;
189
-
190
- dodgeFocusIdDuplicateCheck[dodge_focus_id] = true;
191
-
192
- initNode();
193
- viewRefsMap.current[scrollId].focusIdMap[inputId] = dodge_focus_id;
194
- }
195
-
196
- return {
197
- persist: true,
198
- props: {
199
- ...inputNode.props,
200
- ...dodge_focus_id ? { key: dodge_focus_id } : {},
201
- onFocus: (...args) => {
202
- onKeyboardChanged.current();
203
- return inputNode.props?.onFocus?.(...args);
204
- },
205
- onLayout: (...args) => {
206
- onKeyboardChanged.current();
207
- return inputNode.props?.onLayout?.(...args);
208
- },
209
- ref: r => {
210
- if (r) {
211
- initNode();
212
-
213
- viewRefsMap.current[scrollId].inputRef[inputId] = r;
214
- } else if (viewRefsMap.current[scrollId]?.inputRef?.[inputId]) {
215
- delete viewRefsMap.current[scrollId].inputRef[inputId];
216
- }
217
-
218
- const thatRef = inputNode.props?.ref;
219
- if (typeof thatRef === 'function') {
220
- thatRef(r);
221
- } else if (thatRef) thatRef.current = r;
222
- }
223
- }
224
- };
225
- }
226
- }}>
227
- {node.props?.children}
228
- </ReactHijacker>
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) }
229
306
  }
230
307
  };
231
308
  }
@@ -233,28 +310,101 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
233
310
  {children}
234
311
  </ReactHijacker>
235
312
  );
313
+ };
314
+
315
+ const niceFunction = (func, message) => {
316
+ return (...args) => {
317
+ try {
318
+ return func(...args);
319
+ } catch (error) {
320
+ console.error(`${message} err:`, error);
321
+ }
322
+ }
236
323
  }
237
324
 
238
325
  const isNumber = t => typeof t === 'number' && !isNaN(t) && Number.isFinite(t);
239
326
 
327
+ const REACT_SYMBOLS = {
328
+ forwardRef: Symbol.for('react.forward_ref'),
329
+ memo: Symbol.for('react.memo')
330
+ };
331
+
240
332
  export function ReactHijacker({ children, doHijack, path }) {
241
- let proceedHijacking = true;
333
+ const renderRefs = useMemo(() => new Map(), []);
242
334
 
243
- const injectIntoTree = (node, path = [], wasArray) => {
244
- if (!proceedHijacking || !node) return node;
335
+ const instantDoHijack = useRef();
336
+ instantDoHijack.current = doHijack;
337
+
338
+ const injectIntoTree = (node, path = [], arrayIndex) => {
339
+ if (!node) return node;
245
340
  if (Array.isArray(node)) {
246
- return React.Children.map(node, (v, i) => injectIntoTree(v, [...path, i], true));
341
+ path = [...path, ...arrayIndex === undefined ? [0] : [arrayIndex]];
342
+ return Children.map(node, (v, i) => injectIntoTree(v, path, i));
247
343
  }
248
- if (!React.isValidElement(node)) return node;
344
+ if (!isValidElement(node)) return node;
249
345
 
250
- path = [...path, ...wasArray ? [] : [0], buildNodeId(node)];
346
+ path = [...path, ...arrayIndex === undefined ? [0] : [arrayIndex], getNodeId(node)];
251
347
 
252
348
  let thisObj;
253
- if (thisObj = doHijack(node, path)) {
254
- const { persist, props } = thisObj;
255
- proceedHijacking = persist;
349
+ if (thisObj = instantDoHijack.current?.(node, path)) {
350
+ const { element, props } = thisObj;
256
351
 
257
- return cloneElement(node, props);
352
+ if (Object.hasOwn(thisObj, 'element')) return element;
353
+ if (props) return cloneElement(node, props);
354
+ return node;
355
+ }
356
+
357
+ if (!isHostElement(node)) {
358
+ 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];
364
+
365
+ // if (doLogging) console.log('wrapNodeType path:', pathway, ' node:', nodeType);
366
+ const render = (renderedNode) => {
367
+ // if (doLogging) console.log('deep path:', pathway, ' node:', renderedNode);
368
+ return injectIntoTree(renderedNode, pathway);
369
+ }
370
+
371
+ let newType;
372
+
373
+ if (typeof nodeType === 'function') { // check self closed tag
374
+ newType = hijackRender(nodeType, render);
375
+ } else if (nodeType?.$$typeof === REACT_SYMBOLS.forwardRef) {
376
+ newType = forwardRef(hijackRender(nodeType.render, render));
377
+ } else if (nodeType?.$$typeof === REACT_SYMBOLS.memo) {
378
+ newType = memo(wrapNodeType(nodeType.type, pathway), nodeType.compare);
379
+ newType.displayName = nodeType.displayName || nodeType.name;
380
+ }
381
+
382
+ if (newType) {
383
+ if (!renderRefStore) renderRefs.set(nodeType, renderRefStore = {});
384
+ renderRefStore[path_id] = newType;
385
+ return newType;
386
+ }
387
+
388
+ return nodeType;
389
+ }
390
+
391
+ if (
392
+ typeof node.type === 'function' || // check self closed tag
393
+ node.type?.$$typeof === REACT_SYMBOLS.forwardRef || // check forwardRef
394
+ node.type?.$$typeof === REACT_SYMBOLS.memo // check memo
395
+ ) {
396
+ // 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
406
+ );
407
+ }
258
408
  }
259
409
 
260
410
  const children = node.props?.children;
@@ -267,40 +417,79 @@ export function ReactHijacker({ children, doHijack, path }) {
267
417
  };
268
418
 
269
419
  return injectIntoTree(children, path);
270
- }
420
+ };
421
+
422
+ const hijackRender = (type, doHijack) =>
423
+ new Proxy(type, {
424
+ apply(target, thisArg, args) {
425
+ const renderedNode = Reflect.apply(target, thisArg, args);
426
+ return doHijack(renderedNode);
427
+ },
428
+ get(target, prop) {
429
+ return target[prop];
430
+ },
431
+ set(target, prop, value) {
432
+ target[prop] = value;
433
+ return true;
434
+ }
435
+ });
436
+
437
+ function getNodeId(node, typeObj, typeKey) {
438
+ if ((!node && !typeObj) || (node && !isValidElement(node))) return `${node}`;
439
+
440
+ const type = typeObj || node?.type;
441
+ const withKey = (s) => `${s}:${[typeKey || node?.key].find(v => ![null, undefined].includes(v)) || null}`;
442
+ const withWrapper = (tag, name) => `@${tag}#${name}#`;
443
+
444
+ // Host component
445
+ if (typeof type === 'string' || typeof type === 'number') return withKey(type);
271
446
 
272
- const buildNodeId = (node) => {
273
- const type = node?.type;
274
- const finalType = typeof type === "string" ? type : (type?.displayName || type?.name || "@#Fragment");
275
- return `${finalType}:${node?.key}`;
447
+ if (type?.displayName) return withKey(type.displayName);
448
+
449
+ // Function component
450
+ if (typeof type === "function") return withKey(type.name);
451
+
452
+ if (type?.$$typeof === REACT_SYMBOLS.forwardRef) { // forwardRef
453
+ return withKey(withWrapper('forwardRef', type.render?.name));
454
+ } else if (type?.$$typeof === REACT_SYMBOLS.memo) { // memo
455
+ return withKey(withWrapper('memo', ''));
456
+ }
457
+
458
+ return withKey(
459
+ type?.name ||
460
+ // node?.name ||
461
+ withWrapper('Fragment', type?.$$typeof?.toString?.() || '')
462
+ );
463
+ };
464
+
465
+ export function isHostElement(node) {
466
+ if (!node) return true;
467
+ const t = typeof node.type;
468
+
469
+ return (
470
+ t === 'string' ||
471
+ t === 'number' || // RN internal tags
472
+ node?.type?.Context !== undefined ||
473
+ (t !== 'function' && t !== 'object')
474
+ );
276
475
  }
277
476
 
278
- const isDodgeScrollable = (element, disableTagCheck) => {
279
- if (element?.props?.['dodge-keyboard-scrollview'])
280
- return true;
477
+ export const isDodgeScrollable = (element, disableTagCheck) => {
478
+ if (element?.props?.['dodge_keyboard_scrollable']) return true;
281
479
  if (!element?.type || element?.props?.horizontal || disableTagCheck) return false;
282
480
 
283
481
  const scrollableTypes = ["ScrollView", "FlatList", "SectionList", "VirtualizedList"];
284
482
 
285
- return scrollableTypes.includes(element.type.displayName)
483
+ return scrollableTypes.includes(element.type?.displayName)
286
484
  || scrollableTypes.includes(element.type?.name);
287
485
  };
288
486
 
289
- const isDodgeInput = (element, disableTagCheck) => {
290
- if (
291
- element?.props?.['dodge-keyboard-input'] ||
292
- element?.props?.['dodge-keyboard-focus-id']
293
- ) return true;
294
- if (disableTagCheck) return false;
295
- const { placeholder, onChangeText } = element?.props || {};
296
- if (
297
- typeof onChangeText === 'function' ||
298
- (typeof placeholder === 'string' && placeholder.trim())
299
- ) return true;
300
- if (!element?.type) return false;
487
+ export const isDodgeInput = (element, disableTagCheck) => {
488
+ if (element?.props?.['dodge_keyboard_input']) return true;
489
+ if (disableTagCheck || !element?.type) return false;
301
490
 
302
491
  const inputTypes = ["TextInput"];
303
492
 
304
- return inputTypes.includes(element.type.displayName)
493
+ return inputTypes.includes(element.type?.displayName)
305
494
  || inputTypes.includes(element.type?.name);
306
495
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-dodge-keyboard",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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",