@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.
- package/dist/package.json +3 -3
- package/dist/src/Glimmer/Glimmer.js +42 -0
- package/dist/src/Glimmer/Glimmer.shape.style.js +16 -0
- package/dist/src/Glimmer/Glimmer.size.style.js +9 -0
- package/dist/src/Glimmer/Glimmer.style.js +20 -0
- package/dist/src/Glimmer/index.js +1 -0
- package/dist/src/InputFieldWrapper/InputFieldWrapper.js +20 -2
- package/dist/src/InputFieldWrapper/InputFieldWrapper.style.js +22 -0
- package/dist/src/InputText/InputText.js +4 -3
- package/dist/src/index.js +6 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/Glimmer/Glimmer.d.ts +31 -0
- package/dist/types/src/Glimmer/Glimmer.shape.style.d.ts +14 -0
- package/dist/types/src/Glimmer/Glimmer.size.style.d.ts +17 -0
- package/dist/types/src/Glimmer/Glimmer.style.d.ts +18 -0
- package/dist/types/src/Glimmer/index.d.ts +1 -0
- package/dist/types/src/InputFieldWrapper/InputFieldWrapper.d.ts +11 -1
- package/dist/types/src/InputFieldWrapper/InputFieldWrapper.style.d.ts +22 -0
- package/dist/types/src/InputText/InputText.d.ts +1 -1
- package/dist/types/src/index.d.ts +6 -5
- package/package.json +3 -3
- package/src/Glimmer/Glimmer.shape.style.ts +17 -0
- package/src/Glimmer/Glimmer.size.style.ts +10 -0
- package/src/Glimmer/Glimmer.style.ts +23 -0
- package/src/Glimmer/Glimmer.test.tsx +73 -0
- package/src/Glimmer/Glimmer.tsx +106 -0
- package/src/Glimmer/index.ts +1 -0
- package/src/InputFieldWrapper/InputFieldWrapper.style.ts +25 -0
- package/src/InputFieldWrapper/InputFieldWrapper.test.tsx +30 -0
- package/src/InputFieldWrapper/InputFieldWrapper.tsx +50 -1
- package/src/InputText/InputText.tsx +10 -1
- 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,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
|
|
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.
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
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<
|
|
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 => {
|