@jobber/components-native 0.95.2-JOB-141866-990fa70.9 → 0.95.2-JOB-141866-ccf6ba8.10

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.
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { StyleProp, TextStyle, ViewStyle } from "react-native";
2
+ import type { LayoutChangeEvent, StyleProp, TextStyle, ViewStyle } from "react-native";
3
3
  import type { FieldError } from "react-hook-form";
4
4
  import type { IconNames } from "@jobber/design";
5
5
  export type Clearable = "never" | "while-editing" | "always";
@@ -82,7 +82,8 @@ export interface InputFieldWrapperProps {
82
82
  * Change the type of loading indicator to spinner or glimmer.
83
83
  */
84
84
  readonly loadingType?: "spinner" | "glimmer";
85
+ readonly scrollViewHackOnLayout?: (event: LayoutChangeEvent) => void;
85
86
  }
86
87
  export declare const INPUT_FIELD_WRAPPER_GLIMMERS_TEST_ID = "ATL-InputFieldWrapper-Glimmers";
87
88
  export declare const INPUT_FIELD_WRAPPER_SPINNER_TEST_ID = "ATL-InputFieldWrapper-Spinner";
88
- export declare function InputFieldWrapper({ invalid, disabled, placeholder, assistiveText, prefix, suffix, placeholderMode, hasValue, error, focused, children, onClear, showClearAction, styleOverride, toolbar, toolbarVisibility, loading, loadingType, }: InputFieldWrapperProps): React.JSX.Element;
89
+ export declare function InputFieldWrapper({ invalid, disabled, placeholder, assistiveText, prefix, suffix, placeholderMode, hasValue, error, focused, children, onClear, showClearAction, styleOverride, toolbar, toolbarVisibility, loading, loadingType, scrollViewHackOnLayout, }: InputFieldWrapperProps): React.JSX.Element;
@@ -6,11 +6,6 @@ import type { RegisterOptions } from "react-hook-form";
6
6
  import type { IconNames } from "@jobber/design";
7
7
  import type { Clearable } from "@jobber/hooks";
8
8
  import type { InputFieldStyleOverride, InputFieldWrapperProps } from "../InputFieldWrapper/InputFieldWrapper";
9
- /**
10
- * Buffer zone in pixels for offscreen detection.
11
- * This makes the detection more sensitive by marking the component as offscreen
12
- * even if it's technically still visible but within this buffer distance from the edge.
13
- */
14
9
  export interface InputTextProps extends Pick<InputFieldWrapperProps, "toolbar" | "toolbarVisibility" | "loading" | "loadingType"> {
15
10
  /**
16
11
  * Highlights the field red and shows message below (if string) to indicate an error
@@ -19,7 +19,5 @@ export interface InputAccessoriesContextProps {
19
19
  readonly onFocusNext: () => void;
20
20
  readonly onFocusPrevious: () => void;
21
21
  readonly setFocusedInput: (name: string) => void;
22
- readonly isScrolling: boolean;
23
- readonly setIsScrolling: (isScrolling: boolean) => void;
24
22
  }
25
23
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.95.2-JOB-141866-990fa70.9+990fa7081",
3
+ "version": "0.95.2-JOB-141866-ccf6ba8.10+ccf6ba8d6",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -96,5 +96,5 @@
96
96
  "react-native-safe-area-context": "^5.4.0",
97
97
  "react-native-svg": ">=12.0.0"
98
98
  },
99
- "gitHead": "990fa708191391b1d9f973c5eccd4dd8ec532424"
99
+ "gitHead": "ccf6ba8d6a687be9144debdfeab7c09525685fb0"
100
100
  }
package/src/Form/Form.tsx CHANGED
@@ -26,10 +26,7 @@ import { FormSaveButton } from "./components/FormSaveButton";
26
26
  import { useSaveButtonPosition } from "./hooks/useSaveButtonPosition";
27
27
  import { FormCache } from "./components/FormCache/FormCache";
28
28
  import { useAtlantisFormContext } from "./context/AtlantisFormContext";
29
- import {
30
- InputAccessoriesProvider,
31
- useInputAccessoriesContext,
32
- } from "../InputText";
29
+ import { InputAccessoriesProvider } from "../InputText";
33
30
  import { tokens } from "../utils/design";
34
31
  import { ErrorMessageProvider } from "../ErrorMessageWrapper";
35
32
 
@@ -73,7 +70,6 @@ function InternalForm<T extends FieldValues, S>({
73
70
  const { scrollViewRef, bottomViewRef, scrollToTop } = useFormViewRefs();
74
71
  const [saveButtonHeight, setSaveButtonHeight] = useState(0);
75
72
  const [messageBannerHeight, setMessageBannerHeight] = useState(0);
76
- const { setIsScrolling } = useInputAccessoriesContext();
77
73
  const {
78
74
  formMethods,
79
75
  handleSubmit,
@@ -181,12 +177,6 @@ function InternalForm<T extends FieldValues, S>({
181
177
  contentContainerStyle={
182
178
  !keyboardHeight && styles.scrollContentContainer
183
179
  }
184
- onScrollBeginDrag={() => {
185
- setIsScrolling(true);
186
- }}
187
- onScrollEndDrag={() => {
188
- setIsScrolling(false);
189
- }}
190
180
  >
191
181
  <View
192
182
  onLayout={({ nativeEvent }) => {
@@ -1,5 +1,10 @@
1
1
  import React from "react";
2
- import type { StyleProp, TextStyle, ViewStyle } from "react-native";
2
+ import type {
3
+ LayoutChangeEvent,
4
+ StyleProp,
5
+ TextStyle,
6
+ ViewStyle,
7
+ } from "react-native";
3
8
  import { Text as RNText, View } from "react-native";
4
9
  import type { FieldError } from "react-hook-form";
5
10
  import type { IconNames } from "@jobber/design";
@@ -112,6 +117,8 @@ export interface InputFieldWrapperProps {
112
117
  * Change the type of loading indicator to spinner or glimmer.
113
118
  */
114
119
  readonly loadingType?: "spinner" | "glimmer";
120
+
121
+ readonly scrollViewHackOnLayout?: (event: LayoutChangeEvent) => void;
115
122
  }
116
123
 
117
124
  export const INPUT_FIELD_WRAPPER_GLIMMERS_TEST_ID =
@@ -138,6 +145,7 @@ export function InputFieldWrapper({
138
145
  toolbarVisibility = "while-editing",
139
146
  loading = false,
140
147
  loadingType = "spinner",
148
+ scrollViewHackOnLayout,
141
149
  }: InputFieldWrapperProps) {
142
150
  fieldAffixRequiredPropsCheck([prefix, suffix]);
143
151
  const handleClear = onClear ?? noopClear;
@@ -164,6 +172,7 @@ export function InputFieldWrapper({
164
172
  disabled && styles.disabled,
165
173
  styleOverride?.container,
166
174
  ]}
175
+ onLayout={scrollViewHackOnLayout}
167
176
  >
168
177
  <View style={styles.field}>
169
178
  {prefix?.icon && (
@@ -1,6 +1,7 @@
1
1
  import type { Ref, SyntheticEvent } from "react";
2
2
  import React, {
3
3
  forwardRef,
4
+ useDeferredValue,
4
5
  useEffect,
5
6
  useImperativeHandle,
6
7
  useMemo,
@@ -22,13 +23,14 @@ 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";
25
- import { useFormController } from "../hooks";
26
+ import { useFormController, useKeyboardVisibility } from "../hooks";
26
27
  import type {
27
28
  InputFieldStyleOverride,
28
29
  InputFieldWrapperProps,
29
30
  } from "../InputFieldWrapper/InputFieldWrapper";
30
31
  import { InputFieldWrapper } from "../InputFieldWrapper";
31
32
  import { useCommonInputStyles } from "../InputFieldWrapper/CommonInputStyles.style";
33
+ import { useScreenInformation } from "../Form/hooks/useScreenInformation";
32
34
  // import { useScreenInformation } from "../Form/hooks/useScreenInformation";
33
35
 
34
36
  /**
@@ -37,7 +39,7 @@ import { useCommonInputStyles } from "../InputFieldWrapper/CommonInputStyles.sty
37
39
  * even if it's technically still visible but within this buffer distance from the edge.
38
40
  */
39
41
  // 44 (accessory bar height) + 20 (buffer)
40
- // const KEYBOARD_AWARE_DETECTION_BUFFER = 64;
42
+ const KEYBOARD_AWARE_DETECTION_BUFFER = 40;
41
43
 
42
44
  export interface InputTextProps
43
45
  extends Pick<
@@ -341,7 +343,6 @@ function InputTextInternal(
341
343
  unregister,
342
344
  setFocusedInput,
343
345
  canFocusNext,
344
- isScrolling,
345
346
  onFocusNext,
346
347
  } = useInputAccessoriesContext();
347
348
  useEffect(() => {
@@ -382,13 +383,25 @@ function InputTextInternal(
382
383
 
383
384
  const styles = useStyles();
384
385
  const commonInputStyles = useCommonInputStyles();
385
- // const { headerHeight, windowHeight } = useScreenInformation();
386
- // State to track if the InputText component can fully fit on screen
387
- // (i.e., it's completely visible). Use this state to handle visibility issues.
388
- // const [canFullyFitOnScreen, setCanFullyFitOnScreen] = useState(true);
386
+ const { headerHeight, windowHeight } = useScreenInformation();
387
+ const { keyboardHeight } = useKeyboardVisibility();
388
+ const maxHeight = useDeferredValue(
389
+ windowHeight -
390
+ headerHeight -
391
+ keyboardHeight -
392
+ KEYBOARD_AWARE_DETECTION_BUFFER,
393
+ );
394
+ const [inputHeight, setInputHeight] = useState(0);
395
+
396
+ const enableScroll = useDeferredValue(inputHeight > maxHeight);
389
397
 
390
398
  return (
391
399
  <InputFieldWrapper
400
+ scrollViewHackOnLayout={event => {
401
+ event.target?.measureInWindow((_, y, __, height) => {
402
+ setInputHeight(height);
403
+ });
404
+ }}
392
405
  prefix={prefix}
393
406
  suffix={suffix}
394
407
  hasValue={hasValue}
@@ -408,20 +421,6 @@ function InputTextInternal(
408
421
  loadingType={loadingType}
409
422
  >
410
423
  <TextInput
411
- // onLayout={(event: LayoutChangeEvent) => {
412
- // event.target?.measureInWindow((_, y, __, height) => {
413
- // // Check if component can't fully fit on screen (height only)
414
- // // Account for headerHeight at the top of the screen and buffer zone
415
- // const visibleTop = headerHeight + KEYBOARD_AWARE_DETECTION_BUFFER; // Top of visible area (below header) with buffer
416
- // const visibleBottom =
417
- // windowHeight - KEYBOARD_AWARE_DETECTION_BUFFER; // Bottom of visible area with buffer
418
- // const isOffScreen =
419
- // y < visibleTop || // Top edge is behind or above the header (with buffer)
420
- // y + height > visibleBottom; // Bottom edge is below the window (with buffer)
421
-
422
- // setCanFullyFitOnScreen(!isOffScreen);
423
- // });
424
- // }}
425
424
  inputAccessoryViewID={inputAccessoryID || undefined}
426
425
  testID={testID}
427
426
  autoCapitalize={autoCapitalize}
@@ -441,10 +440,7 @@ function InputTextInternal(
441
440
  styleOverride?.inputText,
442
441
  loading && loadingType === "glimmer" && { color: "transparent" },
443
442
  ]}
444
- // Prevent focus during scroll for multiline inputs to avoid
445
- // the input focusing when the user is trying to scroll the form
446
- readOnly={readonly || (multiline && isScrolling && !focused)}
447
- // readOnly={readonly}
443
+ readOnly={readonly}
448
444
  editable={!disabled}
449
445
  keyboardType={keyboard}
450
446
  value={inputTransform(internalValue)}
@@ -455,7 +451,7 @@ function InputTextInternal(
455
451
  // State for tracking if the input should be scrollable.
456
452
  // This is tech debt related to an issue where keyboard aware scrollview doesn't work if `scrollEnabled` is true. However,
457
453
  // when `scrollEnabled` is false it causes an issue where super long text inputs will jump to the top when a new line is added to the bottom of the input.
458
- scrollEnabled={Platform.OS === "ios"}
454
+ scrollEnabled={Platform.OS === "ios" && enableScroll}
459
455
  textContentType={textContentType}
460
456
  onChangeText={handleChangeText}
461
457
  onSubmitEditing={handleOnSubmitEditing}
@@ -12,8 +12,6 @@ const inputAccessoriesContextDefaultValues: InputAccessoriesContextProps = {
12
12
  onFocusNext: () => undefined,
13
13
  onFocusPrevious: () => undefined,
14
14
  setFocusedInput: () => undefined,
15
- isScrolling: false,
16
- setIsScrolling: () => undefined,
17
15
  };
18
16
 
19
17
  export const InputAccessoriesContext = createContext(
@@ -8,6 +8,7 @@ import { InputText } from "../InputText";
8
8
  const mockUseFormController = jest.fn();
9
9
  jest.mock("../../hooks", () => {
10
10
  return {
11
+ ...jest.requireActual("../../hooks"),
11
12
  useFormController: (
12
13
  ...args: [{ name: string; value: string; validations: unknown }]
13
14
  ) => mockUseFormController(...args),
@@ -27,8 +27,6 @@ export function InputAccessoriesProvider({
27
27
  setElements,
28
28
  previousKey,
29
29
  nextKey,
30
- isScrolling,
31
- setIsScrolling,
32
30
  } = useInputAccessoriesProviderState();
33
31
 
34
32
  const colorScheme = useColorScheme();
@@ -48,8 +46,6 @@ export function InputAccessoriesProvider({
48
46
  onFocusNext,
49
47
  onFocusPrevious,
50
48
  setFocusedInput,
51
- isScrolling,
52
- setIsScrolling,
53
49
  }}
54
50
  >
55
51
  {children}
@@ -100,7 +96,6 @@ function useInputAccessoriesProviderState() {
100
96
  const [canFocusNext, setCanFocusNext] = useState(false);
101
97
  const [canFocusPrevious, setCanFocusPrevious] = useState(false);
102
98
  const [elements, setElements] = useState<Record<string, () => void>>({});
103
- const [isScrolling, setIsScrolling] = useState(false);
104
99
 
105
100
  const keys = Object.keys(elements);
106
101
  const selectedIndex = keys.findIndex(key => key === focusedInput);
@@ -124,7 +119,5 @@ function useInputAccessoriesProviderState() {
124
119
  setCanFocusPrevious,
125
120
  elements,
126
121
  setElements,
127
- isScrolling,
128
- setIsScrolling,
129
122
  };
130
123
  }
@@ -25,6 +25,4 @@ export interface InputAccessoriesContextProps {
25
25
  readonly onFocusNext: () => void;
26
26
  readonly onFocusPrevious: () => void;
27
27
  readonly setFocusedInput: (name: string) => void;
28
- readonly isScrolling: boolean;
29
- readonly setIsScrolling: (isScrolling: boolean) => void;
30
28
  }