@jobber/components-native 0.99.0 → 0.100.0

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 (37) hide show
  1. package/dist/package.json +3 -6
  2. package/dist/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.js +19 -0
  3. package/dist/src/ContentOverlay/ContentOverlay.js +143 -107
  4. package/dist/src/ContentOverlay/ContentOverlay.style.js +8 -12
  5. package/dist/src/ContentOverlay/computeContentOverlayBehavior.js +76 -0
  6. package/dist/src/ContentOverlay/constants.js +1 -0
  7. package/dist/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.js +25 -0
  8. package/dist/src/ContentOverlay/index.js +1 -1
  9. package/dist/src/InputText/InputText.js +44 -1
  10. package/dist/tsconfig.build.tsbuildinfo +1 -1
  11. package/dist/types/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.d.ts +11 -0
  12. package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +2 -5
  13. package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +11 -10
  14. package/dist/types/src/ContentOverlay/computeContentOverlayBehavior.d.ts +32 -0
  15. package/dist/types/src/ContentOverlay/constants.d.ts +1 -0
  16. package/dist/types/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.d.ts +7 -0
  17. package/dist/types/src/ContentOverlay/index.d.ts +1 -1
  18. package/dist/types/src/ContentOverlay/types.d.ts +5 -12
  19. package/jestSetup.js +2 -0
  20. package/package.json +3 -6
  21. package/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.tsx +36 -0
  22. package/src/ContentOverlay/ContentOverlay.stories.tsx +32 -36
  23. package/src/ContentOverlay/ContentOverlay.style.ts +12 -12
  24. package/src/ContentOverlay/ContentOverlay.test.tsx +157 -79
  25. package/src/ContentOverlay/ContentOverlay.tsx +247 -205
  26. package/src/ContentOverlay/computeContentOverlayBehavior.test.ts +276 -0
  27. package/src/ContentOverlay/computeContentOverlayBehavior.ts +119 -0
  28. package/src/ContentOverlay/constants.ts +1 -0
  29. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.test.ts +81 -0
  30. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.ts +36 -0
  31. package/src/ContentOverlay/index.ts +4 -1
  32. package/src/ContentOverlay/types.ts +5 -13
  33. package/src/InputText/InputText.test.tsx +122 -0
  34. package/src/InputText/InputText.tsx +62 -2
  35. package/dist/src/ContentOverlay/UNSAFE_WrappedModalize.js +0 -23
  36. package/dist/types/src/ContentOverlay/UNSAFE_WrappedModalize.d.ts +0 -3
  37. package/src/ContentOverlay/UNSAFE_WrappedModalize.tsx +0 -41
@@ -14,7 +14,8 @@ import type {
14
14
  TextInputProps,
15
15
  TextStyle,
16
16
  } from "react-native";
17
- import { Platform, TextInput } from "react-native";
17
+ import { Platform, TextInput, findNodeHandle } from "react-native";
18
+ import { useBottomSheetInternal } from "@gorhom/bottom-sheet";
18
19
  import type { RegisterOptions } from "react-hook-form";
19
20
  import type { IconNames } from "@jobber/design";
20
21
  import identity from "lodash/identity";
@@ -22,6 +23,7 @@ import type { Clearable } from "@jobber/hooks";
22
23
  import { useShowClear } from "@jobber/hooks";
23
24
  import { useStyles } from "./InputText.style";
24
25
  import { useInputAccessoriesContext } from "./context";
26
+ import { useIsKeyboardHandledByScrollView } from "../ContentOverlay";
25
27
  import { useFormController } from "../hooks";
26
28
  import type {
27
29
  InputFieldStyleOverride,
@@ -315,6 +317,20 @@ function InputTextInternal(
315
317
  disabled,
316
318
  });
317
319
 
320
+ // When inside a scrollable ContentOverlay, keyboard offset is handled by
321
+ // KeyboardAwareScrollView. Registering with the bottom-sheet's keyboard
322
+ // state would cause double-counted spacing, so we skip it.
323
+ const isKeyboardHandledByScrollView = useIsKeyboardHandledByScrollView();
324
+ const bottomSheetContext = useBottomSheetInternal(true);
325
+ const shouldHandleBottomSheetKeyboard =
326
+ bottomSheetContext !== null && !isKeyboardHandledByScrollView;
327
+ const animatedKeyboardState = shouldHandleBottomSheetKeyboard
328
+ ? bottomSheetContext.animatedKeyboardState
329
+ : undefined;
330
+ const textInputNodesRef = shouldHandleBottomSheetKeyboard
331
+ ? bottomSheetContext.textInputNodesRef
332
+ : undefined;
333
+
318
334
  // Android doesn't have an accessibility label like iOS does. By adding
319
335
  // it as a placeholder it readds it like a label. However we don't want to
320
336
  // add a placeholder on iOS.
@@ -439,11 +455,13 @@ function InputTextInternal(
439
455
  secureTextEntry={secureTextEntry}
440
456
  {...androidA11yProps}
441
457
  onFocus={event => {
458
+ handleBottomSheetFocus(event);
442
459
  _name && setFocusedInput(_name);
443
460
  setFocused(true);
444
461
  onFocus?.(event);
445
462
  }}
446
463
  onBlur={event => {
464
+ handleBottomSheetBlur(event);
447
465
  _name && setFocusedInput("");
448
466
  setFocused(false);
449
467
  onBlur?.(event);
@@ -470,6 +488,48 @@ function InputTextInternal(
470
488
  updateFormAndState(removedIOSCharValue);
471
489
  }
472
490
 
491
+ function handleBottomSheetFocus(event?: FocusEvent) {
492
+ if (!animatedKeyboardState || !textInputNodesRef || !event?.nativeEvent) {
493
+ return;
494
+ }
495
+
496
+ animatedKeyboardState.set(state => ({
497
+ ...state,
498
+ target: event.nativeEvent.target,
499
+ }));
500
+ }
501
+
502
+ function handleBottomSheetBlur(event?: FocusEvent) {
503
+ if (!animatedKeyboardState || !textInputNodesRef || !event?.nativeEvent) {
504
+ return;
505
+ }
506
+ const keyboardState = animatedKeyboardState.get();
507
+ const currentlyFocusedInput = TextInput.State.currentlyFocusedInput();
508
+ const currentFocusedInput =
509
+ currentlyFocusedInput !== null
510
+ ? findNodeHandle(
511
+ // @ts-expect-error - TextInput.State.currentlyFocusedInput() returns NativeMethods
512
+ // which is not directly assignable to findNodeHandle's expected type,
513
+ // but it works at runtime. This is a known type limitation in React Native.
514
+ currentlyFocusedInput,
515
+ )
516
+ : null;
517
+
518
+ // Only remove the target if it belongs to the current component
519
+ // and if the currently focused input is not in the targets set
520
+ const shouldRemoveCurrentTarget =
521
+ keyboardState.target === event.nativeEvent.target;
522
+ const shouldIgnoreBlurEvent =
523
+ currentFocusedInput && textInputNodesRef.current.has(currentFocusedInput);
524
+
525
+ if (shouldRemoveCurrentTarget && !shouldIgnoreBlurEvent) {
526
+ animatedKeyboardState.set(state => ({
527
+ ...state,
528
+ target: undefined,
529
+ }));
530
+ }
531
+ }
532
+
473
533
  function handleClear() {
474
534
  handleChangeText("");
475
535
  }
@@ -516,7 +576,7 @@ interface UseTextInputRefProps {
516
576
  }
517
577
 
518
578
  function useTextInputRef({ ref, onClear }: UseTextInputRefProps) {
519
- const textInputRef = useRef<InputTextRef | null>(null);
579
+ const textInputRef = useRef<TextInput | null>(null);
520
580
 
521
581
  useImperativeHandle(
522
582
  ref,
@@ -1,23 +0,0 @@
1
- import React, { forwardRef, useImperativeHandle, useRef, useState, } from "react";
2
- import { Modalize } from "react-native-modalize";
3
- export const UNSAFE_WrappedModalize = forwardRef((props, ref) => {
4
- const innerRef = useRef(null);
5
- const [openRenderId, setOpenRenderId] = useState(0);
6
- useImperativeHandle(ref, () => ({
7
- open(dest) {
8
- setOpenRenderId(id => id + 1);
9
- // Open on a fresh tick for additional safety
10
- requestAnimationFrame(() => {
11
- var _a;
12
- (_a = innerRef.current) === null || _a === void 0 ? void 0 : _a.open(dest);
13
- });
14
- },
15
- close(dest) {
16
- var _a;
17
- (_a = innerRef.current) === null || _a === void 0 ? void 0 : _a.close(dest);
18
- },
19
- }), []);
20
- // Use a unique key to force a remount, ensuring we get fresh gesture handler nodes within modalize
21
- return (React.createElement(Modalize, Object.assign({ key: `modalize-${openRenderId}`, ref: innerRef }, props)));
22
- });
23
- UNSAFE_WrappedModalize.displayName = "UNSAFE_WrappedModalize";
@@ -1,3 +0,0 @@
1
- import React from "react";
2
- import type { IHandles } from "react-native-modalize/lib/options";
3
- export declare const UNSAFE_WrappedModalize: React.ForwardRefExoticComponent<Omit<import("react-native-modalize/lib/options").IProps<any> & React.RefAttributes<any>, "ref"> & React.RefAttributes<IHandles | undefined>>;
@@ -1,41 +0,0 @@
1
- import React, {
2
- forwardRef,
3
- useImperativeHandle,
4
- useRef,
5
- useState,
6
- } from "react";
7
- import { Modalize } from "react-native-modalize";
8
- import type { IHandles } from "react-native-modalize/lib/options";
9
-
10
- type Props = React.ComponentProps<typeof Modalize>;
11
-
12
- export const UNSAFE_WrappedModalize = forwardRef<IHandles | undefined, Props>(
13
- (props, ref) => {
14
- const innerRef = useRef<IHandles | null>(null);
15
- const [openRenderId, setOpenRenderId] = useState(0);
16
-
17
- useImperativeHandle(
18
- ref,
19
- () => ({
20
- open(dest) {
21
- setOpenRenderId(id => id + 1);
22
- // Open on a fresh tick for additional safety
23
- requestAnimationFrame(() => {
24
- innerRef.current?.open(dest);
25
- });
26
- },
27
- close(dest) {
28
- innerRef.current?.close(dest);
29
- },
30
- }),
31
- [],
32
- );
33
-
34
- // Use a unique key to force a remount, ensuring we get fresh gesture handler nodes within modalize
35
- return (
36
- <Modalize key={`modalize-${openRenderId}`} ref={innerRef} {...props} />
37
- );
38
- },
39
- );
40
-
41
- UNSAFE_WrappedModalize.displayName = "UNSAFE_WrappedModalize";