@jobber/components-native 0.95.2-JOB-141866-cab4e3f.3 → 0.95.2-JOB-141866-acfcddc.7

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.
@@ -19,5 +19,7 @@ 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;
22
24
  }
23
25
  export {};
@@ -1,2 +1,3 @@
1
1
  export * from "./useFormController";
2
2
  export * from "./useIsScreenReaderEnabled";
3
+ export * from "./useKeyboardVisibility";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.95.2-JOB-141866-cab4e3f.3+cab4e3feb",
3
+ "version": "0.95.2-JOB-141866-acfcddc.7+acfcddc57",
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": "cab4e3febe1c59269f69b54e3b442122ac0a3cde"
99
+ "gitHead": "acfcddc57c91d179ef43e2ed44fd25bd3cae6405"
100
100
  }
@@ -18,7 +18,6 @@ import {
18
18
  useWindowDimensions,
19
19
  } from "react-native";
20
20
  import { Portal } from "react-native-portalize";
21
- import { useKeyboardVisibility } from "./hooks/useKeyboardVisibility";
22
21
  import { useStyles } from "./ContentOverlay.style";
23
22
  import { useViewLayoutHeight } from "./hooks/useViewLayoutHeight";
24
23
  import type {
@@ -27,7 +26,7 @@ import type {
27
26
  ModalBackgroundColor,
28
27
  } from "./types";
29
28
  import { UNSAFE_WrappedModalize } from "./UNSAFE_WrappedModalize";
30
- import { useIsScreenReaderEnabled } from "../hooks";
29
+ import { useIsScreenReaderEnabled, useKeyboardVisibility } from "../hooks";
31
30
  import { IconButton } from "../IconButton";
32
31
  import { Heading } from "../Heading";
33
32
  import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
package/src/Form/Form.tsx CHANGED
@@ -2,7 +2,6 @@ import React, { useState } from "react";
2
2
  import type { FieldValues } from "react-hook-form";
3
3
  import { FormProvider } from "react-hook-form";
4
4
  import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
5
- import type { LayoutChangeEvent } from "react-native";
6
5
  import { Keyboard, Platform, View, findNodeHandle } from "react-native";
7
6
  import { useStyles } from "./Form.style";
8
7
  import { FormErrorBanner } from "./components/FormErrorBanner";
@@ -27,7 +26,10 @@ import { FormSaveButton } from "./components/FormSaveButton";
27
26
  import { useSaveButtonPosition } from "./hooks/useSaveButtonPosition";
28
27
  import { FormCache } from "./components/FormCache/FormCache";
29
28
  import { useAtlantisFormContext } from "./context/AtlantisFormContext";
30
- import { InputAccessoriesProvider } from "../InputText";
29
+ import {
30
+ InputAccessoriesProvider,
31
+ useInputAccessoriesContext,
32
+ } from "../InputText";
31
33
  import { tokens } from "../utils/design";
32
34
  import { ErrorMessageProvider } from "../ErrorMessageWrapper";
33
35
 
@@ -71,6 +73,7 @@ function InternalForm<T extends FieldValues, S>({
71
73
  const { scrollViewRef, bottomViewRef, scrollToTop } = useFormViewRefs();
72
74
  const [saveButtonHeight, setSaveButtonHeight] = useState(0);
73
75
  const [messageBannerHeight, setMessageBannerHeight] = useState(0);
76
+ const { setIsScrolling } = useInputAccessoriesContext();
74
77
  const {
75
78
  formMethods,
76
79
  handleSubmit,
@@ -122,9 +125,8 @@ function InternalForm<T extends FieldValues, S>({
122
125
 
123
126
  const keyboardProps = Platform.select({
124
127
  ios: {
125
- onKeyboardDidHide: handleKeyboardHide,
126
- onKeyboardDidShow: handleKeyboardShow,
127
- onKeyboardDidChangeFrame: handleKeyboardDidChangeFrame,
128
+ onKeyboardWillHide: handleKeyboardHide,
129
+ onKeyboardWillShow: handleKeyboardShow,
128
130
  },
129
131
  android: {
130
132
  onKeyboardDidHide: handleKeyboardHide,
@@ -132,10 +134,6 @@ function InternalForm<T extends FieldValues, S>({
132
134
  },
133
135
  });
134
136
 
135
- const onLayout = (event: LayoutChangeEvent) => {
136
- setMessageBannerHeight(event.nativeEvent.layout.height);
137
- };
138
-
139
137
  const styles = useStyles();
140
138
 
141
139
  const { edgeToEdgeEnabled } = useAtlantisFormContext();
@@ -179,13 +177,23 @@ function InternalForm<T extends FieldValues, S>({
179
177
  contentContainerStyle={
180
178
  !keyboardHeight && styles.scrollContentContainer
181
179
  }
180
+ onScrollBeginDrag={() => {
181
+ setIsScrolling(true);
182
+ }}
183
+ onScrollEndDrag={() => {
184
+ setIsScrolling(false);
185
+ }}
182
186
  >
183
187
  <View
184
188
  onLayout={({ nativeEvent }) => {
185
189
  setFormContentHeight(nativeEvent.layout.height);
186
190
  }}
187
191
  >
188
- <View onLayout={onLayout}>
192
+ <View
193
+ onLayout={({ nativeEvent }) => {
194
+ setMessageBannerHeight(nativeEvent.layout.height);
195
+ }}
196
+ >
189
197
  <FormMessageBanner bannerMessages={bannerMessages} />
190
198
  <FormErrorBanner
191
199
  networkError={bannerErrors?.networkError}
@@ -230,21 +238,6 @@ function InternalForm<T extends FieldValues, S>({
230
238
  </FormProvider>
231
239
  );
232
240
 
233
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
234
- function handleKeyboardDidChangeFrame(frames: Record<string, any>) {
235
- if (
236
- frames &&
237
- "endCoordinates" in frames &&
238
- "height" in frames.endCoordinates &&
239
- typeof frames.endCoordinates.height === "number" &&
240
- frames.endCoordinates.height > keyboardHeight
241
- ) {
242
- handleKeyboardShow(frames);
243
- } else {
244
- handleKeyboardHide();
245
- }
246
- }
247
-
248
241
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
249
242
  function handleKeyboardShow(frames: Record<string, any>) {
250
243
  setKeyboardScreenY(frames.endCoordinates.screenY);
@@ -9,6 +9,7 @@ import React, {
9
9
  } from "react";
10
10
  import type {
11
11
  FocusEvent,
12
+ LayoutChangeEvent,
12
13
  ReturnKeyTypeOptions,
13
14
  StyleProp,
14
15
  TextInputProps,
@@ -29,6 +30,15 @@ import type {
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";
34
+
35
+ /**
36
+ * Buffer zone in pixels for offscreen detection.
37
+ * This makes the detection more sensitive by marking the component as offscreen
38
+ * even if it's technically still visible but within this buffer distance from the edge.
39
+ */
40
+ // 44 (accessory bar height) + 20 (buffer)
41
+ const KEYBOARD_AWARE_DETECTION_BUFFER = 64;
32
42
 
33
43
  export interface InputTextProps
34
44
  extends Pick<
@@ -333,6 +343,7 @@ function InputTextInternal(
333
343
  setFocusedInput,
334
344
  canFocusNext,
335
345
  onFocusNext,
346
+ isScrolling,
336
347
  } = useInputAccessoriesContext();
337
348
  useEffect(() => {
338
349
  _name &&
@@ -372,6 +383,10 @@ function InputTextInternal(
372
383
 
373
384
  const styles = useStyles();
374
385
  const commonInputStyles = useCommonInputStyles();
386
+ const { headerHeight, windowHeight } = useScreenInformation();
387
+ // State to track if the InputText component can fully fit on screen
388
+ // (i.e., it's completely visible). Use this state to handle visibility issues.
389
+ const [canFullyFitOnScreen, setCanFullyFitOnScreen] = useState(true);
375
390
 
376
391
  return (
377
392
  <InputFieldWrapper
@@ -394,6 +409,20 @@ function InputTextInternal(
394
409
  loadingType={loadingType}
395
410
  >
396
411
  <TextInput
412
+ onLayout={(event: LayoutChangeEvent) => {
413
+ event.target?.measureInWindow((_, y, __, height) => {
414
+ // Check if component can't fully fit on screen (height only)
415
+ // Account for headerHeight at the top of the screen and buffer zone
416
+ const visibleTop = headerHeight + KEYBOARD_AWARE_DETECTION_BUFFER; // Top of visible area (below header) with buffer
417
+ const visibleBottom =
418
+ windowHeight - KEYBOARD_AWARE_DETECTION_BUFFER; // Bottom of visible area with buffer
419
+ const isOffScreen =
420
+ y < visibleTop || // Top edge is behind or above the header (with buffer)
421
+ y + height > visibleBottom; // Bottom edge is below the window (with buffer)
422
+
423
+ setCanFullyFitOnScreen(!isOffScreen);
424
+ });
425
+ }}
397
426
  inputAccessoryViewID={inputAccessoryID || undefined}
398
427
  testID={testID}
399
428
  autoCapitalize={autoCapitalize}
@@ -413,14 +442,21 @@ function InputTextInternal(
413
442
  styleOverride?.inputText,
414
443
  loading && loadingType === "glimmer" && { color: "transparent" },
415
444
  ]}
416
- readOnly={readonly}
445
+ // Prevent focus during scroll for multiline inputs to avoid
446
+ // the input focusing when the user is trying to scroll the form
447
+ readOnly={readonly || (multiline && isScrolling && !focused)}
448
+ // readOnly={readonly}
417
449
  editable={!disabled}
418
450
  keyboardType={keyboard}
419
451
  value={inputTransform(internalValue)}
420
452
  autoFocus={autoFocus}
421
453
  autoComplete={autoComplete}
422
454
  multiline={multiline}
423
- scrollEnabled={false}
455
+ // Makes sure it doesn't jump to the top of the screen when the keyboard is shown and a new line is added.
456
+ // State for tracking if the input should be scrollable.
457
+ // This is tech debt related to an issue where keyboard aware scrollview doesn't work if `scrollEnabled` is true. However,
458
+ // 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.
459
+ scrollEnabled={Platform.OS === "ios" && !canFullyFitOnScreen}
424
460
  textContentType={textContentType}
425
461
  onChangeText={handleChangeText}
426
462
  onSubmitEditing={handleOnSubmitEditing}
@@ -12,6 +12,8 @@ const inputAccessoriesContextDefaultValues: InputAccessoriesContextProps = {
12
12
  onFocusNext: () => undefined,
13
13
  onFocusPrevious: () => undefined,
14
14
  setFocusedInput: () => undefined,
15
+ isScrolling: false,
16
+ setIsScrolling: () => undefined,
15
17
  };
16
18
 
17
19
  export const InputAccessoriesContext = createContext(
@@ -27,6 +27,8 @@ export function InputAccessoriesProvider({
27
27
  setElements,
28
28
  previousKey,
29
29
  nextKey,
30
+ isScrolling,
31
+ setIsScrolling,
30
32
  } = useInputAccessoriesProviderState();
31
33
 
32
34
  const colorScheme = useColorScheme();
@@ -46,6 +48,8 @@ export function InputAccessoriesProvider({
46
48
  onFocusNext,
47
49
  onFocusPrevious,
48
50
  setFocusedInput,
51
+ isScrolling,
52
+ setIsScrolling,
49
53
  }}
50
54
  >
51
55
  {children}
@@ -96,6 +100,7 @@ function useInputAccessoriesProviderState() {
96
100
  const [canFocusNext, setCanFocusNext] = useState(false);
97
101
  const [canFocusPrevious, setCanFocusPrevious] = useState(false);
98
102
  const [elements, setElements] = useState<Record<string, () => void>>({});
103
+ const [isScrolling, setIsScrolling] = useState(false);
99
104
 
100
105
  const keys = Object.keys(elements);
101
106
  const selectedIndex = keys.findIndex(key => key === focusedInput);
@@ -119,5 +124,7 @@ function useInputAccessoriesProviderState() {
119
124
  setCanFocusPrevious,
120
125
  elements,
121
126
  setElements,
127
+ isScrolling,
128
+ setIsScrolling,
122
129
  };
123
130
  }
@@ -25,4 +25,6 @@ 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;
28
30
  }
@@ -1,2 +1,3 @@
1
1
  export * from "./useFormController";
2
2
  export * from "./useIsScreenReaderEnabled";
3
+ export * from "./useKeyboardVisibility";