react-native-dodge-keyboard 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.d.ts +22 -15
  2. package/index.js +342 -164
  3. package/package.json +1 -1
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,81 @@ 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 injectChild = (children, childPath) =>
199
+ ReactHijacker({
200
+ children,
201
+ path: childPath,
202
+ doHijack: (inputNode, path) => {
203
+ if (isDodgeInput(inputNode, disableTagCheck)) {
204
+ const inputId = path.join('=>');
205
+ const initInputNode = () => {
206
+ initNode();
207
+ if (!viewRefsMap.current[scrollId].inputRef[inputId])
208
+ viewRefsMap.current[scrollId].inputRef[inputId] = {};
209
+ viewRefsMap.current[scrollId].inputRef[inputId].props = {
210
+ dodge_keyboard_offset: inputNode.props?.dodge_keyboard_offset,
211
+ dodge_keyboard_lift: inputNode.props?.dodge_keyboard_lift
212
+ };
213
+ }
214
+
215
+ initInputNode();
216
+
217
+ return {
218
+ props: {
219
+ ...inputNode.props,
220
+ onFocus: (...args) => {
221
+ doDodgeKeyboard.current();
222
+ return inputNode.props?.onFocus?.(...args);
223
+ },
224
+ onLayout: (...args) => {
225
+ doDodgeKeyboard.current();
226
+ return inputNode.props?.onLayout?.(...args);
227
+ },
228
+ ref: r => {
229
+ if (r) {
230
+ initInputNode();
231
+
232
+ viewRefsMap.current[scrollId].inputRef[inputId].ref = r;
233
+ } else if (viewRefsMap.current[scrollId]?.inputRef?.[inputId]) {
234
+ delete viewRefsMap.current[scrollId].inputRef[inputId];
235
+ }
236
+
237
+ const thatRef = inputNode.props?.ref;
238
+ if (typeof thatRef === 'function') {
239
+ thatRef(r);
240
+ } else if (thatRef) thatRef.current = r;
241
+ }
242
+ }
243
+ };
244
+ }
245
+ }
246
+ });
146
247
 
147
248
  return {
148
- persist: true,
149
249
  props: {
150
250
  ...node.props,
151
251
  ...shouldPad ? {
@@ -167,65 +267,32 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
167
267
  thatRef(r);
168
268
  } else if (thatRef) thatRef.current = r;
169
269
  },
270
+ ...isStandalone ? {
271
+ onFocus: (...args) => {
272
+ doDodgeKeyboard.current();
273
+ return node.props?.onFocus?.(...args);
274
+ }
275
+ } : {},
170
276
  onLayout: (...args) => {
171
- onKeyboardChanged.current();
277
+ doDodgeKeyboard.current();
172
278
  return node.props?.onLayout?.(...args);
173
279
  },
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>
280
+ ...isStandalone ? {} :
281
+ hasInternalList ? {
282
+ renderItem: (...args) => {
283
+ const { item, index } = args[0] || {};
284
+
285
+ return injectChild(
286
+ rootRenderItem(...args),
287
+ [
288
+ ...path,
289
+ index,
290
+ ...typeof rootKeyExtractor === 'function' ?
291
+ [rootKeyExtractor?.(item, index)] : []
292
+ ]
293
+ );
294
+ }
295
+ } : { children: injectChild(node.props?.children, path) }
229
296
  }
230
297
  };
231
298
  }
@@ -233,28 +300,100 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
233
300
  {children}
234
301
  </ReactHijacker>
235
302
  );
303
+ };
304
+
305
+ const niceFunction = (func, message) => {
306
+ return (...args) => {
307
+ try {
308
+ return func(...args);
309
+ } catch (error) {
310
+ console.error(`${message} err:`, error);
311
+ }
312
+ }
236
313
  }
237
314
 
238
315
  const isNumber = t => typeof t === 'number' && !isNaN(t) && Number.isFinite(t);
239
316
 
317
+ const REACT_SYMBOLS = {
318
+ forwardRef: Symbol.for('react.forward_ref'),
319
+ memo: Symbol.for('react.memo')
320
+ };
321
+
240
322
  export function ReactHijacker({ children, doHijack, path }) {
241
- let proceedHijacking = true;
323
+ const renderRefs = useMemo(() => new Map(), []);
324
+
325
+ const instantDoHijack = useRef();
326
+ instantDoHijack.current = doHijack;
242
327
 
243
328
  const injectIntoTree = (node, path = [], wasArray) => {
244
- if (!proceedHijacking || !node) return node;
329
+ if (!node) return node;
245
330
  if (Array.isArray(node)) {
246
- return React.Children.map(node, (v, i) => injectIntoTree(v, [...path, i], true));
331
+ return Children.map(node, (v, i) => injectIntoTree(v, [...path, i], true));
247
332
  }
248
- if (!React.isValidElement(node)) return node;
333
+ if (!isValidElement(node)) return node;
249
334
 
250
- path = [...path, ...wasArray ? [] : [0], buildNodeId(node)];
335
+ path = [...path, ...wasArray ? [] : [0], getNodeId(node)];
251
336
 
252
337
  let thisObj;
253
- if (thisObj = doHijack(node, path)) {
254
- const { persist, props } = thisObj;
255
- proceedHijacking = persist;
338
+ if (thisObj = instantDoHijack.current?.(node, path)) {
339
+ const { element, props } = thisObj;
340
+
341
+ if (Object.hasOwn(thisObj, 'element')) return element;
342
+ if (props) return cloneElement(node, props);
343
+ return node;
344
+ }
345
+
346
+ if (!isHostElement(node)) {
347
+ const wrapNodeType = (nodeType, pathway, pathKey) => {
348
+ pathway = [...pathway, getNodeId(undefined, nodeType, pathKey)];
349
+ const path_id = pathway.join(',');
350
+ let renderRefStore = renderRefs.get(nodeType);
351
+
352
+ if (renderRefStore?.[path_id]) return renderRefStore[path_id];
353
+
354
+ // if (doLogging) console.log('wrapNodeType path:', pathway, ' node:', nodeType);
355
+ const render = (renderedNode) => {
356
+ // if (doLogging) console.log('deep path:', pathway, ' node:', renderedNode);
357
+ return injectIntoTree(renderedNode, pathway);
358
+ }
359
+
360
+ let newType;
361
+
362
+ if (typeof nodeType === 'function') { // check self closed tag
363
+ newType = hijackRender(nodeType, render);
364
+ } else if (nodeType?.$$typeof === REACT_SYMBOLS.forwardRef) {
365
+ newType = forwardRef(hijackRender(nodeType.render, render));
366
+ } else if (nodeType?.$$typeof === REACT_SYMBOLS.memo) {
367
+ newType = memo(wrapNodeType(nodeType.type, pathway), nodeType.compare);
368
+ newType.displayName = nodeType.displayName || nodeType.name;
369
+ }
370
+
371
+ if (newType) {
372
+ if (!renderRefStore) renderRefs.set(nodeType, renderRefStore = {});
373
+ renderRefStore[path_id] = newType;
374
+ return newType;
375
+ }
376
+
377
+ return nodeType;
378
+ }
256
379
 
257
- return cloneElement(node, props);
380
+ if (
381
+ typeof node.type === 'function' || // check self closed tag
382
+ node.type?.$$typeof === REACT_SYMBOLS.forwardRef || // check forwardRef
383
+ node.type?.$$typeof === REACT_SYMBOLS.memo // check memo
384
+ ) {
385
+ // if (doLogging) console.log('doLog path:', path, ' node:', node);
386
+ const injectedType = wrapNodeType(node.type, path.slice(0, -1), node.key);
387
+ return createElement(
388
+ injectedType,
389
+ {
390
+ ...node.props,
391
+ key: node.key,
392
+ // ...isForwardRef ? { ref: node.ref } : {}
393
+ },
394
+ node.props?.children
395
+ );
396
+ }
258
397
  }
259
398
 
260
399
  const children = node.props?.children;
@@ -267,40 +406,79 @@ export function ReactHijacker({ children, doHijack, path }) {
267
406
  };
268
407
 
269
408
  return injectIntoTree(children, path);
270
- }
409
+ };
410
+
411
+ const hijackRender = (type, doHijack) =>
412
+ new Proxy(type, {
413
+ apply(target, thisArg, args) {
414
+ const renderedNode = Reflect.apply(target, thisArg, args);
415
+ return doHijack(renderedNode);
416
+ },
417
+ get(target, prop) {
418
+ return target[prop];
419
+ },
420
+ set(target, prop, value) {
421
+ target[prop] = value;
422
+ return true;
423
+ }
424
+ });
425
+
426
+ function getNodeId(node, typeObj, typeKey) {
427
+ if ((!node && !typeObj) || (node && !isValidElement(node))) return `${node}`;
428
+
429
+ const type = typeObj || node?.type;
430
+ const withKey = (s) => `${s}:${[typeKey || node?.key].find(v => ![null, undefined].includes(v)) || null}`;
431
+ const withWrapper = (tag, name) => `@${tag}#${name}#`;
432
+
433
+ // Host component
434
+ if (typeof type === 'string' || typeof type === 'number') return withKey(type);
271
435
 
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}`;
436
+ if (type?.displayName) return withKey(type.displayName);
437
+
438
+ // Function component
439
+ if (typeof type === "function") return withKey(type.name);
440
+
441
+ if (type?.$$typeof === REACT_SYMBOLS.forwardRef) { // forwardRef
442
+ return withKey(withWrapper('forwardRef', type.render?.name));
443
+ } else if (type?.$$typeof === REACT_SYMBOLS.memo) { // memo
444
+ return withKey(withWrapper('memo', ''));
445
+ }
446
+
447
+ return withKey(
448
+ type?.name ||
449
+ // node?.name ||
450
+ withWrapper('Fragment', type?.$$typeof?.toString?.() || '')
451
+ );
452
+ };
453
+
454
+ export function isHostElement(node) {
455
+ if (!node) return true;
456
+ const t = typeof node.type;
457
+
458
+ return (
459
+ t === 'string' ||
460
+ t === 'number' || // RN internal tags
461
+ node?.type?.Context !== undefined ||
462
+ (t !== 'function' && t !== 'object')
463
+ );
276
464
  }
277
465
 
278
- const isDodgeScrollable = (element, disableTagCheck) => {
279
- if (element?.props?.['dodge-keyboard-scrollview'])
280
- return true;
466
+ export const isDodgeScrollable = (element, disableTagCheck) => {
467
+ if (element?.props?.['dodge_keyboard_scrollable']) return true;
281
468
  if (!element?.type || element?.props?.horizontal || disableTagCheck) return false;
282
469
 
283
470
  const scrollableTypes = ["ScrollView", "FlatList", "SectionList", "VirtualizedList"];
284
471
 
285
- return scrollableTypes.includes(element.type.displayName)
472
+ return scrollableTypes.includes(element.type?.displayName)
286
473
  || scrollableTypes.includes(element.type?.name);
287
474
  };
288
475
 
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;
476
+ export const isDodgeInput = (element, disableTagCheck) => {
477
+ if (element?.props?.['dodge_keyboard_input']) return true;
478
+ if (disableTagCheck || !element?.type) return false;
301
479
 
302
480
  const inputTypes = ["TextInput"];
303
481
 
304
- return inputTypes.includes(element.type.displayName)
482
+ return inputTypes.includes(element.type?.displayName)
305
483
  || inputTypes.includes(element.type?.name);
306
484
  };
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.1",
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",