react-native-dodge-keyboard 1.0.3 → 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 +5 -2
  2. package/index.js +199 -117
  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,7 +100,7 @@ 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
 
package/index.js CHANGED
@@ -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')
@@ -27,11 +27,17 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
27
27
  const doDodgeKeyboard = useRef();
28
28
  const previousLift = useRef();
29
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
  }
@@ -39,8 +45,11 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
39
45
  /**
40
46
  * @param {import('react-native').KeyboardEvent | undefined} event
41
47
  * @param {boolean} visible
48
+ * @param {boolean} fromIdle
42
49
  */
43
- doDodgeKeyboard.current = (event, visible) => {
50
+ doDodgeKeyboard.current = (event, visible, fromIdle) => {
51
+ if (Platform.OS === 'ios' && event && !event?.isEventFromThisApp) return;
52
+
44
53
  if (typeof visible !== 'boolean') {
45
54
  if (typeof wasVisible.current === 'boolean') {
46
55
  visible = wasVisible.current;
@@ -48,79 +57,123 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
48
57
  }
49
58
 
50
59
  wasVisible.current = visible;
60
+ if (event) lastKeyboardEvent.current = event;
51
61
 
52
62
  try {
53
63
  const keyboardInfo = event?.endCoordinates || Keyboard.metrics();
54
- const { width: windowWidth, height: windowHeight } = Dimensions.get('window');
55
64
 
56
65
  // console.log('doDodgeKeyboard');
66
+
67
+ if (pendingIdleTask.current !== undefined)
68
+ cancelIdleCallback(pendingIdleTask.current);
69
+ pendingIdleTask.current = undefined;
70
+
57
71
  if (
58
72
  visible &&
59
73
  keyboardInfo &&
60
74
  !disabled &&
61
- (keyboardInfo.width === windowWidth ||
62
- keyboardInfo.height + keyboardInfo.screenY === windowHeight) &&
75
+ (!isOffScreenY(keyboardInfo) && !isOffScreenX(keyboardInfo)) &&
63
76
  keyboardInfo.screenY
64
77
  ) {
65
78
  // console.log('doDodgeKeyboard 1 entries:', Object.keys(viewRefsMap.current).length);
66
- 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) {
67
96
  const { scrollRef, inputRef, __is_standalone, _standalone_props } = obj;
68
97
  if (scrollRef) {
69
98
  if (__is_standalone) {
70
- if (checkIfElementIsFocused(scrollRef)) {
71
- if (scrollRef.measureInWindow)
72
- UIManager.measureInWindow(findNodeHandle(scrollRef), (x, y, w, h) => {
73
- const { dodge_keyboard_offset } = _standalone_props || {};
74
- const thisOffset = isNumber(dodge_keyboard_offset) ? dodge_keyboard_offset : offset;
75
-
76
- const liftUp = Math.max(0, (y - keyboardInfo.screenY) + Math.min(h + thisOffset, keyboardInfo.screenY));
77
- clearPreviousDodge(scrollId);
78
- if (liftUp) {
79
- previousLift.current = scrollId;
80
- onHandleDodging?.({ liftUp, viewRef: scrollRef });
81
- }
82
- });
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
+ });
83
116
  return;
84
117
  }
85
118
  } else {
86
119
  for (const { ref: inputObj, props } of Object.values(inputRef)) {
87
- if (checkIfElementIsFocused(inputObj)) {
88
- UIManager.measureInWindow(findNodeHandle(scrollRef), ((sx, sy, sw, sh) => {
89
- 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) => {
90
133
  inputObj.measureLayout(scrollRef, (l, t, w, h) => { // t is fixed
91
- const { dodge_keyboard_offset } = props || {};
92
- const thisOffset = isNumber(dodge_keyboard_offset) ? dodge_keyboard_offset : offset;
93
-
94
- const scrollInputY = y - sy;
95
-
96
- if (scrollInputY >= 0 && scrollInputY <= sh) { // is input visible in viewport
97
- const clampedLift = Math.min(h + thisOffset, keyboardInfo.screenY);
98
-
99
- if (y + clampedLift >= keyboardInfo.screenY) { // is below keyboard
100
- const requiredScrollY = (t - (keyboardInfo.screenY - sy)) + clampedLift;
101
- // for lifting up the scroll-view
102
- const liftUp = Math.max(0, requiredScrollY - t);
103
- clearPreviousDodge(scrollId);
104
- if (liftUp) {
105
- previousLift.current = scrollId;
106
- onHandleDodging?.({ liftUp, viewRef: scrollRef });
107
- }
108
-
109
- const scrollLift = Math.max(0, (sy + sh + (thisOffset >= 0 ? thisOffset : 0)) - keyboardInfo.screenY);
110
- const newScrollY = Math.min(requiredScrollY, t);
111
-
112
- // console.log('scrolling-to:', requiredScrollY, ' scrollLift:', scrollLift);
113
- if (scrollLift) {
114
- setCurrentPaddedScroller([scrollId, scrollLift, newScrollY]);
115
- } else {
116
- tryPerformScroll(scrollRef, newScrollY, true);
117
- setCurrentPaddedScroller();
118
- }
119
- }
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
+ });
120
159
  }
121
- });
122
- });
123
- }));
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
+ });
124
177
  return;
125
178
  }
126
179
  }
@@ -183,7 +236,7 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
183
236
  const nodeIdIte = useRef(0);
184
237
 
185
238
  const onHijackNode = node => {
186
- if (node?.props?.dodge_keyboard_scan_off || node?.props?.__dodging_keyboard) return;
239
+ if (offDodgeScan(node)) return createHijackedElement(node);
187
240
 
188
241
  const isStandalone = isDodgeInput(node);
189
242
  if (!isStandalone && !isDodgeScrollable(node, disableTagCheck)) return;
@@ -216,67 +269,62 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
216
269
  delete viewRefsMap.current[scrollId];
217
270
  }
218
271
 
219
- const injectChild = (children, childPath) =>
220
- ReactHijacker({
221
- children,
222
- path: childPath,
223
- doHijack: inputNode => {
224
- if (node?.props?.__dodging_keyboard) return;
225
-
226
- if (!isDodgeInput(inputNode, disableTagCheck)) return;
227
-
228
- const inputRenderer = () => {
229
- const inputId = useMemo(() => `${++nodeIdIte.current}`, []);
230
- const initInputNode = () => {
231
- initNode();
232
- if (!viewRefsMap.current[scrollId].inputRef[inputId])
233
- viewRefsMap.current[scrollId].inputRef[inputId] = {};
234
- viewRefsMap.current[scrollId].inputRef[inputId].props = {
235
- dodge_keyboard_offset: inputNode.props?.dodge_keyboard_offset,
236
- dodge_keyboard_lift: inputNode.props?.dodge_keyboard_lift
237
- };
238
- }
272
+ const injectChild = inputNode => {
273
+ if (offDodgeScan(inputNode)) return createHijackedElement(inputNode);
239
274
 
240
- initInputNode();
275
+ if (!isDodgeInput(inputNode, disableTagCheck)) return;
241
276
 
242
- const newProps = {
243
- ...inputNode.props,
244
- __dodging_keyboard: true,
245
- onFocus: (...args) => {
246
- doDodgeKeyboard.current();
247
- return inputNode.props?.onFocus?.(...args);
248
- },
249
- onLayout: (...args) => {
250
- doDodgeKeyboard.current();
251
- return inputNode.props?.onLayout?.(...args);
252
- },
253
- ref: r => {
254
- if (r) {
255
- initInputNode();
256
-
257
- viewRefsMap.current[scrollId].inputRef[inputId].ref = r;
258
- } else if (viewRefsMap.current[scrollId]?.inputRef?.[inputId]) {
259
- delete viewRefsMap.current[scrollId].inputRef[inputId];
260
- doRefCleanup();
261
- }
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
+ }
262
288
 
263
- const thatRef = inputNode.props?.ref;
264
- if (typeof thatRef === 'function') {
265
- thatRef(r);
266
- } else if (thatRef) thatRef.current = r;
267
- }
268
- };
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
+ }
269
311
 
270
- return cloneElement(inputNode, newProps);
312
+ const thatRef = inputNode.props?.ref;
313
+ if (typeof thatRef === 'function') {
314
+ thatRef(r);
315
+ } else if (thatRef) thatRef.current = r;
271
316
  }
317
+ };
272
318
 
273
- return createHijackedElement(
274
- <__HijackNode>
275
- {inputRenderer}
276
- </__HijackNode>
277
- );
278
- }
279
- });
319
+ return cloneElement(inputNode, newProps);
320
+ }
321
+
322
+ return createHijackedElement(
323
+ <__HijackNode>
324
+ {inputRenderer}
325
+ </__HijackNode>
326
+ );
327
+ }
280
328
 
281
329
  const newProps = {
282
330
  ...node.props,
@@ -314,9 +362,20 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
314
362
  ...isStandalone ? {} :
315
363
  hasInternalList ? {
316
364
  renderItem: (...args) => {
317
- return injectChild(rootRenderItem(...args));
365
+ return (
366
+ <ReactHijacker
367
+ doHijack={injectChild}>
368
+ {rootRenderItem(...args)}
369
+ </ReactHijacker>
370
+ );
318
371
  }
319
- } : { children: injectChild(node.props?.children) }
372
+ } : {
373
+ children:
374
+ ReactHijacker({
375
+ children: node.props?.children,
376
+ doHijack: injectChild
377
+ })
378
+ }
320
379
  };
321
380
 
322
381
  return cloneElement(node, newProps);
@@ -498,6 +557,8 @@ export function isHostElement(node) {
498
557
  );
499
558
  }
500
559
 
560
+ const offDodgeScan = (node) => node?.props?.dodge_keyboard_scan_off || node?.props?.__dodging_keyboard;
561
+
501
562
  export const isDodgeScrollable = (element, disableTagCheck) => {
502
563
  if (element?.props?.['dodge_keyboard_scrollable']) return true;
503
564
  if (!element?.type || element?.props?.horizontal || disableTagCheck) return false;
@@ -518,6 +579,19 @@ export const isDodgeInput = (element, disableTagCheck) => {
518
579
  || inputTypes.includes(element.type?.name);
519
580
  };
520
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
+
521
595
  export const KeyboardPlaceholderView = ({ doHeight }) => {
522
596
  const height = useAnimatedValue(0);
523
597
 
@@ -542,19 +616,27 @@ export const KeyboardPlaceholderView = ({ doHeight }) => {
542
616
  const { endCoordinates, isEventFromThisApp, duration } = event;
543
617
  if (Platform.OS === 'ios' && !isEventFromThisApp) return;
544
618
 
545
- const kh = visible ? endCoordinates.height : 0;
619
+ const kh = (visible && !isOffScreenX(endCoordinates) && !isOffScreenY(endCoordinates, .3))
620
+ ? Math.max(Dimensions.get('window').height - endCoordinates.screenY, 0)
621
+ : 0;
622
+
546
623
  const newHeight = Math.max(0, instantDoHeight.current ? instantDoHeight.current(kh) : kh);
547
- const newDuration = (Math.abs(height._value - newHeight) * duration) / endCoordinates.height;
624
+ const newDuration = (Math.abs(height._value - newHeight) * duration) / Math.max(0, endCoordinates.height);
548
625
 
549
626
  Animated.timing(height, {
550
- duration: newDuration,
627
+ duration: newDuration || 0,
551
628
  toValue: newHeight,
552
629
  useNativeDriver: false
553
630
  }).start();
554
631
  }
555
632
 
556
633
  const initialMetric = Keyboard.metrics();
557
- if (initialMetric) updateKeyboardHeight(initialMetric, true);
634
+ if (initialMetric)
635
+ updateKeyboardHeight({
636
+ endCoordinates: initialMetric,
637
+ isEventFromThisApp: true,
638
+ duration: 0
639
+ }, true);
558
640
 
559
641
  const frameListener = Keyboard.addListener('keyboardDidChangeFrame', e => updateKeyboardHeight(e));
560
642
  const showListener = Keyboard.addListener(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-dodge-keyboard",
3
- "version": "1.0.3",
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