@jobber/components-native 0.30.0 → 0.32.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/README.md +3 -0
- package/dist/src/InputNumber/InputNumber.js +82 -0
- package/dist/src/InputNumber/index.js +1 -0
- package/dist/src/InputNumber/messages.js +8 -0
- package/dist/src/Select/Select.js +79 -0
- package/dist/src/Select/Select.style.js +45 -0
- package/dist/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.ios.js +30 -0
- package/dist/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.js +16 -0
- package/dist/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.style.js +29 -0
- package/dist/src/Select/components/SelectDefaultPicker/index.js +1 -0
- package/dist/src/Select/components/SelectDefaultPicker/messages.js +8 -0
- package/dist/src/Select/components/SelectIOSPicker/SelectIOSPicker.js +2 -0
- package/dist/src/Select/components/SelectIOSPicker/index.js +1 -0
- package/dist/src/Select/components/SelectInternalPicker/SelectInternalPicker.js +14 -0
- package/dist/src/Select/components/SelectInternalPicker/index.js +1 -0
- package/dist/src/Select/components/SelectInternalPicker/utils.js +13 -0
- package/dist/src/Select/components/SelectPressable/SelectPressable.js +15 -0
- package/dist/src/Select/components/SelectPressable/SelectPressable.style.js +7 -0
- package/dist/src/Select/components/SelectPressable/index.js +1 -0
- package/dist/src/Select/index.js +1 -0
- package/dist/src/Select/messages.js +13 -0
- package/dist/src/Select/types.js +1 -0
- package/dist/src/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/InputNumber/InputNumber.d.ts +20 -0
- package/dist/types/src/InputNumber/index.d.ts +2 -0
- package/dist/types/src/InputNumber/messages.d.ts +7 -0
- package/dist/types/src/Select/Select.d.ts +69 -0
- package/dist/types/src/Select/Select.style.d.ts +56 -0
- package/dist/types/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.d.ts +5 -0
- package/dist/types/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.ios.d.ts +5 -0
- package/dist/types/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.style.d.ts +30 -0
- package/dist/types/src/Select/components/SelectDefaultPicker/index.d.ts +1 -0
- package/dist/types/src/Select/components/SelectDefaultPicker/messages.d.ts +7 -0
- package/dist/types/src/Select/components/SelectIOSPicker/SelectIOSPicker.d.ts +10 -0
- package/dist/types/src/Select/components/SelectIOSPicker/index.d.ts +1 -0
- package/dist/types/src/Select/components/SelectInternalPicker/SelectInternalPicker.d.ts +3 -0
- package/dist/types/src/Select/components/SelectInternalPicker/index.d.ts +1 -0
- package/dist/types/src/Select/components/SelectInternalPicker/utils.d.ts +3 -0
- package/dist/types/src/Select/components/SelectPressable/SelectPressable.d.ts +11 -0
- package/dist/types/src/Select/components/SelectPressable/SelectPressable.style.d.ts +5 -0
- package/dist/types/src/Select/components/SelectPressable/index.d.ts +1 -0
- package/dist/types/src/Select/index.d.ts +1 -0
- package/dist/types/src/Select/messages.d.ts +12 -0
- package/dist/types/src/Select/types.d.ts +38 -0
- package/dist/types/src/index.d.ts +2 -0
- package/ios/ComponentsNative-Bridging-Header.h +2 -0
- package/ios/ComponentsNative.xcodeproj/project.pbxproj +303 -0
- package/ios/Picker/ATLFallBackPickerView.swift +13 -0
- package/ios/Picker/ATLPickerOption.swift +44 -0
- package/ios/Picker/ATLPickerView.swift +61 -0
- package/ios/Picker/RCTATLPickerManager.m +26 -0
- package/ios/Picker/RCTATLPickerManager.swift +25 -0
- package/jobber-components-native.podspec +35 -0
- package/package.json +19 -3
- package/src/InputNumber/InputNumber.test.tsx +323 -0
- package/src/InputNumber/InputNumber.tsx +126 -0
- package/src/InputNumber/index.ts +2 -0
- package/src/InputNumber/messages.ts +10 -0
- package/src/Select/Select.style.ts +51 -0
- package/src/Select/Select.test.tsx +323 -0
- package/src/Select/Select.tsx +240 -0
- package/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.ios.tsx +64 -0
- package/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.style.ts +30 -0
- package/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.test.tsx +76 -0
- package/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.tsx +45 -0
- package/src/Select/components/SelectDefaultPicker/index.ts +1 -0
- package/src/Select/components/SelectDefaultPicker/messages.ts +9 -0
- package/src/Select/components/SelectIOSPicker/SelectIOSPicker.tsx +16 -0
- package/src/Select/components/SelectIOSPicker/index.ts +1 -0
- package/src/Select/components/SelectInternalPicker/SelectInternalPicker.test.tsx +100 -0
- package/src/Select/components/SelectInternalPicker/SelectInternalPicker.tsx +33 -0
- package/src/Select/components/SelectInternalPicker/index.ts +1 -0
- package/src/Select/components/SelectInternalPicker/utils.ts +20 -0
- package/src/Select/components/SelectPressable/SelectPressable.style.ts +8 -0
- package/src/Select/components/SelectPressable/SelectPressable.tsx +32 -0
- package/src/Select/components/SelectPressable/index.ts +1 -0
- package/src/Select/index.ts +1 -0
- package/src/Select/messages.ts +14 -0
- package/src/Select/types.ts +46 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineMessages } from "react-intl";
|
|
2
|
+
|
|
3
|
+
export const messages = defineMessages({
|
|
4
|
+
notANumberError: {
|
|
5
|
+
id: "notANumberError",
|
|
6
|
+
defaultMessage: "Enter a number",
|
|
7
|
+
description:
|
|
8
|
+
"Error message shown when a non-numeric value is typed in number input",
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { commonInputStyles } from "../InputFieldWrapper";
|
|
3
|
+
import { tokens } from "../utils/design";
|
|
4
|
+
|
|
5
|
+
export const styles = StyleSheet.create({
|
|
6
|
+
container: StyleSheet.flatten([
|
|
7
|
+
commonInputStyles.container,
|
|
8
|
+
{
|
|
9
|
+
flexDirection: "column",
|
|
10
|
+
justifyContent: "flex-end",
|
|
11
|
+
minHeight: tokens["space-largest"] + tokens["border-base"],
|
|
12
|
+
},
|
|
13
|
+
]),
|
|
14
|
+
|
|
15
|
+
input: StyleSheet.flatten([
|
|
16
|
+
commonInputStyles.input,
|
|
17
|
+
{
|
|
18
|
+
flexDirection: "row",
|
|
19
|
+
flexGrow: 0,
|
|
20
|
+
paddingBottom: tokens["space-smaller"],
|
|
21
|
+
minHeight: 0,
|
|
22
|
+
minWidth: "100%",
|
|
23
|
+
},
|
|
24
|
+
]),
|
|
25
|
+
|
|
26
|
+
value: {
|
|
27
|
+
flexGrow: 1,
|
|
28
|
+
flexShrink: 1,
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
icon: {
|
|
32
|
+
flexGrow: 0,
|
|
33
|
+
flexShrink: 0,
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
invalid: {
|
|
37
|
+
color: tokens["color-critical"],
|
|
38
|
+
borderColor: tokens["color-critical"],
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
errorMessageWrapperIcon: {
|
|
42
|
+
flex: 0,
|
|
43
|
+
flexBasis: "auto",
|
|
44
|
+
paddingTop: tokens["space-minuscule"],
|
|
45
|
+
paddingRight: tokens["space-smaller"],
|
|
46
|
+
},
|
|
47
|
+
messageWrapper: {
|
|
48
|
+
paddingTop: tokens["space-smaller"],
|
|
49
|
+
flexDirection: "row",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
RenderAPI,
|
|
4
|
+
cleanup,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
} from "@testing-library/react-native";
|
|
8
|
+
import { tokens } from "@jobber/design/foundation";
|
|
9
|
+
import { AccessibilityInfo } from "react-native";
|
|
10
|
+
import { Option, Select } from ".";
|
|
11
|
+
import { messages } from "./messages";
|
|
12
|
+
import { SelectInternalPicker } from "./components/SelectInternalPicker";
|
|
13
|
+
|
|
14
|
+
const A11yInfoSpy = jest.spyOn(AccessibilityInfo, "isScreenReaderEnabled");
|
|
15
|
+
|
|
16
|
+
const onChange = jest.fn();
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
A11yInfoSpy.mockImplementation(() => Promise.resolve(false));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
cleanup();
|
|
24
|
+
jest.resetAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("Select", () => {
|
|
28
|
+
it("renders a Select", () => {
|
|
29
|
+
const component = render(
|
|
30
|
+
<Select onChange={onChange}>
|
|
31
|
+
<Option value={"one"}>one</Option>
|
|
32
|
+
<Option value={"2"}>2</Option>
|
|
33
|
+
</Select>,
|
|
34
|
+
);
|
|
35
|
+
expect(component.getByTestId("arrowDown")).toBeDefined();
|
|
36
|
+
expect(
|
|
37
|
+
component.getByText(messages.emptyValue.defaultMessage, {
|
|
38
|
+
includeHiddenElements: true,
|
|
39
|
+
}),
|
|
40
|
+
).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("renders a Select with a label", () => {
|
|
44
|
+
const label = "Press me";
|
|
45
|
+
const { getByText } = render(
|
|
46
|
+
<Select onChange={onChange} label={label}>
|
|
47
|
+
<Option value={"1"}>1</Option>
|
|
48
|
+
<Option value={"2"}>2</Option>
|
|
49
|
+
</Select>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(getByText(label, { includeHiddenElements: true })).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("renders a Select with a placeholder", () => {
|
|
56
|
+
const placeholder = "I am a placeholder";
|
|
57
|
+
const { getByText } = render(
|
|
58
|
+
<Select onChange={onChange} placeholder={placeholder}>
|
|
59
|
+
<Option value={"1"}>1</Option>
|
|
60
|
+
<Option value={"2"}>2</Option>
|
|
61
|
+
</Select>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(
|
|
65
|
+
getByText(placeholder, { includeHiddenElements: true }),
|
|
66
|
+
).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders a Select with assistive text", () => {
|
|
70
|
+
const assistiveText = "You need to pick something";
|
|
71
|
+
const { getByText } = render(
|
|
72
|
+
<Select onChange={onChange} assistiveText={assistiveText}>
|
|
73
|
+
<Option value={"1"}>1</Option>
|
|
74
|
+
<Option value={"2"}>2</Option>
|
|
75
|
+
</Select>,
|
|
76
|
+
);
|
|
77
|
+
expect(
|
|
78
|
+
getByText(assistiveText, { includeHiddenElements: true }),
|
|
79
|
+
).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("when invalid", () => {
|
|
83
|
+
const labelText = "labelText";
|
|
84
|
+
|
|
85
|
+
it("renders an invalid Select", () => {
|
|
86
|
+
const { getByText } = render(
|
|
87
|
+
<Select onChange={onChange} invalid={true} label={labelText}>
|
|
88
|
+
<Option value={"1"}>1</Option>
|
|
89
|
+
<Option value={"2"}>2</Option>
|
|
90
|
+
</Select>,
|
|
91
|
+
);
|
|
92
|
+
expect(
|
|
93
|
+
getByText(labelText, { includeHiddenElements: true }).props.style,
|
|
94
|
+
).toContainEqual({
|
|
95
|
+
color: tokens["color-critical"],
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("renders an invalid Select with placeholder", () => {
|
|
100
|
+
const placeholder = "Place me in the holder";
|
|
101
|
+
const { getByText } = render(
|
|
102
|
+
<Select
|
|
103
|
+
label={labelText}
|
|
104
|
+
onChange={onChange}
|
|
105
|
+
invalid={true}
|
|
106
|
+
placeholder={placeholder}
|
|
107
|
+
>
|
|
108
|
+
<Option value={"1"}>1</Option>
|
|
109
|
+
<Option value={"2"}>2</Option>
|
|
110
|
+
</Select>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(
|
|
114
|
+
getByText(placeholder, { includeHiddenElements: true }),
|
|
115
|
+
).toBeDefined();
|
|
116
|
+
expect(
|
|
117
|
+
getByText(labelText, { includeHiddenElements: true }).props.style,
|
|
118
|
+
).toContainEqual({
|
|
119
|
+
color: tokens["color-critical"],
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("renders a disabled Select", () => {
|
|
125
|
+
const labelText = "labelText";
|
|
126
|
+
const { getByText } = render(
|
|
127
|
+
<Select label={labelText} onChange={onChange} disabled={true}>
|
|
128
|
+
<Option value={"1"}>1</Option>
|
|
129
|
+
<Option value={"2"}>2</Option>
|
|
130
|
+
</Select>,
|
|
131
|
+
);
|
|
132
|
+
expect(
|
|
133
|
+
getByText(labelText, { includeHiddenElements: true }).props.style,
|
|
134
|
+
).toContainEqual({
|
|
135
|
+
color: tokens["color-disabled"],
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("renders a Select with a value provided", () => {
|
|
140
|
+
const expectedValue = "That should be me";
|
|
141
|
+
const { getByText } = render(
|
|
142
|
+
<Select onChange={onChange} value={"2"}>
|
|
143
|
+
<Option value={"1"}>1</Option>
|
|
144
|
+
<Option value={"2"}>{expectedValue}</Option>
|
|
145
|
+
</Select>,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(
|
|
149
|
+
getByText(expectedValue, { includeHiddenElements: true }),
|
|
150
|
+
).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("renders a Select with a defaultValue provided", () => {
|
|
154
|
+
const expectedValue = "It's the 3rd value";
|
|
155
|
+
const { getByText } = render(
|
|
156
|
+
<Select onChange={onChange} defaultValue={"3"}>
|
|
157
|
+
<Option value={"1"}>1</Option>
|
|
158
|
+
<Option value={"2"}>2</Option>
|
|
159
|
+
<Option value={"3"}>{expectedValue}</Option>
|
|
160
|
+
</Select>,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(
|
|
164
|
+
getByText(expectedValue, { includeHiddenElements: true }),
|
|
165
|
+
).toBeDefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("fires the onChange callback", () => {
|
|
169
|
+
it("fires", () => {
|
|
170
|
+
const { getByTestId } = render(
|
|
171
|
+
<Select onChange={onChange} value={"2"}>
|
|
172
|
+
<Option value={"1"}>1</Option>
|
|
173
|
+
<Option value={"2"}>2</Option>
|
|
174
|
+
</Select>,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const select = getByTestId("ATL-Select").findByType(SelectInternalPicker);
|
|
178
|
+
expect(select).toBeTruthy();
|
|
179
|
+
fireEvent(select, "onChange", "1");
|
|
180
|
+
expect(onChange).toHaveBeenCalledWith("1");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("Invalid value", () => {
|
|
185
|
+
it("renders with the empty value option", () => {
|
|
186
|
+
const { getByText } = render(
|
|
187
|
+
<Select onChange={onChange} value={"invalid"}>
|
|
188
|
+
<Option value={"first"}>first</Option>
|
|
189
|
+
<Option value={"2"}>2</Option>
|
|
190
|
+
</Select>,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(
|
|
194
|
+
getByText(messages.emptyValue.defaultMessage, {
|
|
195
|
+
includeHiddenElements: true,
|
|
196
|
+
}),
|
|
197
|
+
).toBeDefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("renders with the placeholder", () => {
|
|
201
|
+
const { getByText } = render(
|
|
202
|
+
<Select
|
|
203
|
+
onChange={onChange}
|
|
204
|
+
value={"invalid"}
|
|
205
|
+
placeholder={"Make a selection"}
|
|
206
|
+
>
|
|
207
|
+
<Option value={"1"}>1</Option>
|
|
208
|
+
<Option value={"2"}>2</Option>
|
|
209
|
+
</Select>,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(
|
|
213
|
+
getByText("Make a selection", { includeHiddenElements: true }),
|
|
214
|
+
).toBeDefined();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("accessibilityLabel", () => {
|
|
219
|
+
it("uses accessibilityLabel if specified", () => {
|
|
220
|
+
const { getByLabelText } = render(
|
|
221
|
+
<Select
|
|
222
|
+
onChange={onChange}
|
|
223
|
+
label="label"
|
|
224
|
+
accessibilityLabel="accessibilityLabel"
|
|
225
|
+
>
|
|
226
|
+
<Option value={"1"}>1</Option>
|
|
227
|
+
<Option value={"2"}>2</Option>
|
|
228
|
+
</Select>,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
expect(getByLabelText("accessibilityLabel")).toBeTruthy();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("when validations are passed to the component", () => {
|
|
236
|
+
describe("validations fail", () => {
|
|
237
|
+
let tree: RenderAPI;
|
|
238
|
+
const labelText = "labelText";
|
|
239
|
+
const errorMsg = "Too short";
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
tree = render(
|
|
242
|
+
<Select
|
|
243
|
+
label={labelText}
|
|
244
|
+
onChange={onChange}
|
|
245
|
+
value={"Watermelon"}
|
|
246
|
+
validations={{
|
|
247
|
+
minLength: { value: 60, message: errorMsg },
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
<Option value={"Apple"}>Apple</Option>
|
|
251
|
+
<Option value={"Watermelon"}>Watermelon</Option>
|
|
252
|
+
</Select>,
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("renders the error message when there is an error", async () => {
|
|
257
|
+
const select = tree
|
|
258
|
+
.getByTestId("ATL-Select")
|
|
259
|
+
.findByType(SelectInternalPicker);
|
|
260
|
+
fireEvent(select, "onChange", "Apple");
|
|
261
|
+
expect(
|
|
262
|
+
await tree.findByText(errorMsg, { includeHiddenElements: true }),
|
|
263
|
+
).toBeTruthy();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("shows the invalid colours", async () => {
|
|
267
|
+
const select = tree
|
|
268
|
+
.getByTestId("ATL-Select")
|
|
269
|
+
.findByType(SelectInternalPicker);
|
|
270
|
+
fireEvent(select, "onChange", "Apple");
|
|
271
|
+
expect(
|
|
272
|
+
(await tree.findByText(labelText, { includeHiddenElements: true }))
|
|
273
|
+
.props.style,
|
|
274
|
+
).toContainEqual({
|
|
275
|
+
color: tokens["color-critical"],
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("validations passes", () => {
|
|
281
|
+
let tree: RenderAPI;
|
|
282
|
+
const labelText = "labelText";
|
|
283
|
+
const errorMsg = "Not too short";
|
|
284
|
+
beforeEach(() => {
|
|
285
|
+
tree = render(
|
|
286
|
+
<Select
|
|
287
|
+
label={labelText}
|
|
288
|
+
onChange={onChange}
|
|
289
|
+
value={"Watermelon"}
|
|
290
|
+
validations={{
|
|
291
|
+
minLength: { value: 4, message: errorMsg },
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
<Option value={"Apple"}>Apple</Option>
|
|
295
|
+
<Option value={"Watermelon"}>Watermelon</Option>
|
|
296
|
+
</Select>,
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("does not render any error messages", () => {
|
|
301
|
+
const select = tree
|
|
302
|
+
.getByTestId("ATL-Select")
|
|
303
|
+
.findByType(SelectInternalPicker);
|
|
304
|
+
expect(select).toBeTruthy();
|
|
305
|
+
fireEvent(select, "onChange", "Apple");
|
|
306
|
+
expect(tree.queryByText(errorMsg)).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("has non-critical colours", () => {
|
|
310
|
+
const select = tree
|
|
311
|
+
.getByTestId("ATL-Select")
|
|
312
|
+
.findByType(SelectInternalPicker);
|
|
313
|
+
fireEvent(select, "onChange", "Apple");
|
|
314
|
+
expect(
|
|
315
|
+
tree.getByText(labelText, { includeHiddenElements: true }).props
|
|
316
|
+
.style,
|
|
317
|
+
).toContainEqual({
|
|
318
|
+
color: tokens["color-text--secondary"],
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import React, { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useIntl } from "react-intl";
|
|
4
|
+
import { RegisterOptions } from "react-hook-form";
|
|
5
|
+
import { styles } from "./Select.style";
|
|
6
|
+
import { SelectInternalPicker } from "./components/SelectInternalPicker";
|
|
7
|
+
import { messages } from "./messages";
|
|
8
|
+
import { InputFieldWrapper } from "../InputFieldWrapper";
|
|
9
|
+
import { Icon } from "../Icon";
|
|
10
|
+
import { TextVariation } from "../Typography";
|
|
11
|
+
import { Text } from "../Text";
|
|
12
|
+
import { useFormController } from "../hooks";
|
|
13
|
+
|
|
14
|
+
export interface SelectOption {
|
|
15
|
+
/**
|
|
16
|
+
* Text that shows up as the option
|
|
17
|
+
*/
|
|
18
|
+
readonly children: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The value that gets returned when an option is selected
|
|
22
|
+
*/
|
|
23
|
+
readonly value: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SelectProps {
|
|
27
|
+
/**
|
|
28
|
+
* Current value of the component
|
|
29
|
+
*/
|
|
30
|
+
readonly value?: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Default value for when the component is uncontrolled
|
|
34
|
+
*/
|
|
35
|
+
readonly defaultValue?: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Adds a first option to let users select a "no value".
|
|
39
|
+
* Placeholder item selected by default until a selection is made.
|
|
40
|
+
*/
|
|
41
|
+
readonly placeholder?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Label text shown above the selection.
|
|
45
|
+
*/
|
|
46
|
+
readonly label?: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Help text shown below the control.
|
|
50
|
+
*/
|
|
51
|
+
readonly assistiveText?: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Callback that provides the new value when the selection changes
|
|
55
|
+
*/
|
|
56
|
+
readonly onChange?: (newValue?: string) => void;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Disables input selection
|
|
60
|
+
*/
|
|
61
|
+
readonly disabled?: boolean;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Indicates the current selection is invalid
|
|
65
|
+
*/
|
|
66
|
+
readonly invalid?: boolean;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* VoiceOver will read this string when a user selects the element
|
|
70
|
+
*/
|
|
71
|
+
readonly accessibilityLabel?: string;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Helps users understand what will happen when they perform an action
|
|
75
|
+
*/
|
|
76
|
+
readonly accessibilityHint?: string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Name of the input.
|
|
80
|
+
*/
|
|
81
|
+
readonly name?: string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The options to select from
|
|
85
|
+
*/
|
|
86
|
+
readonly children: ReactElement<SelectOption>[];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The validations that will mark this component as invalid
|
|
90
|
+
*/
|
|
91
|
+
readonly validations?: RegisterOptions;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function Select({
|
|
95
|
+
value,
|
|
96
|
+
defaultValue,
|
|
97
|
+
onChange,
|
|
98
|
+
children,
|
|
99
|
+
placeholder,
|
|
100
|
+
label,
|
|
101
|
+
assistiveText,
|
|
102
|
+
disabled,
|
|
103
|
+
invalid,
|
|
104
|
+
validations,
|
|
105
|
+
accessibilityLabel,
|
|
106
|
+
name,
|
|
107
|
+
}: SelectProps): JSX.Element {
|
|
108
|
+
const { field, error } = useFormController({
|
|
109
|
+
name,
|
|
110
|
+
validations,
|
|
111
|
+
value: value ?? defaultValue,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const { formatMessage } = useIntl();
|
|
115
|
+
const internalValue = value ?? field.value;
|
|
116
|
+
const textVariation = getTextVariation({
|
|
117
|
+
disabled,
|
|
118
|
+
invalid: invalid || !!error,
|
|
119
|
+
});
|
|
120
|
+
const valueTextVariation = disabled ? "disabled" : undefined;
|
|
121
|
+
const hasValue = internalValue && internalValue?.length > 0;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<InputFieldWrapper
|
|
125
|
+
error={error}
|
|
126
|
+
invalid={invalid || !!error}
|
|
127
|
+
hasValue={hasValue}
|
|
128
|
+
styleOverride={{
|
|
129
|
+
container: { borderBottomWidth: undefined },
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<View
|
|
133
|
+
testID="ATL-Select"
|
|
134
|
+
accessible={true}
|
|
135
|
+
accessibilityLabel={getA11yLabel()}
|
|
136
|
+
accessibilityValue={{ text: getValue() }}
|
|
137
|
+
accessibilityHint={formatMessage(messages.a11yHint)}
|
|
138
|
+
accessibilityRole="button"
|
|
139
|
+
accessibilityState={{ disabled: disabled }}
|
|
140
|
+
>
|
|
141
|
+
<SelectInternalPicker
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
options={getOptions()}
|
|
144
|
+
onChange={handleChange}
|
|
145
|
+
>
|
|
146
|
+
<View
|
|
147
|
+
style={[styles.container, (invalid || !!error) && styles.invalid]}
|
|
148
|
+
>
|
|
149
|
+
{label && (
|
|
150
|
+
<Text
|
|
151
|
+
level="textSupporting"
|
|
152
|
+
variation={textVariation}
|
|
153
|
+
hideFromScreenReader={true}
|
|
154
|
+
>
|
|
155
|
+
{label}
|
|
156
|
+
</Text>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
<View style={styles.input}>
|
|
160
|
+
<View style={styles.value}>
|
|
161
|
+
<Text
|
|
162
|
+
variation={disabled ? "disabled" : "base"}
|
|
163
|
+
hideFromScreenReader={true}
|
|
164
|
+
>
|
|
165
|
+
{getValue()}
|
|
166
|
+
</Text>
|
|
167
|
+
</View>
|
|
168
|
+
<View style={styles.icon}>
|
|
169
|
+
<Icon name="arrowDown" color={valueTextVariation} />
|
|
170
|
+
</View>
|
|
171
|
+
</View>
|
|
172
|
+
</View>
|
|
173
|
+
</SelectInternalPicker>
|
|
174
|
+
|
|
175
|
+
{assistiveText && (
|
|
176
|
+
<Text
|
|
177
|
+
level="textSupporting"
|
|
178
|
+
variation={textVariation}
|
|
179
|
+
hideFromScreenReader={true}
|
|
180
|
+
>
|
|
181
|
+
{assistiveText}
|
|
182
|
+
</Text>
|
|
183
|
+
)}
|
|
184
|
+
</View>
|
|
185
|
+
</InputFieldWrapper>
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
function getA11yLabel(): string | undefined {
|
|
189
|
+
let text = [accessibilityLabel || label, assistiveText];
|
|
190
|
+
text = text.filter(Boolean);
|
|
191
|
+
return text.join(", ");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function handleChange(val: string) {
|
|
195
|
+
onChange?.(val);
|
|
196
|
+
// need this onBlur for validations to occur
|
|
197
|
+
field.onBlur();
|
|
198
|
+
field.onChange(val);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getOptions() {
|
|
202
|
+
const options = children.map(({ props }) => ({
|
|
203
|
+
label: props.children,
|
|
204
|
+
value: props.value,
|
|
205
|
+
isActive: props.value === internalValue,
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
if (!internalValue || placeholder) {
|
|
209
|
+
options.unshift({
|
|
210
|
+
label: placeholder || formatMessage(messages.emptyValue),
|
|
211
|
+
value: "",
|
|
212
|
+
isActive: !internalValue,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return options;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getValue() {
|
|
220
|
+
const options = getOptions();
|
|
221
|
+
|
|
222
|
+
const activeValue = options.find(option => option.isActive);
|
|
223
|
+
return (
|
|
224
|
+
activeValue?.label || placeholder || formatMessage(messages.emptyValue)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function getTextVariation({
|
|
230
|
+
invalid,
|
|
231
|
+
disabled,
|
|
232
|
+
}: Pick<SelectProps, "invalid" | "disabled">): TextVariation {
|
|
233
|
+
if (invalid) return "error";
|
|
234
|
+
if (disabled) return "disabled";
|
|
235
|
+
return "subdued";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function Option({ children }: SelectOption): JSX.Element {
|
|
239
|
+
return <>{children}</>;
|
|
240
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
// Need to use iOS style button
|
|
4
|
+
Button,
|
|
5
|
+
Modal,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
View,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import { Picker } from "@react-native-picker/picker";
|
|
10
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
11
|
+
import { useIntl } from "react-intl";
|
|
12
|
+
import { styles } from "./SelectDefaultPicker.style";
|
|
13
|
+
import { messages } from "./messages";
|
|
14
|
+
import { SelectInternalPickerProps } from "../../types";
|
|
15
|
+
import { SelectPressable } from "../SelectPressable/SelectPressable";
|
|
16
|
+
|
|
17
|
+
type SelectDefaultPickerProps = SelectInternalPickerProps;
|
|
18
|
+
|
|
19
|
+
export function SelectDefaultPicker({
|
|
20
|
+
children,
|
|
21
|
+
options,
|
|
22
|
+
onChange,
|
|
23
|
+
}: SelectDefaultPickerProps): JSX.Element {
|
|
24
|
+
const [show, setShow] = useState(false);
|
|
25
|
+
const { formatMessage } = useIntl();
|
|
26
|
+
const selectedLanguage = options.find(option => option.isActive);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<SelectPressable onPress={showPicker}>{children}</SelectPressable>
|
|
31
|
+
<Modal
|
|
32
|
+
visible={show}
|
|
33
|
+
transparent
|
|
34
|
+
animationType="slide"
|
|
35
|
+
onRequestClose={hidePicker}
|
|
36
|
+
>
|
|
37
|
+
<TouchableOpacity style={styles.overlay} onPress={hidePicker} />
|
|
38
|
+
<View style={styles.actionBar}>
|
|
39
|
+
<Button title={formatMessage(messages.done)} onPress={hidePicker} />
|
|
40
|
+
</View>
|
|
41
|
+
<View style={styles.pickerContainer} testID="select-wheel-picker">
|
|
42
|
+
<SafeAreaView edges={["bottom"]}>
|
|
43
|
+
<Picker
|
|
44
|
+
selectedValue={selectedLanguage?.value}
|
|
45
|
+
onValueChange={onChange}
|
|
46
|
+
>
|
|
47
|
+
{options.map(({ label, value }, i) => (
|
|
48
|
+
<Picker.Item key={i} label={label} value={value} />
|
|
49
|
+
))}
|
|
50
|
+
</Picker>
|
|
51
|
+
</SafeAreaView>
|
|
52
|
+
</View>
|
|
53
|
+
</Modal>
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
function showPicker() {
|
|
58
|
+
setShow(true);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hidePicker() {
|
|
62
|
+
setShow(false);
|
|
63
|
+
}
|
|
64
|
+
}
|