@jobber/components-native 0.9.0 → 0.11.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 (66) hide show
  1. package/dist/src/Button/Button.js +78 -0
  2. package/dist/src/Button/Button.style.js +92 -0
  3. package/dist/src/Button/components/InternalButtonLoading/InternalButtonLoading.js +36 -0
  4. package/dist/src/Button/components/InternalButtonLoading/InternalButtonLoading.style.js +4 -0
  5. package/dist/src/Button/components/InternalButtonLoading/index.js +1 -0
  6. package/dist/src/Button/index.js +1 -0
  7. package/dist/src/Button/types.js +1 -0
  8. package/dist/src/InputFieldWrapper/CommonInputStyles.style.js +33 -0
  9. package/dist/src/InputFieldWrapper/InputFieldWrapper.js +88 -0
  10. package/dist/src/InputFieldWrapper/InputFieldWrapper.style.js +79 -0
  11. package/dist/src/InputFieldWrapper/components/ClearAction/ClearAction.js +12 -0
  12. package/dist/src/InputFieldWrapper/components/ClearAction/ClearAction.style.js +25 -0
  13. package/dist/src/InputFieldWrapper/components/ClearAction/index.js +2 -0
  14. package/dist/src/InputFieldWrapper/components/ClearAction/messages.js +8 -0
  15. package/dist/src/InputFieldWrapper/components/Prefix/Prefix.js +34 -0
  16. package/dist/src/InputFieldWrapper/components/Suffix/Suffix.js +35 -0
  17. package/dist/src/InputFieldWrapper/hooks/useShowClear.js +15 -0
  18. package/dist/src/InputFieldWrapper/index.js +3 -0
  19. package/dist/src/index.js +2 -0
  20. package/dist/tsconfig.tsbuildinfo +1 -1
  21. package/dist/types/src/Button/Button.d.ts +71 -0
  22. package/dist/types/src/Button/Button.style.d.ts +86 -0
  23. package/dist/types/src/Button/components/InternalButtonLoading/InternalButtonLoading.d.ts +11 -0
  24. package/dist/types/src/Button/components/InternalButtonLoading/InternalButtonLoading.style.d.ts +4 -0
  25. package/dist/types/src/Button/components/InternalButtonLoading/index.d.ts +1 -0
  26. package/dist/types/src/Button/index.d.ts +2 -0
  27. package/dist/types/src/Button/types.d.ts +3 -0
  28. package/dist/types/src/InputFieldWrapper/CommonInputStyles.style.d.ts +30 -0
  29. package/dist/types/src/InputFieldWrapper/InputFieldWrapper.d.ts +63 -0
  30. package/dist/types/src/InputFieldWrapper/InputFieldWrapper.style.d.ts +82 -0
  31. package/dist/types/src/InputFieldWrapper/components/ClearAction/ClearAction.d.ts +10 -0
  32. package/dist/types/src/InputFieldWrapper/components/ClearAction/ClearAction.style.d.ts +22 -0
  33. package/dist/types/src/InputFieldWrapper/components/ClearAction/index.d.ts +2 -0
  34. package/dist/types/src/InputFieldWrapper/components/ClearAction/messages.d.ts +7 -0
  35. package/dist/types/src/InputFieldWrapper/components/Prefix/Prefix.d.ts +23 -0
  36. package/dist/types/src/InputFieldWrapper/components/Suffix/Suffix.d.ts +25 -0
  37. package/dist/types/src/InputFieldWrapper/hooks/useShowClear.d.ts +10 -0
  38. package/dist/types/src/InputFieldWrapper/index.d.ts +4 -0
  39. package/dist/types/src/index.d.ts +2 -0
  40. package/package.json +5 -2
  41. package/src/Button/Button.style.ts +116 -0
  42. package/src/Button/Button.test.tsx +298 -0
  43. package/src/Button/Button.tsx +223 -0
  44. package/src/Button/components/InternalButtonLoading/InternalButtonLoading.style.ts +5 -0
  45. package/src/Button/components/InternalButtonLoading/InternalButtonLoading.test.tsx +39 -0
  46. package/src/Button/components/InternalButtonLoading/InternalButtonLoading.tsx +77 -0
  47. package/src/Button/components/InternalButtonLoading/index.ts +1 -0
  48. package/src/Button/index.ts +2 -0
  49. package/src/Button/types.ts +3 -0
  50. package/src/InputFieldWrapper/CommonInputStyles.style.ts +37 -0
  51. package/src/InputFieldWrapper/InputFieldWrapper.style.ts +93 -0
  52. package/src/InputFieldWrapper/InputFieldWrapper.test.tsx +243 -0
  53. package/src/InputFieldWrapper/InputFieldWrapper.tsx +317 -0
  54. package/src/InputFieldWrapper/components/ClearAction/ClearAction.style.ts +27 -0
  55. package/src/InputFieldWrapper/components/ClearAction/ClearAction.test.tsx +15 -0
  56. package/src/InputFieldWrapper/components/ClearAction/ClearAction.tsx +32 -0
  57. package/src/InputFieldWrapper/components/ClearAction/index.ts +2 -0
  58. package/src/InputFieldWrapper/components/ClearAction/messages.ts +9 -0
  59. package/src/InputFieldWrapper/components/Prefix/Prefix.test.tsx +221 -0
  60. package/src/InputFieldWrapper/components/Prefix/Prefix.tsx +104 -0
  61. package/src/InputFieldWrapper/components/Suffix/Suffix.test.tsx +101 -0
  62. package/src/InputFieldWrapper/components/Suffix/Suffix.tsx +113 -0
  63. package/src/InputFieldWrapper/hooks/useShowClear.test.ts +158 -0
  64. package/src/InputFieldWrapper/hooks/useShowClear.ts +31 -0
  65. package/src/InputFieldWrapper/index.ts +4 -0
  66. package/src/index.ts +2 -0
@@ -0,0 +1,298 @@
1
+ import React, { CSSProperties, ReactElement } from "react";
2
+ import { fireEvent, render } from "@testing-library/react-native";
3
+ import { Path } from "react-native-svg";
4
+ import { ReactTestInstance } from "react-test-renderer";
5
+ import { Button, ButtonType, ButtonVariation } from ".";
6
+ import { ButtonSize } from "./types";
7
+ import { baseButtonHeight, smallButtonHeight } from "./Button.style";
8
+ import { tokens } from "../utils/design";
9
+
10
+ function getIconAndTextColorFromRender({
11
+ type = "primary",
12
+ variation = "work",
13
+ }: {
14
+ type?: ButtonType;
15
+ variation?: ButtonVariation;
16
+ }) {
17
+ const pressHandler = jest.fn();
18
+ const text = "🌚 I am the text 🌚";
19
+ const iconName = "cog";
20
+
21
+ const { getByTestId, getByRole } = render(
22
+ <Button
23
+ onPress={pressHandler}
24
+ label={text}
25
+ icon={iconName}
26
+ type={type}
27
+ variation={variation}
28
+ />,
29
+ );
30
+
31
+ const iconColor = getByTestId(iconName).findByType(Path).props.fill;
32
+ const textColor = getByRole("text").props.style.find(
33
+ (style: CSSProperties) => style.color,
34
+ ).color;
35
+
36
+ return { iconColor, textColor };
37
+ }
38
+
39
+ function renderButton(element: ReactElement) {
40
+ const instance = render(element);
41
+
42
+ const button = instance.getByLabelText(element.props.label);
43
+ expect(button).toBeDefined();
44
+ expect(instance.getByText(element.props.label)).toBeDefined();
45
+
46
+ const buttonStyleEl = button.children[0] as ReactTestInstance;
47
+ const buttonStyle = buttonStyleEl.props.style.reduce(
48
+ (mergedStyles: CSSProperties, additionalStyles: CSSProperties) => ({
49
+ ...mergedStyles,
50
+ ...additionalStyles,
51
+ }),
52
+ {},
53
+ );
54
+
55
+ return { ...instance, button, buttonStyle };
56
+ }
57
+
58
+ describe("Button", () => {
59
+ it("renders the default primary button", () => {
60
+ const { button, buttonStyle } = renderButton(
61
+ <Button label={"Foo"} onPress={jest.fn()} />,
62
+ );
63
+
64
+ expect(button.props.accessibilityRole).toBe("button");
65
+ expect(buttonStyle).toMatchObject({
66
+ backgroundColor: tokens["color-interactive"],
67
+ borderColor: tokens["color-interactive"],
68
+ });
69
+ });
70
+
71
+ it.each<[ButtonVariation, Record<string, string>]>([
72
+ ["work", { bgColor: tokens["color-interactive"] }],
73
+ [
74
+ "cancel",
75
+ {
76
+ bgColor: tokens["color-white"],
77
+ borderColor: tokens["color-interactive--subtle"],
78
+ },
79
+ ],
80
+ ["destructive", { bgColor: tokens["color-destructive"] }],
81
+ ["learning", { bgColor: tokens["color-informative"] }],
82
+ ])("renders a %s Button", (variation, { bgColor, borderColor }) => {
83
+ const { buttonStyle } = renderButton(
84
+ <Button label={variation} variation={variation} onPress={jest.fn()} />,
85
+ );
86
+
87
+ expect(buttonStyle).toMatchObject({
88
+ backgroundColor: bgColor,
89
+ borderColor: borderColor || bgColor,
90
+ });
91
+ });
92
+
93
+ it.each<[ButtonType, Record<string, string>]>([
94
+ ["primary", { bgColor: tokens["color-interactive"] }],
95
+ [
96
+ "secondary",
97
+ {
98
+ bgColor: tokens["color-white"],
99
+ borderColor: tokens["color-interactive"],
100
+ },
101
+ ],
102
+ ["tertiary", { bgColor: tokens["color-white"] }],
103
+ ])("renders a %s Button", (type, { bgColor, borderColor }) => {
104
+ const { buttonStyle } = renderButton(
105
+ <Button label={type} type={type} onPress={jest.fn()} />,
106
+ );
107
+
108
+ expect(buttonStyle).toMatchObject({
109
+ backgroundColor: bgColor,
110
+ borderColor: borderColor || bgColor,
111
+ });
112
+ });
113
+
114
+ it.each<[ButtonSize, Record<string, number>]>([
115
+ [
116
+ "small",
117
+ {
118
+ minHeight: smallButtonHeight,
119
+ },
120
+ ],
121
+ [
122
+ "base",
123
+ {
124
+ minHeight: baseButtonHeight,
125
+ },
126
+ ],
127
+ ])("renders a %s Button", (size, { minHeight }) => {
128
+ const { buttonStyle } = renderButton(
129
+ <Button label="Button Size" size={size} />,
130
+ );
131
+
132
+ expect(buttonStyle).toMatchObject({
133
+ minHeight,
134
+ });
135
+ });
136
+
137
+ it("renders a disabled Button", () => {
138
+ const { button, buttonStyle } = renderButton(
139
+ <Button label="Can't touch this" disabled={true} onPress={jest.fn()} />,
140
+ );
141
+
142
+ expect(button.props.accessibilityState).toHaveProperty("disabled", true);
143
+ expect(buttonStyle).toMatchObject({
144
+ backgroundColor: tokens["color-disabled--secondary"],
145
+ borderColor: tokens["color-disabled--secondary"],
146
+ });
147
+ });
148
+
149
+ it("renders a non-fullWidth Button", () => {
150
+ const expectedValue = { alignSelf: "stretch" };
151
+ const { button, rerender } = renderButton(
152
+ <Button label="Thicc" onPress={jest.fn()} />,
153
+ );
154
+
155
+ expect(button.props.style).toContainEqual(expectedValue);
156
+
157
+ rerender(<Button label="Thicc" fullWidth={false} onPress={jest.fn()} />);
158
+
159
+ expect(button.props.style).not.toContainEqual(expectedValue);
160
+ });
161
+
162
+ it("should call the onPress handler", () => {
163
+ const pressHandler = jest.fn();
164
+ const text = "🌚 I am the text 🌚";
165
+ const a11yLabel = "A button";
166
+ const { getByLabelText } = render(
167
+ <Button
168
+ onPress={pressHandler}
169
+ label={text}
170
+ accessibilityLabel={a11yLabel}
171
+ />,
172
+ );
173
+
174
+ fireEvent.press(getByLabelText(a11yLabel));
175
+ expect(pressHandler).toHaveBeenCalled();
176
+ });
177
+
178
+ describe("accessibilityLabel", () => {
179
+ it("uses accessibilityLabel if specified", () => {
180
+ const pressHandler = jest.fn();
181
+ const text = "🌚 I am the text 🌚";
182
+ const a11yLabel = "A button";
183
+ const { getByLabelText } = render(
184
+ <Button
185
+ onPress={pressHandler}
186
+ label={text}
187
+ accessibilityLabel={a11yLabel}
188
+ />,
189
+ );
190
+
191
+ expect(getByLabelText(a11yLabel)).toBeTruthy();
192
+ });
193
+
194
+ it("uses label if unspecified", () => {
195
+ const pressHandler = jest.fn();
196
+ const text = "🌚 I am the text 🌚";
197
+ const { getByLabelText } = render(
198
+ <Button onPress={pressHandler} label={text} />,
199
+ );
200
+
201
+ expect(getByLabelText(text)).toBeTruthy();
202
+ });
203
+ });
204
+
205
+ describe("if an icon is passed in", () => {
206
+ it("renders an icon Button with same color as the Button text", () => {
207
+ const { iconColor, textColor } = getIconAndTextColorFromRender({});
208
+
209
+ expect(iconColor).toBe(tokens["color-white"]);
210
+ expect(textColor).toBe(iconColor);
211
+ });
212
+
213
+ it("renders the learning variation and secondary type with icon and label with the same color", () => {
214
+ const { iconColor, textColor } = getIconAndTextColorFromRender({
215
+ variation: "learning",
216
+ type: "secondary",
217
+ });
218
+ expect(iconColor).toBe(tokens["color-informative"]);
219
+ expect(textColor).toBe(iconColor);
220
+ });
221
+
222
+ it("renders the destructive variation and secondary type with icon and label with the same color", () => {
223
+ const { iconColor, textColor } = getIconAndTextColorFromRender({
224
+ variation: "destructive",
225
+ type: "secondary",
226
+ });
227
+ expect(iconColor).toBe(tokens["color-destructive"]);
228
+ expect(textColor).toBe(iconColor);
229
+ });
230
+
231
+ it("renders the cancel variation and tertiary type with icon and label with the same color", () => {
232
+ const { iconColor, textColor } = getIconAndTextColorFromRender({
233
+ variation: "cancel",
234
+ type: "tertiary",
235
+ });
236
+ expect(iconColor).toBe(tokens["color-interactive--subtle"]);
237
+ expect(textColor).toBe(iconColor);
238
+ });
239
+
240
+ it("renders an icon Button if only an icon is passed", () => {
241
+ const pressHandler = jest.fn();
242
+ const icon = "cog";
243
+ const accessibilityLabel = "cog";
244
+
245
+ const { getByLabelText } = render(
246
+ <Button
247
+ onPress={pressHandler}
248
+ icon={icon}
249
+ accessibilityLabel={accessibilityLabel}
250
+ />,
251
+ );
252
+
253
+ expect(getByLabelText(icon)).toBeDefined();
254
+ });
255
+ });
256
+
257
+ describe("Loading", () => {
258
+ const label = "I am loading";
259
+ it("does render a loading state", () => {
260
+ const { getByTestId, getByRole } = render(
261
+ <Button label={label} onPress={jest.fn} loading={true} />,
262
+ );
263
+
264
+ expect(getByTestId("loadingImage")).toBeDefined();
265
+ expect(getByRole("button", { busy: true })).toBeDefined();
266
+ });
267
+
268
+ it("doesn't render a loading state", () => {
269
+ const { queryByTestId, queryByRole, rerender } = render(
270
+ <Button label={label} onPress={jest.fn} loading={false} />,
271
+ );
272
+
273
+ expect(queryByTestId("loadingImage")).toBeNull();
274
+ expect(queryByRole("button", { busy: true })).toBeNull();
275
+
276
+ rerender(<Button label="I am loading" onPress={jest.fn} />);
277
+
278
+ expect(queryByTestId("loadingImage")).toBeNull();
279
+ expect(queryByRole("button", { busy: true })).toBeNull();
280
+ });
281
+
282
+ it("should not allow press events", () => {
283
+ const handlePress = jest.fn();
284
+ const setup = (loading?: boolean) => (
285
+ <Button label={label} onPress={handlePress} loading={loading} />
286
+ );
287
+ const { getByLabelText, rerender } = render(setup(true));
288
+
289
+ fireEvent.press(getByLabelText(label));
290
+ expect(handlePress).not.toHaveBeenCalled();
291
+
292
+ // Sanity check that it's not a false positive
293
+ rerender(setup());
294
+ fireEvent.press(getByLabelText(label));
295
+ expect(handlePress).toHaveBeenCalledTimes(1);
296
+ });
297
+ });
298
+ });
@@ -0,0 +1,223 @@
1
+ import React from "react";
2
+ import { TouchableHighlight, View } from "react-native";
3
+ import { IconColorNames, IconNames } from "@jobber/design";
4
+ import { XOR } from "ts-xor";
5
+ import { styles } from "./Button.style";
6
+ // eslint-disable-next-line import/no-internal-modules
7
+ import { InternalButtonLoading } from "./components/InternalButtonLoading";
8
+ import { ButtonSize, ButtonType, ButtonVariation } from "./types";
9
+ import { ActionLabel, ActionLabelVariation } from "../ActionLabel";
10
+ import { Icon } from "../Icon";
11
+ import { tokens } from "../utils/design";
12
+
13
+ interface CommonButtonProps {
14
+ /**
15
+ * Press handler
16
+ */
17
+ readonly onPress?: () => void;
18
+
19
+ /**
20
+ * Themes the button to the type of action it performs
21
+ */
22
+ readonly variation?: ButtonVariation;
23
+
24
+ /**
25
+ * Sets the visual hierarchy
26
+ */
27
+ readonly type?: ButtonType;
28
+
29
+ /**
30
+ * Defines the size of the button
31
+ *
32
+ * @default "base"
33
+ */
34
+ readonly size?: ButtonSize;
35
+
36
+ /**
37
+ * Will make the button scale to take up all the available height
38
+ */
39
+ readonly fullHeight?: boolean;
40
+
41
+ /**
42
+ * Will make the button scale to take up all of the available width
43
+ */
44
+ readonly fullWidth?: boolean;
45
+
46
+ /**
47
+ * Makes the button un-clickable
48
+ */
49
+ readonly disabled?: boolean;
50
+
51
+ /**
52
+ * Accessibility hint to help users understand what will happen when they press the button
53
+ */
54
+ readonly accessibilityHint?: string;
55
+
56
+ /**
57
+ * Changes the button interface to imply loading and prevents the press callback
58
+ *
59
+ * @default false
60
+ */
61
+ readonly loading?: boolean;
62
+
63
+ /**
64
+ * Adds an leading icon beside the label.
65
+ */
66
+ readonly icon?: IconNames;
67
+
68
+ /**
69
+ * Accessibility label for the component. This is required for components that
70
+ * have an `icon` but not a `label`.
71
+ *
72
+ * If the string is the same as the `label` prop, you don't need to add an
73
+ * `accessibilityLabel`. **Don't use this for testing purposes.**
74
+ */
75
+ readonly accessibilityLabel?: string;
76
+ }
77
+
78
+ interface LabelButton extends CommonButtonProps {
79
+ /**
80
+ * Text to be displayed on the button
81
+ */
82
+ readonly label: string;
83
+ }
84
+
85
+ interface IconButton extends CommonButtonProps {
86
+ readonly icon: IconNames;
87
+ readonly accessibilityLabel: string;
88
+ }
89
+
90
+ export type ButtonProps = XOR<LabelButton, IconButton>;
91
+ export function Button({
92
+ label,
93
+ onPress,
94
+ variation = "work",
95
+ type = "primary",
96
+ fullHeight = false,
97
+ fullWidth = true,
98
+ disabled = false,
99
+ loading = false,
100
+ size = "base",
101
+ accessibilityLabel,
102
+ accessibilityHint,
103
+ icon,
104
+ }: ButtonProps): JSX.Element {
105
+ const buttonStyle = [
106
+ styles.button,
107
+ styles[variation],
108
+ styles[type],
109
+ styles[size],
110
+ disabled && styles.disabled,
111
+ fullHeight && styles.fullHeight,
112
+ fullWidth && styles.reducedPaddingForFullWidth,
113
+ ];
114
+
115
+ // attempts to use Pressable caused problems. When a ScrollView contained
116
+ // an InputText that was focused, it required two presses to activate the
117
+ // Pressable. Using a TouchableHighlight made things register correctly
118
+ // in a single press
119
+
120
+ return (
121
+ <TouchableHighlight
122
+ onPress={onPress}
123
+ testID={accessibilityLabel || label}
124
+ accessibilityLabel={accessibilityLabel || label}
125
+ accessibilityHint={accessibilityHint}
126
+ accessibilityRole="button"
127
+ accessibilityState={{ disabled, busy: loading }}
128
+ disabled={disabled || loading}
129
+ underlayColor={tokens["color-greyBlue--dark"]}
130
+ activeOpacity={tokens["opacity-pressed"]}
131
+ style={[
132
+ styles.touchable,
133
+ fullWidth && styles.fullWidth,
134
+ fullHeight && styles.fullHeight,
135
+ ]}
136
+ >
137
+ <View style={buttonStyle}>
138
+ {loading && <InternalButtonLoading variation={variation} type={type} />}
139
+ <View style={getContentStyles(label, icon)}>
140
+ {icon && (
141
+ <View style={styles.iconStyle}>
142
+ <Icon
143
+ name={icon}
144
+ color={getIconColorVariation(variation, type, disabled)}
145
+ />
146
+ </View>
147
+ )}
148
+ {label && (
149
+ <View style={styles.labelStyle}>
150
+ <ActionLabel
151
+ variation={getActionLabelVariation(variation, type)}
152
+ disabled={disabled}
153
+ align={icon ? "start" : undefined}
154
+ >
155
+ {label}
156
+ </ActionLabel>
157
+ </View>
158
+ )}
159
+ </View>
160
+ </View>
161
+ </TouchableHighlight>
162
+ );
163
+ }
164
+
165
+ function getActionLabelVariation(
166
+ variation: string,
167
+ type: string,
168
+ ): ActionLabelVariation {
169
+ if (type === "primary" && variation !== "cancel") {
170
+ return "onPrimary";
171
+ }
172
+
173
+ switch (variation) {
174
+ case "learning":
175
+ return "learning";
176
+ case "destructive":
177
+ return "destructive";
178
+ case "cancel":
179
+ return "subtle";
180
+ default:
181
+ return "interactive";
182
+ }
183
+ }
184
+
185
+ function getIconColorVariation(
186
+ variation: ButtonVariation,
187
+ type: string,
188
+ disabled: boolean,
189
+ ): IconColorNames {
190
+ if (disabled) {
191
+ return "disabled";
192
+ }
193
+
194
+ if (type === "primary" && variation !== "cancel") {
195
+ return "white";
196
+ }
197
+
198
+ switch (variation) {
199
+ case "learning":
200
+ return "informative";
201
+ case "destructive":
202
+ return "destructive";
203
+ case "cancel":
204
+ return "interactiveSubtle";
205
+ default:
206
+ return "interactive";
207
+ }
208
+ }
209
+
210
+ function getContentStyles(
211
+ label: string | undefined,
212
+ icon: IconNames | undefined,
213
+ ) {
214
+ if (label && !icon) {
215
+ return undefined;
216
+ }
217
+
218
+ return [
219
+ styles.content,
220
+ icon && !!label && styles.iconPaddingOffset,
221
+ !!label && styles.contentWithLabel,
222
+ ];
223
+ }
@@ -0,0 +1,5 @@
1
+ import { StyleSheet } from "react-native";
2
+
3
+ export const styles = StyleSheet.create({
4
+ image: StyleSheet.absoluteFillObject,
5
+ });
@@ -0,0 +1,39 @@
1
+ import React from "react";
2
+ import { cleanup, render } from "@testing-library/react-native";
3
+ import {
4
+ InternalButtonLoading,
5
+ darkPattern,
6
+ lightPattern,
7
+ } from "./InternalButtonLoading";
8
+ import { ButtonType, ButtonVariation } from "../../types";
9
+
10
+ afterEach(cleanup);
11
+
12
+ describe("Loading pattern", () => {
13
+ it.each<[string, ButtonType, ButtonVariation]>([
14
+ [lightPattern, "primary", "work"],
15
+ [lightPattern, "primary", "destructive"],
16
+ [lightPattern, "primary", "learning"],
17
+ [darkPattern, "primary", "cancel"],
18
+ [darkPattern, "secondary", "cancel"],
19
+ [darkPattern, "secondary", "work"],
20
+ [darkPattern, "secondary", "destructive"],
21
+ [darkPattern, "secondary", "learning"],
22
+ [darkPattern, "tertiary", "cancel"],
23
+ [darkPattern, "tertiary", "work"],
24
+ [darkPattern, "tertiary", "destructive"],
25
+ [darkPattern, "tertiary", "learning"],
26
+ ])(
27
+ "should render a %s pattern on %s %s combination",
28
+ (pattern, type, variation) => {
29
+ const { getByTestId } = render(
30
+ <InternalButtonLoading type={type} variation={variation} />,
31
+ );
32
+
33
+ const component = getByTestId("loadingImage");
34
+ expect(component.props.source).toMatchObject({
35
+ uri: expect.stringContaining(pattern),
36
+ });
37
+ },
38
+ );
39
+ });
@@ -0,0 +1,77 @@
1
+ import React from "react";
2
+ import { ImageBackground, PixelRatio } from "react-native";
3
+ import Animated, {
4
+ Easing,
5
+ useAnimatedStyle,
6
+ useSharedValue,
7
+ withDelay,
8
+ withRepeat,
9
+ withTiming,
10
+ } from "react-native-reanimated";
11
+ import { styles } from "./InternalButtonLoading.style";
12
+ import { tokens } from "../../../utils/design";
13
+ import { ButtonType, ButtonVariation } from "../../types";
14
+
15
+ interface InternalButtonLoadingProps {
16
+ readonly variation: ButtonVariation;
17
+ readonly type: ButtonType;
18
+ }
19
+
20
+ const imageWidth = 96;
21
+ const offset = PixelRatio.roundToNearestPixel(imageWidth / PixelRatio.get());
22
+ const leftOffset = -1 * offset;
23
+
24
+ const AnimatedImage = Animated.createAnimatedComponent(ImageBackground);
25
+
26
+ function InternalButtonLoadingInternal({
27
+ variation,
28
+ type,
29
+ }: InternalButtonLoadingProps): JSX.Element {
30
+ const translateX = useSharedValue(0);
31
+ translateX.value = withRepeat(
32
+ withTiming(offset, {
33
+ duration: tokens["timing-loading"],
34
+ easing: Easing.linear,
35
+ }),
36
+ -1,
37
+ );
38
+
39
+ const opacity = useSharedValue(0);
40
+ opacity.value = withDelay(
41
+ tokens["timing-quick"],
42
+ withTiming(1, {
43
+ duration: tokens["timing-base"],
44
+ easing: Easing.linear,
45
+ }),
46
+ );
47
+
48
+ const animations = useAnimatedStyle(() => ({
49
+ opacity: opacity.value,
50
+ transform: [{ translateX: translateX.value }],
51
+ }));
52
+
53
+ return (
54
+ <AnimatedImage
55
+ testID="loadingImage"
56
+ source={{ uri: getLoadingPattern({ variation, type }) }}
57
+ resizeMode="repeat"
58
+ style={[styles.image, { left: leftOffset }, animations]}
59
+ />
60
+ );
61
+ }
62
+
63
+ export const darkPattern =
64
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgAgMAAACf9p+rAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAAITgAACE4AUWWMWAAAAAMUExURQAAAEdwTAAAAAAAAKDh18UAAAAEdFJOUxkADQwimkzpAAAAtUlEQVRIx+3NqxHDQBRDUc0YuxyXokxgSkmT7sdgP++3YoYrqAsOYDto+7gfpwtfHy4Xfj7cLvw3sYlNbOINAoI4IIgTgrggiBuCIAThQyB8CIQLkXAhEi5EwoVIWEiEhURYSISFRMyQiRkyMUMmZsjECIUYoRAjFGKEQvRQiR4q0UMleqhECwuihQXRwoJoYUEQgiAEQQiCEAQhCEIQhCAIQRCCIARBCIIQBCEIQhCEIAhB8AEuzZ5wHe17xgAAAABJRU5ErkJggg==";
65
+ export const lightPattern =
66
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgAgMAAACf9p+rAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAAITgAACE4AUWWMWAAAAAJUExURf///0dwTP///0SistEAAAADdFJOU0AAILGCadYAAAC0SURBVEjH7c2pFcNAFENRHTMX4pKUE5hS0oT7NZjlbyNmOIJ64AK2g7aP+3G68PXhcuHnw+3CfxOb2MQm3iAgiAOCOCGIC4K4IQhCED4EwodAuBAJFyLhQiRciISFRFhIhIVEWEjEDJmYIRMzZGKGTIxQiBEKMUIhRihED5XooRI9VKKHSrSwIFpYEC0siBYWBCEIQhCEIAhBEIIgBEEIghAEIQhCEIQgCEEQgiAEQQiCEAQfva6WeBniVLgAAAAASUVORK5CYII=";
67
+
68
+ function getLoadingPattern({
69
+ variation,
70
+ type,
71
+ }: InternalButtonLoadingProps): string {
72
+ if (variation === "cancel") return darkPattern;
73
+ if (type === "primary") return lightPattern;
74
+ return darkPattern;
75
+ }
76
+
77
+ export const InternalButtonLoading = React.memo(InternalButtonLoadingInternal);
@@ -0,0 +1 @@
1
+ export { InternalButtonLoading } from "./InternalButtonLoading";
@@ -0,0 +1,2 @@
1
+ export { Button } from "./Button";
2
+ export type { ButtonVariation, ButtonType } from "./types";
@@ -0,0 +1,3 @@
1
+ export type ButtonVariation = "work" | "cancel" | "destructive" | "learning";
2
+ export type ButtonType = "primary" | "secondary" | "tertiary";
3
+ export type ButtonSize = "small" | "base";
@@ -0,0 +1,37 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { typographyStyles } from "../Typography";
3
+ import { tokens } from "../utils/design";
4
+
5
+ export const commonInputStyles = StyleSheet.create({
6
+ input: {
7
+ width: "100%",
8
+ flexShrink: 1,
9
+ flexGrow: 1,
10
+ color: tokens["color-text"],
11
+ fontFamily: typographyStyles.baseRegularRegular.fontFamily,
12
+ fontSize: typographyStyles.defaultSize.fontSize,
13
+ letterSpacing: typographyStyles.baseLetterSpacing.letterSpacing,
14
+ minHeight: tokens["space-largest"],
15
+ padding: 0,
16
+ },
17
+
18
+ inputEmpty: {
19
+ paddingTop: 0,
20
+ },
21
+
22
+ inputDisabled: {
23
+ color: typographyStyles.disabled.color,
24
+ },
25
+
26
+ container: {
27
+ marginVertical: tokens["space-smaller"],
28
+ backgroundColor: tokens["color-surface"],
29
+ minHeight: tokens["space-largest"],
30
+ flexDirection: "row",
31
+ justifyContent: "space-between",
32
+ width: "100%",
33
+ borderColor: tokens["color-grey"],
34
+ borderStyle: "solid",
35
+ borderBottomWidth: tokens["border-base"],
36
+ },
37
+ });