react-native-dodge-keyboard 1.0.3 → 1.0.5

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 +221 -119
  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,18 @@ 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 resizerTimer = useRef();
32
+ const lastKeyboardEvent = useRef();
30
33
 
31
34
  const clearPreviousDodge = (scrollId) => {
32
35
  if (previousLift.current && previousLift.current !== scrollId) {
33
36
  const viewRef = viewRefsMap.current[previousLift.current]?.scrollRef;
34
- onHandleDodging?.({ liftUp: 0, viewRef: viewRef || null });
37
+ onHandleDodging?.({
38
+ liftUp: 0,
39
+ viewRef: viewRef || null,
40
+ keyboardEvent: lastKeyboardEvent.current
41
+ });
35
42
  previousLift.current = undefined;
36
43
  }
37
44
  }
@@ -39,8 +46,11 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
39
46
  /**
40
47
  * @param {import('react-native').KeyboardEvent | undefined} event
41
48
  * @param {boolean} visible
49
+ * @param {{ fromIdle?: boolean, fromTimer?: boolean } | undefined} eventContext
42
50
  */
43
- doDodgeKeyboard.current = (event, visible) => {
51
+ doDodgeKeyboard.current = (event, visible, eventContext) => {
52
+ if (Platform.OS === 'ios' && event && !event?.isEventFromThisApp) return;
53
+
44
54
  if (typeof visible !== 'boolean') {
45
55
  if (typeof wasVisible.current === 'boolean') {
46
56
  visible = wasVisible.current;
@@ -48,79 +58,132 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
48
58
  }
49
59
 
50
60
  wasVisible.current = visible;
61
+ if (event) lastKeyboardEvent.current = event;
51
62
 
52
63
  try {
53
64
  const keyboardInfo = event?.endCoordinates || Keyboard.metrics();
54
- const { width: windowWidth, height: windowHeight } = Dimensions.get('window');
55
65
 
56
66
  // console.log('doDodgeKeyboard');
67
+
68
+ if (pendingIdleTask.current !== undefined)
69
+ cancelIdleCallback(pendingIdleTask.current);
70
+ pendingIdleTask.current = undefined;
71
+
72
+ if (resizerTimer.current !== undefined)
73
+ clearTimeout(resizerTimer.current);
74
+ resizerTimer.current = undefined;
75
+
57
76
  if (
58
77
  visible &&
59
78
  keyboardInfo &&
60
79
  !disabled &&
61
- (keyboardInfo.width === windowWidth ||
62
- keyboardInfo.height + keyboardInfo.screenY === windowHeight) &&
80
+ (!isOffScreenY(keyboardInfo) && !isOffScreenX(keyboardInfo)) &&
63
81
  keyboardInfo.screenY
64
82
  ) {
65
83
  // console.log('doDodgeKeyboard 1 entries:', Object.keys(viewRefsMap.current).length);
66
- for (const [scrollId, obj] of Object.entries(viewRefsMap.current)) {
84
+ const itemIteList = Object.entries(viewRefsMap.current);
85
+ const allInputList = checkIfElementIsFocused && itemIteList.map(v =>
86
+ v[1].__is_standalone
87
+ ? [v[1].scrollRef]
88
+ : Object.values(v[1].inputRef).map(v => v.ref)
89
+ ).flat();
90
+
91
+ const initIdleTask = () => {
92
+ if (!eventContext && pendingIdleTask.current === undefined)
93
+ pendingIdleTask.current = requestIdleCallback(() => {
94
+ doDodgeKeyboard.current(undefined, undefined, { fromIdle: true });
95
+ }, { timeout: 300 });
96
+
97
+ if (!eventContext?.fromTimer && resizerTimer.current === undefined)
98
+ resizerTimer.current = setTimeout(() => {
99
+ doDodgeKeyboard.current(undefined, undefined, { fromTimer: true });
100
+ }, 500);
101
+ }
102
+
103
+ const checkFocused = checkIfElementIsFocused || (r => r?.isFocused?.());
104
+
105
+ for (const [scrollId, obj] of itemIteList) {
67
106
  const { scrollRef, inputRef, __is_standalone, _standalone_props } = obj;
68
107
  if (scrollRef) {
69
108
  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
- });
109
+ if (checkFocused(scrollRef, allInputList)) {
110
+ UIManager.measure(findNodeHandle(scrollRef), (_x, _y, w, h, x, y) => {
111
+ const { dodge_keyboard_offset } = _standalone_props || {};
112
+ const thisOffset = isNumber(dodge_keyboard_offset) ? dodge_keyboard_offset : offset;
113
+
114
+ const liftUp = Math.max(0, (y - keyboardInfo.screenY) + Math.min(h + thisOffset, keyboardInfo.screenY));
115
+ clearPreviousDodge(scrollId);
116
+ if (liftUp) {
117
+ previousLift.current = scrollId;
118
+ onHandleDodging?.({
119
+ liftUp,
120
+ viewRef: scrollRef,
121
+ keyboardEvent: lastKeyboardEvent.current
122
+ });
123
+ }
124
+ initIdleTask();
125
+ });
83
126
  return;
84
127
  }
85
128
  } else {
86
129
  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
130
+ if (checkFocused(inputObj, allInputList)) {
131
+ Promise.all([
132
+ new Promise(resolve => {
133
+ UIManager.measure(findNodeHandle(scrollRef), (x, y, w, h, px, py) => {
134
+ resolve({ h, py });
135
+ });
136
+ }),
137
+ new Promise(resolve => {
138
+ inputObj.measure((x, y, w, h, px, py) => { // y is dynamic
139
+ resolve({ py });
140
+ });
141
+ }),
142
+ new Promise((resolve, reject) => {
90
143
  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
- }
144
+ resolve({ t, h })
145
+ }, reject);
146
+ })
147
+ ]).then(([{ h: sh, py: sy }, { py: y }, { t, h }]) => {
148
+
149
+ const { dodge_keyboard_offset } = props || {};
150
+ const thisOffset = isNumber(dodge_keyboard_offset) ? dodge_keyboard_offset : offset;
151
+
152
+ const scrollInputY = y - sy;
153
+
154
+ if (scrollInputY >= 0 && scrollInputY <= sh) { // is input visible in viewport
155
+ const clampedLift = Math.min(h + thisOffset, keyboardInfo.screenY);
156
+
157
+ if (y + clampedLift >= keyboardInfo.screenY) { // is below keyboard
158
+ const requiredScrollY = (t - (keyboardInfo.screenY - sy)) + clampedLift;
159
+ // for lifting up the scroll-view
160
+ const liftUp = Math.max(0, requiredScrollY - t);
161
+ clearPreviousDodge(scrollId);
162
+ if (liftUp) {
163
+ previousLift.current = scrollId;
164
+ onHandleDodging?.({
165
+ liftUp,
166
+ viewRef: scrollRef,
167
+ keyboardEvent: lastKeyboardEvent.current
168
+ });
120
169
  }
121
- });
122
- });
123
- }));
170
+
171
+ const scrollLift = Math.max(0, (sy + sh + (thisOffset >= 0 ? thisOffset : 0)) - keyboardInfo.screenY);
172
+ const newScrollY = Math.min(requiredScrollY, t);
173
+
174
+ // console.log('scrolling-to:', requiredScrollY, ' scrollLift:', scrollLift);
175
+ if (scrollLift) {
176
+ setCurrentPaddedScroller([scrollId, scrollLift, newScrollY]);
177
+ } else {
178
+ tryPerformScroll(scrollRef, newScrollY, true);
179
+ setCurrentPaddedScroller();
180
+ }
181
+ initIdleTask();
182
+ }
183
+ }
184
+ }).catch(e => {
185
+ console.error('frame calculation error:', e);
186
+ });
124
187
  return;
125
188
  }
126
189
  }
@@ -165,11 +228,19 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
165
228
  if (disabled) return;
166
229
  const frameListener = Keyboard.addListener('keyboardDidChangeFrame', e => doDodgeKeyboard.current(e));
167
230
  const showListener = Keyboard.addListener(
168
- Platform.OS === 'android' ? 'keyboardDidShow' : 'keyboardWillShow',
231
+ 'keyboardWillShow',
169
232
  e => doDodgeKeyboard.current(e, true)
170
233
  );
171
234
  const hiddenListener = Keyboard.addListener(
172
- Platform.OS === 'android' ? 'keyboardDidHide' : 'keyboardWillHide',
235
+ 'keyboardWillHide',
236
+ e => doDodgeKeyboard.current(e, false)
237
+ );
238
+ const didShowListener = Keyboard.addListener(
239
+ 'keyboardDidShow',
240
+ e => doDodgeKeyboard.current(e, true)
241
+ );
242
+ const didHideListener = Keyboard.addListener(
243
+ 'keyboardDidHide',
173
244
  e => doDodgeKeyboard.current(e, false)
174
245
  );
175
246
 
@@ -177,13 +248,15 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
177
248
  frameListener.remove();
178
249
  showListener.remove();
179
250
  hiddenListener.remove();
251
+ didShowListener.remove();
252
+ didHideListener.remove();
180
253
  }
181
254
  }, [!disabled]);
182
255
 
183
256
  const nodeIdIte = useRef(0);
184
257
 
185
258
  const onHijackNode = node => {
186
- if (node?.props?.dodge_keyboard_scan_off || node?.props?.__dodging_keyboard) return;
259
+ if (offDodgeScan(node)) return createHijackedElement(node);
187
260
 
188
261
  const isStandalone = isDodgeInput(node);
189
262
  if (!isStandalone && !isDodgeScrollable(node, disableTagCheck)) return;
@@ -216,67 +289,62 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
216
289
  delete viewRefsMap.current[scrollId];
217
290
  }
218
291
 
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
- }
292
+ const injectChild = inputNode => {
293
+ if (offDodgeScan(inputNode)) return createHijackedElement(inputNode);
239
294
 
240
- initInputNode();
295
+ if (!isDodgeInput(inputNode, disableTagCheck)) return;
241
296
 
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
- }
297
+ const inputRenderer = () => {
298
+ const inputId = useMemo(() => `${++nodeIdIte.current}`, []);
299
+ const initInputNode = () => {
300
+ initNode();
301
+ if (!viewRefsMap.current[scrollId].inputRef[inputId])
302
+ viewRefsMap.current[scrollId].inputRef[inputId] = {};
303
+ viewRefsMap.current[scrollId].inputRef[inputId].props = {
304
+ dodge_keyboard_offset: inputNode.props?.dodge_keyboard_offset,
305
+ dodge_keyboard_lift: inputNode.props?.dodge_keyboard_lift
306
+ };
307
+ }
262
308
 
263
- const thatRef = inputNode.props?.ref;
264
- if (typeof thatRef === 'function') {
265
- thatRef(r);
266
- } else if (thatRef) thatRef.current = r;
267
- }
268
- };
309
+ initInputNode();
310
+
311
+ const newProps = {
312
+ ...inputNode.props,
313
+ __dodging_keyboard: true,
314
+ onFocus: (...args) => {
315
+ doDodgeKeyboard.current();
316
+ return inputNode.props?.onFocus?.(...args);
317
+ },
318
+ onLayout: (...args) => {
319
+ doDodgeKeyboard.current();
320
+ return inputNode.props?.onLayout?.(...args);
321
+ },
322
+ ref: r => {
323
+ if (r) {
324
+ initInputNode();
325
+
326
+ viewRefsMap.current[scrollId].inputRef[inputId].ref = r;
327
+ } else if (viewRefsMap.current[scrollId]?.inputRef?.[inputId]) {
328
+ delete viewRefsMap.current[scrollId].inputRef[inputId];
329
+ doRefCleanup();
330
+ }
269
331
 
270
- return cloneElement(inputNode, newProps);
332
+ const thatRef = inputNode.props?.ref;
333
+ if (typeof thatRef === 'function') {
334
+ thatRef(r);
335
+ } else if (thatRef) thatRef.current = r;
271
336
  }
337
+ };
272
338
 
273
- return createHijackedElement(
274
- <__HijackNode>
275
- {inputRenderer}
276
- </__HijackNode>
277
- );
278
- }
279
- });
339
+ return cloneElement(inputNode, newProps);
340
+ }
341
+
342
+ return createHijackedElement(
343
+ <__HijackNode>
344
+ {inputRenderer}
345
+ </__HijackNode>
346
+ );
347
+ }
280
348
 
281
349
  const newProps = {
282
350
  ...node.props,
@@ -314,9 +382,20 @@ export default function ({ children, offset = 10, disabled, onHandleDodging, dis
314
382
  ...isStandalone ? {} :
315
383
  hasInternalList ? {
316
384
  renderItem: (...args) => {
317
- return injectChild(rootRenderItem(...args));
385
+ return (
386
+ <ReactHijacker
387
+ doHijack={injectChild}>
388
+ {rootRenderItem(...args)}
389
+ </ReactHijacker>
390
+ );
318
391
  }
319
- } : { children: injectChild(node.props?.children) }
392
+ } : {
393
+ children:
394
+ ReactHijacker({
395
+ children: node.props?.children,
396
+ doHijack: injectChild
397
+ })
398
+ }
320
399
  };
321
400
 
322
401
  return cloneElement(node, newProps);
@@ -498,6 +577,8 @@ export function isHostElement(node) {
498
577
  );
499
578
  }
500
579
 
580
+ const offDodgeScan = (node) => node?.props?.dodge_keyboard_scan_off || node?.props?.__dodging_keyboard;
581
+
501
582
  export const isDodgeScrollable = (element, disableTagCheck) => {
502
583
  if (element?.props?.['dodge_keyboard_scrollable']) return true;
503
584
  if (!element?.type || element?.props?.horizontal || disableTagCheck) return false;
@@ -518,6 +599,19 @@ export const isDodgeInput = (element, disableTagCheck) => {
518
599
  || inputTypes.includes(element.type?.name);
519
600
  };
520
601
 
602
+ const isOffScreenY = (keyboardInfo, minimumThreshold = .27) =>
603
+ !keyboardInfo ||
604
+ keyboardInfo.height <= 0 ||
605
+ ((keyboardInfo.screenY / (keyboardInfo.screenY + keyboardInfo.height)) < .40) ||
606
+ (keyboardInfo.screenY / Dimensions.get('window').height) < minimumThreshold;
607
+
608
+ const isOffScreenX = (keyboardInfo, minimumThreshold = .7) => {
609
+ const vw = Dimensions.get('window').width;
610
+
611
+ return !keyboardInfo ||
612
+ (Math.min(keyboardInfo.width, vw) / Math.max(keyboardInfo.width, vw)) < minimumThreshold;
613
+ }
614
+
521
615
  export const KeyboardPlaceholderView = ({ doHeight }) => {
522
616
  const height = useAnimatedValue(0);
523
617
 
@@ -542,19 +636,27 @@ export const KeyboardPlaceholderView = ({ doHeight }) => {
542
636
  const { endCoordinates, isEventFromThisApp, duration } = event;
543
637
  if (Platform.OS === 'ios' && !isEventFromThisApp) return;
544
638
 
545
- const kh = visible ? endCoordinates.height : 0;
639
+ const kh = (visible && !isOffScreenX(endCoordinates) && !isOffScreenY(endCoordinates, .3))
640
+ ? Math.max(Dimensions.get('window').height - endCoordinates.screenY, 0)
641
+ : 0;
642
+
546
643
  const newHeight = Math.max(0, instantDoHeight.current ? instantDoHeight.current(kh) : kh);
547
- const newDuration = (Math.abs(height._value - newHeight) * duration) / endCoordinates.height;
644
+ const newDuration = (Math.abs(height._value - newHeight) * duration) / Math.max(0, endCoordinates.height);
548
645
 
549
646
  Animated.timing(height, {
550
- duration: newDuration,
647
+ duration: newDuration || 0,
551
648
  toValue: newHeight,
552
649
  useNativeDriver: false
553
650
  }).start();
554
651
  }
555
652
 
556
653
  const initialMetric = Keyboard.metrics();
557
- if (initialMetric) updateKeyboardHeight(initialMetric, true);
654
+ if (initialMetric)
655
+ updateKeyboardHeight({
656
+ endCoordinates: initialMetric,
657
+ isEventFromThisApp: true,
658
+ duration: 0
659
+ }, true);
558
660
 
559
661
  const frameListener = Keyboard.addListener('keyboardDidChangeFrame', e => updateKeyboardHeight(e));
560
662
  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.5",
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