@jobber/components-native 0.62.3 → 0.63.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 (32) hide show
  1. package/dist/package.json +3 -3
  2. package/dist/src/Glimmer/Glimmer.js +42 -0
  3. package/dist/src/Glimmer/Glimmer.shape.style.js +16 -0
  4. package/dist/src/Glimmer/Glimmer.size.style.js +9 -0
  5. package/dist/src/Glimmer/Glimmer.style.js +20 -0
  6. package/dist/src/Glimmer/index.js +1 -0
  7. package/dist/src/InputFieldWrapper/InputFieldWrapper.js +20 -2
  8. package/dist/src/InputFieldWrapper/InputFieldWrapper.style.js +22 -0
  9. package/dist/src/InputText/InputText.js +4 -3
  10. package/dist/src/index.js +6 -5
  11. package/dist/tsconfig.tsbuildinfo +1 -1
  12. package/dist/types/src/Glimmer/Glimmer.d.ts +31 -0
  13. package/dist/types/src/Glimmer/Glimmer.shape.style.d.ts +14 -0
  14. package/dist/types/src/Glimmer/Glimmer.size.style.d.ts +17 -0
  15. package/dist/types/src/Glimmer/Glimmer.style.d.ts +18 -0
  16. package/dist/types/src/Glimmer/index.d.ts +1 -0
  17. package/dist/types/src/InputFieldWrapper/InputFieldWrapper.d.ts +11 -1
  18. package/dist/types/src/InputFieldWrapper/InputFieldWrapper.style.d.ts +22 -0
  19. package/dist/types/src/InputText/InputText.d.ts +1 -1
  20. package/dist/types/src/index.d.ts +6 -5
  21. package/package.json +3 -3
  22. package/src/Glimmer/Glimmer.shape.style.ts +17 -0
  23. package/src/Glimmer/Glimmer.size.style.ts +10 -0
  24. package/src/Glimmer/Glimmer.style.ts +23 -0
  25. package/src/Glimmer/Glimmer.test.tsx +73 -0
  26. package/src/Glimmer/Glimmer.tsx +106 -0
  27. package/src/Glimmer/index.ts +1 -0
  28. package/src/InputFieldWrapper/InputFieldWrapper.style.ts +25 -0
  29. package/src/InputFieldWrapper/InputFieldWrapper.test.tsx +30 -0
  30. package/src/InputFieldWrapper/InputFieldWrapper.tsx +50 -1
  31. package/src/InputText/InputText.tsx +10 -1
  32. package/src/index.ts +6 -5
@@ -0,0 +1,31 @@
1
+ /// <reference types="react" />
2
+ import { sizeStyles } from "./Glimmer.size.style";
3
+ import { shapeStyles } from "./Glimmer.shape.style";
4
+ export type GlimmerShapes = keyof typeof shapeStyles;
5
+ export type GlimmerSizes = keyof typeof sizeStyles;
6
+ export type GlimmerTimings = "base" | "fast";
7
+ interface GlimmerProps {
8
+ /**
9
+ * Sets the size of the glimmer.
10
+ */
11
+ readonly shape?: GlimmerShapes;
12
+ /**
13
+ * Sets the shape of the glimmer.
14
+ *
15
+ * If you need a specific width, use the `width` prop.
16
+ */
17
+ readonly size?: GlimmerSizes;
18
+ /**
19
+ * Control how fast the shine moves from left to right. This is useful when
20
+ * the glimmer is used on smaller spaces.
21
+ */
22
+ readonly timing?: GlimmerTimings;
23
+ /**
24
+ * Adjust the width of the glimmer in px or % values.
25
+ */
26
+ readonly width?: number | `${number}%`;
27
+ }
28
+ export declare const GLIMMER_TEST_ID = "ATL-Glimmer";
29
+ export declare const GLIMMER_SHINE_TEST_ID = "ATL-Glimmer-Shine";
30
+ export declare function Glimmer({ width, shape, size, timing, }: GlimmerProps): JSX.Element;
31
+ export {};
@@ -0,0 +1,14 @@
1
+ export declare const shapeStyles: {
2
+ rectangle: {
3
+ width: string;
4
+ };
5
+ square: {
6
+ width: string;
7
+ aspectRatio: number;
8
+ };
9
+ circle: {
10
+ width: string;
11
+ aspectRatio: number;
12
+ borderRadius: number;
13
+ };
14
+ };
@@ -0,0 +1,17 @@
1
+ export declare const sizeStyles: {
2
+ small: {
3
+ height: number;
4
+ };
5
+ base: {
6
+ height: number;
7
+ };
8
+ large: {
9
+ height: number;
10
+ };
11
+ larger: {
12
+ height: number;
13
+ };
14
+ largest: {
15
+ height: number;
16
+ };
17
+ };
@@ -0,0 +1,18 @@
1
+ export declare const shineWidth: number;
2
+ export declare const styles: {
3
+ container: {
4
+ backgroundColor: string;
5
+ overflow: "hidden";
6
+ position: "relative";
7
+ width: string;
8
+ height: number;
9
+ borderRadius: number;
10
+ };
11
+ shine: {
12
+ position: "absolute";
13
+ top: number;
14
+ left: number;
15
+ width: number;
16
+ height: string;
17
+ };
18
+ };
@@ -0,0 +1 @@
1
+ export * from "./Glimmer";
@@ -67,5 +67,15 @@ export interface InputFieldWrapperProps {
67
67
  * Change the behaviour of when the toolbar becomes visible.
68
68
  */
69
69
  readonly toolbarVisibility?: "always" | "while-editing";
70
+ /**
71
+ * Show loading indicator.
72
+ */
73
+ readonly loading?: boolean;
74
+ /**
75
+ * Change the type of loading indicator to spinner or glimmer.
76
+ */
77
+ readonly loadingType?: "spinner" | "glimmer";
70
78
  }
71
- export declare function InputFieldWrapper({ invalid, disabled, placeholder, assistiveText, prefix, suffix, hasMiniLabel, hasValue, error, focused, children, onClear, showClearAction, styleOverride, toolbar, toolbarVisibility, }: InputFieldWrapperProps): JSX.Element;
79
+ export declare const INPUT_FIELD_WRAPPER_GLIMMERS_TEST_ID = "ATL-InputFieldWrapper-Glimmers";
80
+ export declare const INPUT_FIELD_WRAPPER_SPINNER_TEST_ID = "ATL-InputFieldWrapper-Spinner";
81
+ export declare function InputFieldWrapper({ invalid, disabled, placeholder, assistiveText, prefix, suffix, hasMiniLabel, hasValue, error, focused, children, onClear, showClearAction, styleOverride, toolbar, toolbarVisibility, loading, loadingType, }: InputFieldWrapperProps): JSX.Element;
@@ -91,8 +91,30 @@ export declare const styles: {
91
91
  zIndex: number;
92
92
  };
93
93
  toolbar: {
94
+ flexBasis: string;
94
95
  flexDirection: "row";
95
96
  gap: number;
96
97
  paddingBottom: number;
97
98
  };
99
+ loadingSpinner: {
100
+ justifyContent: "center";
101
+ paddingRight: number;
102
+ };
103
+ loadingGlimmers: {
104
+ position: "absolute";
105
+ top: number;
106
+ bottom: number;
107
+ left: number;
108
+ right: number;
109
+ gap: number;
110
+ paddingTop: number;
111
+ paddingRight: number;
112
+ backgroundColor: string;
113
+ overflow: "hidden";
114
+ };
115
+ loadingGlimmersHasValue: {
116
+ top: number;
117
+ paddingTop: number;
118
+ bottom: number;
119
+ };
98
120
  };
@@ -4,7 +4,7 @@ import { RegisterOptions } from "react-hook-form";
4
4
  import { IconNames } from "@jobber/design";
5
5
  import { Clearable } from "@jobber/hooks";
6
6
  import { InputFieldStyleOverride, InputFieldWrapperProps } from "../InputFieldWrapper/InputFieldWrapper";
7
- export interface InputTextProps extends Pick<InputFieldWrapperProps, "toolbar" | "toolbarVisibility"> {
7
+ export interface InputTextProps extends Pick<InputFieldWrapperProps, "toolbar" | "toolbarVisibility" | "loading" | "loadingType"> {
8
8
  /**
9
9
  * Highlights the field red and shows message below (if string) to indicate an error
10
10
  */
@@ -17,29 +17,30 @@ export * from "./Divider";
17
17
  export * from "./EmptyState";
18
18
  export * from "./ErrorMessageWrapper";
19
19
  export * from "./Flex";
20
- export * from "./FormatFile";
21
20
  export * from "./Form";
21
+ export * from "./FormatFile";
22
22
  export * from "./FormField";
23
+ export * from "./Glimmer";
23
24
  export * from "./Heading";
24
25
  export * from "./Icon";
25
26
  export * from "./IconButton";
26
- export * from "./InputFieldWrapper";
27
27
  export * from "./InputCurrency";
28
28
  export * from "./InputDate";
29
29
  export * from "./InputEmail";
30
+ export * from "./InputFieldWrapper";
30
31
  export * from "./InputNumber";
31
32
  export * from "./InputPassword";
32
33
  export * from "./InputPressable";
33
34
  export * from "./InputSearch";
34
- export * from "./InputTime";
35
35
  export * from "./InputText";
36
+ export * from "./InputTime";
36
37
  export * from "./Menu";
37
- export * from "./TextList";
38
- export * from "./ThumbnailList";
39
38
  export * from "./ProgressBar";
40
39
  export * from "./Select";
41
40
  export * from "./StatusLabel";
42
41
  export * from "./Switch";
43
42
  export * from "./Text";
43
+ export * from "./TextList";
44
+ export * from "./ThumbnailList";
44
45
  export * from "./Toast";
45
46
  export * from "./Typography";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.62.3",
3
+ "version": "0.63.0",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -36,7 +36,7 @@
36
36
  "build:clean": "rm -rf ./dist"
37
37
  },
38
38
  "dependencies": {
39
- "@jobber/design": "^0.56.3",
39
+ "@jobber/design": "^0.56.4",
40
40
  "@jobber/hooks": "^2.9.4",
41
41
  "@react-native-clipboard/clipboard": "^1.11.2",
42
42
  "@react-native-picker/picker": "^2.4.10",
@@ -84,5 +84,5 @@
84
84
  "react-native-safe-area-context": "^4.5.2",
85
85
  "react-native-svg": ">=12.0.0"
86
86
  },
87
- "gitHead": "49814f608cafabdde783c712972de4665cc6c10c"
87
+ "gitHead": "15034baa004c4a11f17338b96d6f2678f3183d56"
88
88
  }
@@ -0,0 +1,17 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../utils/design";
3
+
4
+ export const shapeStyles = StyleSheet.create({
5
+ rectangle: {
6
+ width: "100%",
7
+ },
8
+ square: {
9
+ width: "auto",
10
+ aspectRatio: 1 / 1,
11
+ },
12
+ circle: {
13
+ width: "auto",
14
+ aspectRatio: 1 / 1,
15
+ borderRadius: tokens["radius-circle"],
16
+ },
17
+ });
@@ -0,0 +1,10 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../utils/design";
3
+
4
+ export const sizeStyles = StyleSheet.create({
5
+ small: { height: tokens["space-small"] },
6
+ base: { height: tokens["space-base"] },
7
+ large: { height: tokens["space-large"] },
8
+ larger: { height: tokens["space-larger"] },
9
+ largest: { height: tokens["space-largest"] },
10
+ });
@@ -0,0 +1,23 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../utils/design";
3
+
4
+ export const shineWidth = tokens["space-largest"];
5
+
6
+ export const styles = StyleSheet.create({
7
+ container: {
8
+ backgroundColor: tokens["color-surface--background"],
9
+ overflow: "hidden",
10
+ position: "relative",
11
+ width: "100%",
12
+ height: tokens["space-base"],
13
+ borderRadius: tokens["radius-base"],
14
+ },
15
+
16
+ shine: {
17
+ position: "absolute",
18
+ top: 0,
19
+ left: 0,
20
+ width: shineWidth,
21
+ height: "100%",
22
+ },
23
+ });
@@ -0,0 +1,73 @@
1
+ import React from "react";
2
+ import {
3
+ act,
4
+ fireEvent,
5
+ render as renderComponent,
6
+ } from "@testing-library/react-native";
7
+ import { GLIMMER_SHINE_TEST_ID, GLIMMER_TEST_ID, Glimmer } from "./Glimmer";
8
+ import { tokens } from "../utils/design";
9
+
10
+ let screen: ReturnType<typeof renderComponent<typeof Glimmer>>;
11
+
12
+ function render<T>(...params: Parameters<typeof renderComponent<T>>) {
13
+ screen = renderComponent(...params);
14
+
15
+ return screen;
16
+ }
17
+
18
+ describe("Glimmer", () => {
19
+ it("renders a Glimmer with default styling", () => {
20
+ render(<Glimmer />);
21
+ const element = screen.getByTestId(GLIMMER_TEST_ID);
22
+
23
+ expect(element.props.style).toEqual(
24
+ expect.arrayContaining([
25
+ expect.objectContaining({ height: 16 }),
26
+ expect.objectContaining({ width: "100%" }),
27
+ ]),
28
+ );
29
+ });
30
+
31
+ it("renders a Glimmer with custom width", () => {
32
+ render(<Glimmer width={50} />);
33
+ const element = screen.getByTestId(GLIMMER_TEST_ID);
34
+
35
+ expect(element.props.style).toEqual(
36
+ expect.arrayContaining([expect.objectContaining({ width: 50 })]),
37
+ );
38
+ });
39
+
40
+ it("renders a Glimmer with custom percent width", () => {
41
+ render(<Glimmer width="50%" />);
42
+ const element = screen.getByTestId(GLIMMER_TEST_ID);
43
+
44
+ expect(element.props.style).toEqual(
45
+ expect.arrayContaining([expect.objectContaining({ width: "50%" })]),
46
+ );
47
+ });
48
+
49
+ it("renders sets the correct width", () => {
50
+ jest.useFakeTimers();
51
+ render(<Glimmer />);
52
+
53
+ act(() => {
54
+ fireEvent(screen.getByTestId(GLIMMER_TEST_ID), "onLayout", {
55
+ nativeEvent: { layout: { width: 300 } },
56
+ });
57
+ });
58
+
59
+ const element = screen.getByTestId(GLIMMER_SHINE_TEST_ID);
60
+
61
+ expect(element.props.style).toEqual(
62
+ expect.objectContaining({ transform: [{ translateX: -48 }] }),
63
+ );
64
+
65
+ jest.advanceTimersByTime(tokens["timing-loading--extended"]);
66
+
67
+ expect(element.props.style).toEqual(
68
+ expect.objectContaining({ transform: [{ translateX: 348 }] }),
69
+ );
70
+
71
+ jest.useRealTimers();
72
+ });
73
+ });
@@ -0,0 +1,106 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { Animated, Easing, LayoutChangeEvent, View } from "react-native";
3
+ import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
4
+ import { shineWidth, styles } from "./Glimmer.style";
5
+ import { sizeStyles } from "./Glimmer.size.style";
6
+ import { shapeStyles } from "./Glimmer.shape.style";
7
+ import { tokens } from "../utils/design";
8
+
9
+ export type GlimmerShapes = keyof typeof shapeStyles;
10
+ export type GlimmerSizes = keyof typeof sizeStyles;
11
+ export type GlimmerTimings = "base" | "fast";
12
+
13
+ interface GlimmerProps {
14
+ /**
15
+ * Sets the size of the glimmer.
16
+ */
17
+ readonly shape?: GlimmerShapes;
18
+
19
+ /**
20
+ * Sets the shape of the glimmer.
21
+ *
22
+ * If you need a specific width, use the `width` prop.
23
+ */
24
+ readonly size?: GlimmerSizes;
25
+
26
+ /**
27
+ * Control how fast the shine moves from left to right. This is useful when
28
+ * the glimmer is used on smaller spaces.
29
+ */
30
+ readonly timing?: GlimmerTimings;
31
+
32
+ /**
33
+ * Adjust the width of the glimmer in px or % values.
34
+ */
35
+ readonly width?: number | `${number}%`;
36
+ }
37
+
38
+ export const GLIMMER_TEST_ID = "ATL-Glimmer";
39
+ export const GLIMMER_SHINE_TEST_ID = "ATL-Glimmer-Shine";
40
+
41
+ export function Glimmer({
42
+ width,
43
+ shape = "rectangle",
44
+ size = "base",
45
+ timing = "base",
46
+ }: GlimmerProps) {
47
+ const leftPosition = useRef(new Animated.Value(-shineWidth)).current;
48
+ const [parentWidth, setParentWidth] = useState(0);
49
+
50
+ useEffect(() => {
51
+ const shine = Animated.loop(
52
+ Animated.timing(leftPosition, {
53
+ toValue: parentWidth + shineWidth,
54
+ duration:
55
+ timing === "base"
56
+ ? tokens["timing-loading--extended"]
57
+ : tokens["timing-loading"],
58
+ easing: Easing.ease,
59
+ useNativeDriver: true,
60
+ }),
61
+ );
62
+
63
+ shine.start();
64
+
65
+ return shine.stop;
66
+ }, [parentWidth]);
67
+
68
+ return (
69
+ <View
70
+ style={[
71
+ styles.container,
72
+ sizeStyles[size],
73
+ shapeStyles[shape],
74
+ { width },
75
+ ]}
76
+ onLayout={getWidth}
77
+ testID={GLIMMER_TEST_ID}
78
+ >
79
+ <Animated.View
80
+ style={[styles.shine, { transform: [{ translateX: leftPosition }] }]}
81
+ testID={GLIMMER_SHINE_TEST_ID}
82
+ >
83
+ <Svg>
84
+ <Defs>
85
+ <LinearGradient id="gradientShine" x1={0} y1={0.5} x2={1} y2={0.5}>
86
+ <Stop
87
+ offset="0%"
88
+ stopColor={tokens["color-surface--background"]}
89
+ />
90
+ <Stop offset="50%" stopColor={tokens["color-surface"]} />
91
+ <Stop
92
+ offset="100%"
93
+ stopColor={tokens["color-surface--background"]}
94
+ />
95
+ </LinearGradient>
96
+ </Defs>
97
+ <Rect fill="url(#gradientShine)" height="100%" width="100%" />
98
+ </Svg>
99
+ </Animated.View>
100
+ </View>
101
+ );
102
+
103
+ function getWidth(event: LayoutChangeEvent) {
104
+ setParentWidth(event.nativeEvent.layout.width);
105
+ }
106
+ }
@@ -0,0 +1 @@
1
+ export * from "./Glimmer";
@@ -108,8 +108,33 @@ export const styles = StyleSheet.create({
108
108
  },
109
109
 
110
110
  toolbar: {
111
+ flexBasis: "100%",
111
112
  flexDirection: "row",
112
113
  gap: tokens["space-small"],
113
114
  paddingBottom: tokens["space-small"],
114
115
  },
116
+
117
+ loadingSpinner: {
118
+ justifyContent: "center",
119
+ paddingRight: tokens["space-small"],
120
+ },
121
+
122
+ loadingGlimmers: {
123
+ position: "absolute",
124
+ top: tokens["space-base"],
125
+ bottom: tokens["space-base"],
126
+ left: 0,
127
+ right: 0,
128
+ gap: tokens["space-small"],
129
+ paddingTop: tokens["space-small"],
130
+ paddingRight: tokens["space-large"],
131
+ backgroundColor: tokens["color-surface"],
132
+ overflow: "hidden",
133
+ },
134
+
135
+ loadingGlimmersHasValue: {
136
+ top: tokens["space-large"],
137
+ paddingTop: tokens["space-base"] - tokens["space-smaller"],
138
+ bottom: tokens["space-smaller"],
139
+ },
115
140
  });
@@ -8,6 +8,10 @@ import {
8
8
  commonInputStyles,
9
9
  } from ".";
10
10
  import { styles } from "./InputFieldWrapper.style";
11
+ import {
12
+ INPUT_FIELD_WRAPPER_GLIMMERS_TEST_ID,
13
+ INPUT_FIELD_WRAPPER_SPINNER_TEST_ID,
14
+ } from "./InputFieldWrapper";
11
15
  import { typographyStyles } from "../Typography";
12
16
 
13
17
  const mockLabel = { label: "$" };
@@ -31,6 +35,7 @@ function renderWithSuffixLabel(hasValue: boolean): RenderAPI {
31
35
  }
32
36
 
33
37
  const clearInput = "Clear input";
38
+ // eslint-disable-next-line max-statements
34
39
  describe("InputFieldWrapper", () => {
35
40
  it("renders an invalid InputFieldWrapper", () => {
36
41
  const { getByTestId } = renderInputFieldWrapper({ invalid: true });
@@ -269,4 +274,29 @@ describe("InputFieldWrapper", () => {
269
274
  expect(getByText("I am a tool")).toBeDefined();
270
275
  });
271
276
  });
277
+
278
+ describe("Loading state", () => {
279
+ it("does not render any loading indicators", () => {
280
+ const { queryByTestId } = renderInputFieldWrapper({});
281
+ expect(queryByTestId(INPUT_FIELD_WRAPPER_SPINNER_TEST_ID)).toBeNull();
282
+ expect(queryByTestId(INPUT_FIELD_WRAPPER_GLIMMERS_TEST_ID)).toBeNull();
283
+ });
284
+
285
+ it("renders a loading spinner by default when loading is true and loadingType is not set", () => {
286
+ const { getByTestId, queryByTestId } = renderInputFieldWrapper({
287
+ loading: true,
288
+ });
289
+ expect(getByTestId(INPUT_FIELD_WRAPPER_SPINNER_TEST_ID)).toBeDefined();
290
+ expect(queryByTestId(INPUT_FIELD_WRAPPER_GLIMMERS_TEST_ID)).toBeNull();
291
+ });
292
+
293
+ it("renders a glimmer when loading is true and loadingType is glimmer", () => {
294
+ const { getByTestId, queryByTestId } = renderInputFieldWrapper({
295
+ loading: true,
296
+ loadingType: "glimmer",
297
+ });
298
+ expect(getByTestId(INPUT_FIELD_WRAPPER_GLIMMERS_TEST_ID)).toBeDefined();
299
+ expect(queryByTestId(INPUT_FIELD_WRAPPER_SPINNER_TEST_ID)).toBeNull();
300
+ });
301
+ });
272
302
  });
@@ -13,9 +13,11 @@ import { styles } from "./InputFieldWrapper.style";
13
13
  import { PrefixIcon, PrefixLabel } from "./components/Prefix/Prefix";
14
14
  import { SuffixIcon, SuffixLabel } from "./components/Suffix/Suffix";
15
15
  import { ClearAction } from "./components/ClearAction";
16
+ import { Glimmer } from "../Glimmer/Glimmer";
16
17
  import { ErrorMessageWrapper } from "../ErrorMessageWrapper";
17
18
  import { TextVariation, typographyStyles } from "../Typography";
18
19
  import { Text } from "../Text";
20
+ import { ActivityIndicator } from "../ActivityIndicator";
19
21
 
20
22
  export type Clearable = "never" | "while-editing" | "always";
21
23
 
@@ -98,8 +100,23 @@ export interface InputFieldWrapperProps {
98
100
  * Change the behaviour of when the toolbar becomes visible.
99
101
  */
100
102
  readonly toolbarVisibility?: "always" | "while-editing";
103
+
104
+ /**
105
+ * Show loading indicator.
106
+ */
107
+ readonly loading?: boolean;
108
+
109
+ /**
110
+ * Change the type of loading indicator to spinner or glimmer.
111
+ */
112
+ readonly loadingType?: "spinner" | "glimmer";
101
113
  }
102
114
 
115
+ export const INPUT_FIELD_WRAPPER_GLIMMERS_TEST_ID =
116
+ "ATL-InputFieldWrapper-Glimmers";
117
+ export const INPUT_FIELD_WRAPPER_SPINNER_TEST_ID =
118
+ "ATL-InputFieldWrapper-Spinner";
119
+
103
120
  export function InputFieldWrapper({
104
121
  invalid,
105
122
  disabled,
@@ -117,6 +134,8 @@ export function InputFieldWrapper({
117
134
  styleOverride,
118
135
  toolbar,
119
136
  toolbarVisibility = "while-editing",
137
+ loading = false,
138
+ loadingType = "spinner",
120
139
  }: InputFieldWrapperProps): JSX.Element {
121
140
  fieldAffixRequiredPropsCheck([prefix, suffix]);
122
141
  const handleClear = onClear ?? noopClear;
@@ -125,6 +144,9 @@ export function InputFieldWrapper({
125
144
  const isToolbarVisible =
126
145
  toolbar && (toolbarVisibility === "always" || focused);
127
146
 
147
+ const showLoadingSpinner = loading && loadingType === "spinner";
148
+ const showLoadingGlimmer = loading && loadingType === "glimmer";
149
+
128
150
  return (
129
151
  <ErrorMessageWrapper message={getMessage({ invalid, error })}>
130
152
  <View
@@ -177,7 +199,25 @@ export function InputFieldWrapper({
177
199
  />
178
200
  )}
179
201
  {children}
180
- {(showClearAction || suffix?.label || suffix?.icon) && (
202
+
203
+ {showLoadingGlimmer && (
204
+ <View
205
+ testID={INPUT_FIELD_WRAPPER_GLIMMERS_TEST_ID}
206
+ style={[
207
+ styles.loadingGlimmers,
208
+ hasValue && styles.loadingGlimmersHasValue,
209
+ ]}
210
+ >
211
+ <Glimmer size="small" width="80%" />
212
+ <Glimmer size="small" />
213
+ <Glimmer size="small" width="70%" />
214
+ </View>
215
+ )}
216
+
217
+ {(showClearAction ||
218
+ suffix?.label ||
219
+ suffix?.icon ||
220
+ showLoadingSpinner) && (
181
221
  <View style={styles.inputEndContainer}>
182
222
  {showClearAction && (
183
223
  <ClearAction
@@ -196,6 +236,15 @@ export function InputFieldWrapper({
196
236
  styleOverride={styleOverride?.suffixLabel}
197
237
  />
198
238
  )}
239
+
240
+ {showLoadingSpinner && (
241
+ <View style={styles.loadingSpinner}>
242
+ <ActivityIndicator
243
+ testID={INPUT_FIELD_WRAPPER_SPINNER_TEST_ID}
244
+ />
245
+ </View>
246
+ )}
247
+
199
248
  {suffix?.icon && (
200
249
  <SuffixIcon
201
250
  disabled={disabled}
@@ -33,7 +33,10 @@ import { InputFieldWrapper } from "../InputFieldWrapper";
33
33
  import { commonInputStyles } from "../InputFieldWrapper/CommonInputStyles.style";
34
34
 
35
35
  export interface InputTextProps
36
- extends Pick<InputFieldWrapperProps, "toolbar" | "toolbarVisibility"> {
36
+ extends Pick<
37
+ InputFieldWrapperProps,
38
+ "toolbar" | "toolbarVisibility" | "loading" | "loadingType"
39
+ > {
37
40
  /**
38
41
  * Highlights the field red and shows message below (if string) to indicate an error
39
42
  */
@@ -272,6 +275,8 @@ function InputTextInternal(
272
275
  styleOverride,
273
276
  toolbar,
274
277
  toolbarVisibility,
278
+ loading,
279
+ loadingType,
275
280
  }: InputTextProps,
276
281
  ref: Ref<InputTextRef>,
277
282
  ) {
@@ -375,6 +380,8 @@ function InputTextInternal(
375
380
  styleOverride={styleOverride}
376
381
  toolbar={toolbar}
377
382
  toolbarVisibility={toolbarVisibility}
383
+ loading={loading}
384
+ loadingType={loadingType}
378
385
  >
379
386
  <TextInput
380
387
  inputAccessoryViewID={inputAccessoryID || undefined}
@@ -391,6 +398,7 @@ function InputTextInternal(
391
398
  multiline && Platform.OS === "ios" && styles.multilineInputiOS,
392
399
  multiline && hasMiniLabel && styles.multiLineInputWithMini,
393
400
  styleOverride?.inputText,
401
+ loading && loadingType === "glimmer" && { color: "transparent" },
394
402
  ]}
395
403
  // @ts-expect-error - does exist on 0.71 and up https://github.com/facebook/react-native/pull/39281
396
404
  readOnly={readonly}
@@ -408,6 +416,7 @@ function InputTextInternal(
408
416
  blurOnSubmit={shouldBlurOnSubmit}
409
417
  accessibilityLabel={accessibilityLabel || placeholder}
410
418
  accessibilityHint={accessibilityHint}
419
+ accessibilityState={{ busy: loading }}
411
420
  secureTextEntry={secureTextEntry}
412
421
  {...androidA11yProps}
413
422
  onFocus={event => {