@jobber/components-native 0.95.2-JOB-141866-1ab1d84.4 → 0.95.2-JOB-141866-80fe04c.8

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-1ab1d84.4+1ab1d8407",
3
+ "version": "0.95.2-JOB-141866-80fe04c.8+80fe04c86",
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": "1ab1d8407fce8e7c5ea822477740ec1d5b8f1abb"
99
+ "gitHead": "80fe04c86fc91e07591231460c32bc3194072bdc"
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
@@ -1,8 +1,7 @@
1
- import React, { useState } from "react";
1
+ import React, { useMemo, 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";
@@ -71,6 +70,7 @@ function InternalForm<T extends FieldValues, S>({
71
70
  const { scrollViewRef, bottomViewRef, scrollToTop } = useFormViewRefs();
72
71
  const [saveButtonHeight, setSaveButtonHeight] = useState(0);
73
72
  const [messageBannerHeight, setMessageBannerHeight] = useState(0);
73
+ // const { setIsScrolling } = useInputAccessoriesContext();
74
74
  const {
75
75
  formMethods,
76
76
  handleSubmit,
@@ -108,8 +108,12 @@ function InternalForm<T extends FieldValues, S>({
108
108
  const [isSecondaryActionLoading, setIsSecondaryActionLoading] =
109
109
  useState<boolean>(false);
110
110
 
111
- const extraViewHeight = paddingBottom + KEYBOARD_SAVE_BUTTON_DISTANCE;
112
- const calculatedKeyboardHeight = keyboardHeight - extraViewHeight;
111
+ const { calculatedKeyboardHeight } = useMemo(() => {
112
+ return {
113
+ calculatedKeyboardHeight:
114
+ keyboardHeight - (paddingBottom + KEYBOARD_SAVE_BUTTON_DISTANCE),
115
+ };
116
+ }, [paddingBottom, keyboardHeight]);
113
117
 
114
118
  useScrollToError({
115
119
  formState: formMethods.formState,
@@ -122,9 +126,8 @@ function InternalForm<T extends FieldValues, S>({
122
126
 
123
127
  const keyboardProps = Platform.select({
124
128
  ios: {
125
- // onKeyboardDidHide: handleKeyboardHide,
126
- // onKeyboardDidShow: handleKeyboardShow,
127
- onKeyboardDidChangeFrame: handleKeyboardDidChangeFrame,
129
+ onKeyboardWillHide: handleKeyboardHide,
130
+ onKeyboardWillShow: handleKeyboardShow,
128
131
  },
129
132
  android: {
130
133
  onKeyboardDidHide: handleKeyboardHide,
@@ -132,10 +135,6 @@ function InternalForm<T extends FieldValues, S>({
132
135
  },
133
136
  });
134
137
 
135
- const onLayout = (event: LayoutChangeEvent) => {
136
- setMessageBannerHeight(event.nativeEvent.layout.height);
137
- };
138
-
139
138
  const styles = useStyles();
140
139
 
141
140
  const { edgeToEdgeEnabled } = useAtlantisFormContext();
@@ -179,13 +178,23 @@ function InternalForm<T extends FieldValues, S>({
179
178
  contentContainerStyle={
180
179
  !keyboardHeight && styles.scrollContentContainer
181
180
  }
181
+ // onScrollBeginDrag={() => {
182
+ // setIsScrolling(true);
183
+ // }}
184
+ // onScrollEndDrag={() => {
185
+ // setIsScrolling(false);
186
+ // }}
182
187
  >
183
188
  <View
184
189
  onLayout={({ nativeEvent }) => {
185
190
  setFormContentHeight(nativeEvent.layout.height);
186
191
  }}
187
192
  >
188
- <View onLayout={onLayout}>
193
+ <View
194
+ onLayout={({ nativeEvent }) => {
195
+ setMessageBannerHeight(nativeEvent.layout.height);
196
+ }}
197
+ >
189
198
  <FormMessageBanner bannerMessages={bannerMessages} />
190
199
  <FormErrorBanner
191
200
  networkError={bannerErrors?.networkError}
@@ -230,21 +239,6 @@ function InternalForm<T extends FieldValues, S>({
230
239
  </FormProvider>
231
240
  );
232
241
 
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
242
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
249
243
  function handleKeyboardShow(frames: Record<string, any>) {
250
244
  setKeyboardScreenY(frames.endCoordinates.screenY);
@@ -29,6 +29,15 @@ import type {
29
29
  } from "../InputFieldWrapper/InputFieldWrapper";
30
30
  import { InputFieldWrapper } from "../InputFieldWrapper";
31
31
  import { useCommonInputStyles } from "../InputFieldWrapper/CommonInputStyles.style";
32
+ import { useScreenInformation } from "../Form/hooks/useScreenInformation";
33
+
34
+ /**
35
+ * Buffer zone in pixels for offscreen detection.
36
+ * This makes the detection more sensitive by marking the component as offscreen
37
+ * even if it's technically still visible but within this buffer distance from the edge.
38
+ */
39
+ // 44 (accessory bar height) + 20 (buffer)
40
+ const KEYBOARD_AWARE_DETECTION_BUFFER = 64;
32
41
 
33
42
  export interface InputTextProps
34
43
  extends Pick<
@@ -372,6 +381,10 @@ function InputTextInternal(
372
381
 
373
382
  const styles = useStyles();
374
383
  const commonInputStyles = useCommonInputStyles();
384
+ // const { headerHeight, windowHeight } = useScreenInformation();
385
+ // State to track if the InputText component can fully fit on screen
386
+ // (i.e., it's completely visible). Use this state to handle visibility issues.
387
+ // const [canFullyFitOnScreen, setCanFullyFitOnScreen] = useState(true);
375
388
 
376
389
  return (
377
390
  <InputFieldWrapper
@@ -394,6 +407,20 @@ function InputTextInternal(
394
407
  loadingType={loadingType}
395
408
  >
396
409
  <TextInput
410
+ // onLayout={(event: LayoutChangeEvent) => {
411
+ // event.target?.measureInWindow((_, y, __, height) => {
412
+ // // Check if component can't fully fit on screen (height only)
413
+ // // Account for headerHeight at the top of the screen and buffer zone
414
+ // const visibleTop = headerHeight + KEYBOARD_AWARE_DETECTION_BUFFER; // Top of visible area (below header) with buffer
415
+ // const visibleBottom =
416
+ // windowHeight - KEYBOARD_AWARE_DETECTION_BUFFER; // Bottom of visible area with buffer
417
+ // const isOffScreen =
418
+ // y < visibleTop || // Top edge is behind or above the header (with buffer)
419
+ // y + height > visibleBottom; // Bottom edge is below the window (with buffer)
420
+
421
+ // setCanFullyFitOnScreen(!isOffScreen);
422
+ // });
423
+ // }}
397
424
  inputAccessoryViewID={inputAccessoryID || undefined}
398
425
  testID={testID}
399
426
  autoCapitalize={autoCapitalize}
@@ -413,6 +440,9 @@ function InputTextInternal(
413
440
  styleOverride?.inputText,
414
441
  loading && loadingType === "glimmer" && { color: "transparent" },
415
442
  ]}
443
+ // Prevent focus during scroll for multiline inputs to avoid
444
+ // the input focusing when the user is trying to scroll the form
445
+ // readOnly={readonly || (multiline && isScrolling && !focused)}
416
446
  readOnly={readonly}
417
447
  editable={!disabled}
418
448
  keyboardType={keyboard}
@@ -420,7 +450,11 @@ function InputTextInternal(
420
450
  autoFocus={autoFocus}
421
451
  autoComplete={autoComplete}
422
452
  multiline={multiline}
423
- scrollEnabled={false}
453
+ // Makes sure it doesn't jump to the top of the screen when the keyboard is shown and a new line is added.
454
+ // State for tracking if the input should be scrollable.
455
+ // This is tech debt related to an issue where keyboard aware scrollview doesn't work if `scrollEnabled` is true. However,
456
+ // 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.
457
+ scrollEnabled={Platform.OS === "ios"}
424
458
  textContentType={textContentType}
425
459
  onChangeText={handleChangeText}
426
460
  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";