@khanacademy/wonder-blocks-birthday-picker 1.0.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/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@khanacademy/wonder-blocks-birthday-picker",
3
+ "version": "1.0.0",
4
+ "design": "v1",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "description": "",
9
+ "main": "dist/index.js",
10
+ "module": "dist/es/index.js",
11
+ "source": "src/index.js",
12
+ "scripts": {
13
+ "test": "echo \"Error: no test specified\" && exit 1"
14
+ },
15
+ "dependencies": {
16
+ "@babel/runtime": "^7.16.3",
17
+ "@khanacademy/wonder-blocks-color": "^1.1.20",
18
+ "@khanacademy/wonder-blocks-core": "^4.2.0",
19
+ "@khanacademy/wonder-blocks-dropdown": "^2.6.3",
20
+ "@khanacademy/wonder-blocks-icon": "^1.2.24",
21
+ "@khanacademy/wonder-blocks-layout": "^1.4.6",
22
+ "@khanacademy/wonder-blocks-spacing": "^3.0.5",
23
+ "@khanacademy/wonder-blocks-typography": "^1.1.28"
24
+ },
25
+ "peerDependencies": {
26
+ "aphrodite": "^1.2.5",
27
+ "moment": "^2.24.0",
28
+ "react": "16.14.0"
29
+ },
30
+ "devDependencies": {
31
+ "wb-dev-build-settings": "^0.3.0"
32
+ },
33
+ "author": "",
34
+ "license": "MIT"
35
+ }
@@ -0,0 +1,38 @@
1
+ // @flow
2
+ import {defaultLabels} from "../birthday-picker.js";
3
+
4
+ export default {
5
+ defaultValue: {
6
+ control: {type: "text"},
7
+ description:
8
+ "The default value to populate the birthdate with. Should be in the format: `YYYY-MM-DD` (e.g. 2021-05-26). It's only used to populate the initial value as this is an uncontrolled component.",
9
+ table: {
10
+ type: {
11
+ summary: "string",
12
+ },
13
+ },
14
+ },
15
+ labels: {
16
+ control: {type: "object"},
17
+ description:
18
+ "The object containing the custom labels used inside this component.",
19
+ defaultValue: defaultLabels,
20
+ table: {
21
+ type: {
22
+ summary: "Labels",
23
+ detail: "{month?: string, year?: string, day?: string, errorMessage?: string}",
24
+ },
25
+ },
26
+ },
27
+ onChange: {
28
+ action: "onChanged",
29
+ description:
30
+ "Listen for changes to the birthdate. Could be a string in the `YYYY-MM-DD` format or `null`.",
31
+ table: {
32
+ category: "Events",
33
+ type: {
34
+ summary: "(date: ?string) => mixed",
35
+ },
36
+ },
37
+ },
38
+ };
@@ -0,0 +1,89 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import {View} from "@khanacademy/wonder-blocks-core";
5
+
6
+ import type {StoryComponentType} from "@storybook/react";
7
+
8
+ import ComponentInfo from "../../../../../.storybook/components/component-info.js";
9
+ import {name, version} from "../../../package.json";
10
+
11
+ import ArgTypes from "./birthday-picker.argtypes.js";
12
+ import BirthdayPicker from "../birthday-picker.js";
13
+
14
+ export default {
15
+ title: "BirthdayPicker",
16
+ component: BirthdayPicker,
17
+ argTypes: ArgTypes,
18
+ parameters: {
19
+ componentSubtitle: ((
20
+ <ComponentInfo name={name} version={version} />
21
+ ): any),
22
+ docs: {
23
+ description: {
24
+ component: null,
25
+ },
26
+ source: {
27
+ // See https://github.com/storybookjs/storybook/issues/12596
28
+ excludeDecorators: true,
29
+ },
30
+ },
31
+ },
32
+ decorators: [(Story: any): React.Node => <View>{Story()}</View>],
33
+ };
34
+
35
+ /**
36
+ * Default BirthdayPicker example. It will be rendered as the first/default
37
+ * story and it can be interacted with the controls panel in the Browser.
38
+ */
39
+ const Template = (args) => <BirthdayPicker {...args} />;
40
+
41
+ export const BirthdayPickerDefault: StoryComponentType = Template.bind({});
42
+
43
+ BirthdayPickerDefault.args = {
44
+ onChange: () => {},
45
+ defaultValue: "",
46
+ };
47
+
48
+ BirthdayPickerDefault.parameters = {
49
+ chromatic: {
50
+ disableSnapshot: true,
51
+ },
52
+ };
53
+
54
+ export const BirthdayPickerWithDefaultValue: StoryComponentType = Template.bind(
55
+ {},
56
+ );
57
+
58
+ BirthdayPickerWithDefaultValue.args = {
59
+ onChange: () => {},
60
+ defaultValue: "2021-07-19",
61
+ };
62
+
63
+ BirthdayPickerWithDefaultValue.parameters = {
64
+ chromatic: {
65
+ disableSnapshot: true,
66
+ },
67
+ };
68
+
69
+ export const InvalidBirthdayPicker: StoryComponentType = Template.bind({});
70
+
71
+ InvalidBirthdayPicker.args = {
72
+ onChange: () => {},
73
+ defaultValue: "2030-07-19",
74
+ };
75
+
76
+ export const BirthdayPickerWithCustomLabels: StoryComponentType = Template.bind(
77
+ {},
78
+ );
79
+
80
+ BirthdayPickerWithCustomLabels.args = {
81
+ onChange: () => {},
82
+ defaultValue: "",
83
+ labels: {
84
+ day: "Día",
85
+ month: "Mes",
86
+ year: "Año",
87
+ errorMessage: "Por favor seleccione una fecha válida.",
88
+ },
89
+ };
@@ -0,0 +1,420 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import moment from "moment";
4
+ import {render, screen} from "@testing-library/react";
5
+ import * as DateMock from "jest-date-mock";
6
+ import userEvent from "../../../../../utils/testing/user-event.js";
7
+
8
+ import BirthdayPicker, {defaultLabels} from "../birthday-picker.js";
9
+
10
+ import type {Labels} from "../birthday-picker.js";
11
+
12
+ describe("BirthdayPicker", () => {
13
+ const today = new Date("2021-07-19T09:30:00Z");
14
+
15
+ describe("render", () => {
16
+ beforeEach(() => {
17
+ jest.useRealTimers();
18
+ DateMock.advanceTo(today);
19
+ });
20
+
21
+ it("renders without a default value", () => {
22
+ // Arrange
23
+
24
+ // Act
25
+ render(<BirthdayPicker onChange={() => {}} />);
26
+
27
+ const monthPicker = screen.getByTestId("birthday-picker-month");
28
+ const dayPicker = screen.getByTestId("birthday-picker-day");
29
+ const yearPicker = screen.getByTestId("birthday-picker-year");
30
+
31
+ // Assert
32
+ expect(monthPicker).toHaveTextContent("Month");
33
+ expect(dayPicker).toHaveTextContent("Day");
34
+ expect(yearPicker).toHaveTextContent("Year");
35
+ });
36
+
37
+ it("renders with a valid default value", () => {
38
+ // Arrange
39
+ const date = moment(today);
40
+ const defaultValue = date.format("YYYY-MM-DD");
41
+
42
+ // Act
43
+ render(
44
+ <BirthdayPicker
45
+ defaultValue={defaultValue}
46
+ onChange={() => {}}
47
+ />,
48
+ );
49
+
50
+ const monthPicker = screen.getByTestId("birthday-picker-month");
51
+ const dayPicker = screen.getByTestId("birthday-picker-day");
52
+ const yearPicker = screen.getByTestId("birthday-picker-year");
53
+
54
+ // Assert
55
+ expect(monthPicker).toHaveTextContent("Jul");
56
+ expect(dayPicker).toHaveTextContent("19");
57
+ expect(yearPicker).toHaveTextContent("2021");
58
+ expect(screen.queryByRole("alert")).not.toBeInTheDocument();
59
+ });
60
+
61
+ it("renders with a invalid default future value", () => {
62
+ // Arrange
63
+ jest.useRealTimers();
64
+ DateMock.advanceTo(today);
65
+ const date = moment(today).add(1, "day");
66
+ const defaultValue = date.format("YYYY-MM-DD");
67
+
68
+ // Act
69
+ render(
70
+ <BirthdayPicker
71
+ defaultValue={defaultValue}
72
+ onChange={() => {}}
73
+ />,
74
+ );
75
+
76
+ const monthPicker = screen.getByTestId("birthday-picker-month");
77
+ const dayPicker = screen.getByTestId("birthday-picker-day");
78
+ const yearPicker = screen.getByTestId("birthday-picker-year");
79
+
80
+ // Assert
81
+ expect(monthPicker).toHaveTextContent("Jul");
82
+ expect(dayPicker).toHaveTextContent("20");
83
+ expect(yearPicker).toHaveTextContent("2021");
84
+ });
85
+
86
+ it("renders an error with a invalid default future value", async () => {
87
+ // Arrange
88
+ const date = moment(today).add(1, "day");
89
+ const defaultValue = date.format("YYYY-MM-DD");
90
+
91
+ // Act
92
+ render(
93
+ <BirthdayPicker
94
+ defaultValue={defaultValue}
95
+ onChange={() => {}}
96
+ />,
97
+ );
98
+
99
+ // Assert
100
+ expect(screen.getByRole("alert")).toBeInTheDocument();
101
+ });
102
+
103
+ it("renders with an invalid default value", () => {
104
+ // Arrange
105
+ const defaultValue = "2021-02-31"; // There is no Feb 31st
106
+
107
+ // Act
108
+ render(
109
+ <BirthdayPicker
110
+ defaultValue={defaultValue}
111
+ onChange={() => {}}
112
+ />,
113
+ );
114
+
115
+ const monthPicker = screen.getByTestId("birthday-picker-month");
116
+ const dayPicker = screen.getByTestId("birthday-picker-day");
117
+ const yearPicker = screen.getByTestId("birthday-picker-year");
118
+
119
+ // Assert
120
+ expect(monthPicker).toHaveTextContent("Month");
121
+ expect(dayPicker).toHaveTextContent("Day");
122
+ expect(yearPicker).toHaveTextContent("Year");
123
+ });
124
+
125
+ it("renders an error with an invalid default value", async () => {
126
+ // Arrange
127
+ const defaultValue = "2021-02-31";
128
+
129
+ // Act
130
+ render(
131
+ <BirthdayPicker
132
+ defaultValue={defaultValue}
133
+ onChange={() => {}}
134
+ />,
135
+ );
136
+
137
+ // Assert
138
+ expect(screen.getByRole("alert")).toBeInTheDocument();
139
+ });
140
+ });
141
+
142
+ describe("onChange", () => {
143
+ beforeEach(() => {
144
+ jest.useFakeTimers();
145
+ });
146
+
147
+ afterEach(() => {
148
+ jest.runOnlyPendingTimers();
149
+ jest.useRealTimers();
150
+ });
151
+
152
+ it("onChange triggers when a new value is selected", async () => {
153
+ // Arrange
154
+ const onChange = jest.fn();
155
+
156
+ // Act
157
+ render(<BirthdayPicker onChange={onChange} />);
158
+
159
+ userEvent.click(screen.getByTestId("birthday-picker-month"));
160
+ const monthOption = await screen.findByRole("option", {
161
+ name: "Jul",
162
+ });
163
+ userEvent.click(monthOption, undefined, {
164
+ skipPointerEventsCheck: true,
165
+ });
166
+
167
+ userEvent.click(screen.getByTestId("birthday-picker-day"));
168
+ const dayOption = await screen.findByRole("option", {name: "5"});
169
+ userEvent.click(dayOption, undefined, {
170
+ skipPointerEventsCheck: true,
171
+ });
172
+
173
+ userEvent.click(screen.getByTestId("birthday-picker-year"));
174
+ const yearOption = await screen.findByRole("option", {
175
+ name: "2021",
176
+ });
177
+ userEvent.click(yearOption, undefined, {
178
+ skipPointerEventsCheck: true,
179
+ });
180
+
181
+ // Assert
182
+ expect(onChange).toHaveBeenCalledWith("2021-07-05");
183
+ });
184
+
185
+ it("onChange triggers multiple times when a new value is selected", async () => {
186
+ // Arrange
187
+ const onChange = jest.fn();
188
+ render(<BirthdayPicker onChange={onChange} />);
189
+ // Pick one date
190
+ userEvent.click(screen.getByTestId("birthday-picker-month"));
191
+ const monthOption = await screen.findByRole("option", {
192
+ name: "Jul",
193
+ });
194
+ userEvent.click(monthOption, undefined, {
195
+ skipPointerEventsCheck: true,
196
+ });
197
+
198
+ userEvent.click(screen.getByTestId("birthday-picker-day"));
199
+ const dayOption = await screen.findByRole("option", {name: "5"});
200
+
201
+ userEvent.click(dayOption, undefined, {
202
+ skipPointerEventsCheck: true,
203
+ });
204
+
205
+ userEvent.click(screen.getByTestId("birthday-picker-year"));
206
+ const yearOption = await screen.findByRole("option", {
207
+ name: "2021",
208
+ });
209
+ userEvent.click(yearOption, undefined, {
210
+ skipPointerEventsCheck: true,
211
+ });
212
+
213
+ // Act
214
+ // Pick Another Date
215
+ userEvent.click(screen.getByTestId("birthday-picker-month"));
216
+ const monthOptionNew = await screen.findByRole("option", {
217
+ name: "Aug",
218
+ });
219
+ userEvent.click(monthOptionNew, undefined, {
220
+ skipPointerEventsCheck: true,
221
+ });
222
+
223
+ userEvent.click(screen.getByTestId("birthday-picker-day"));
224
+ const dayOptionNew = await screen.findByRole("option", {name: "9"});
225
+ userEvent.click(dayOptionNew, undefined, {
226
+ skipPointerEventsCheck: true,
227
+ });
228
+
229
+ userEvent.click(screen.getByTestId("birthday-picker-year"));
230
+ const yearOptionNew = await screen.findByRole("option", {
231
+ name: "2020",
232
+ });
233
+ userEvent.click(yearOptionNew, undefined, {
234
+ skipPointerEventsCheck: true,
235
+ });
236
+
237
+ // Assert
238
+ expect(onChange).toHaveBeenLastCalledWith("2020-08-09");
239
+ });
240
+
241
+ it("onChange triggers when a new value is selected after a default value is set", async () => {
242
+ // Arrange
243
+ const onChange = jest.fn();
244
+
245
+ // Act
246
+ render(
247
+ <BirthdayPicker
248
+ defaultValue="2017-07-17"
249
+ onChange={onChange}
250
+ />,
251
+ );
252
+
253
+ userEvent.click(screen.getByTestId("birthday-picker-month"));
254
+ const monthOption = await screen.findByRole("option", {
255
+ name: "Aug",
256
+ });
257
+ userEvent.click(monthOption, undefined, {
258
+ skipPointerEventsCheck: true,
259
+ });
260
+
261
+ userEvent.click(screen.getByTestId("birthday-picker-day"));
262
+ const dayOption = await screen.findByRole("option", {name: "9"});
263
+
264
+ userEvent.click(dayOption, undefined, {
265
+ skipPointerEventsCheck: true,
266
+ });
267
+
268
+ userEvent.click(screen.getByTestId("birthday-picker-year"));
269
+ const yearOption = await screen.findByRole("option", {
270
+ name: "2018",
271
+ });
272
+ userEvent.click(yearOption, undefined, {
273
+ skipPointerEventsCheck: true,
274
+ });
275
+
276
+ // Assert
277
+ expect(onChange).toHaveBeenCalledWith("2018-08-09");
278
+ });
279
+
280
+ it("onChange triggers with null when an invalid value is selected after a default value is set", () => {
281
+ // Arrange
282
+ const onChange = jest.fn();
283
+ let maybeInstance: ?BirthdayPicker = null;
284
+
285
+ // Act
286
+ render(
287
+ <BirthdayPicker
288
+ defaultValue="2017-07-17"
289
+ onChange={onChange}
290
+ ref={(node) => (maybeInstance = node)}
291
+ />,
292
+ );
293
+
294
+ if (!maybeInstance) {
295
+ throw new Error("BirthdayPicker instance is undefined");
296
+ }
297
+ const instance = maybeInstance;
298
+
299
+ // This test was written by calling methods on the instance because
300
+ // react-window (used by SingleSelect) doesn't show all of the items
301
+ // in the dropdown.
302
+ instance.handleMonthChange("1");
303
+ instance.handleDayChange("31");
304
+ instance.handleYearChange("2021");
305
+
306
+ // Assert
307
+ expect(onChange).toHaveBeenCalledWith(null);
308
+ });
309
+
310
+ it("onChange triggers only one null when multiple invalid values are selected after a default value is set", async () => {
311
+ // Arrange
312
+ const onChange = jest.fn();
313
+
314
+ // Act
315
+ render(
316
+ <BirthdayPicker
317
+ defaultValue="2021-02-31"
318
+ onChange={onChange}
319
+ />,
320
+ );
321
+
322
+ userEvent.click(screen.getByTestId("birthday-picker-year"));
323
+ const yearOption = await screen.findByRole("option", {
324
+ name: "2020",
325
+ });
326
+ userEvent.click(yearOption, undefined, {
327
+ skipPointerEventsCheck: true,
328
+ });
329
+
330
+ // Assert
331
+ expect(onChange).toHaveBeenCalledTimes(1);
332
+ });
333
+ });
334
+
335
+ describe("labels", () => {
336
+ const translatedLabels: $Shape<Labels> = {
337
+ month: "Mes",
338
+ day: "Día",
339
+ year: "Año",
340
+ errorMessage: "Por favor ingrese una fecha valida.",
341
+ };
342
+
343
+ beforeEach(() => {
344
+ jest.useRealTimers();
345
+ DateMock.advanceTo(today);
346
+ });
347
+
348
+ it.each([defaultLabels.month, defaultLabels.day, defaultLabels.year])(
349
+ "renders the placeholder as %s",
350
+ (label) => {
351
+ // Arrange
352
+
353
+ // Act
354
+ render(<BirthdayPicker onChange={() => {}} />);
355
+
356
+ // Assert
357
+ expect(screen.getByText(label)).toBeInTheDocument();
358
+ },
359
+ );
360
+
361
+ it.each([
362
+ translatedLabels.month,
363
+ translatedLabels.day,
364
+ translatedLabels.year,
365
+ ])("renders the translated placeholder as %s", (translatedLabel) => {
366
+ // Arrange
367
+
368
+ // Act
369
+ render(
370
+ <BirthdayPicker
371
+ onChange={() => {}}
372
+ labels={translatedLabels}
373
+ />,
374
+ );
375
+
376
+ // Assert
377
+ expect(screen.getByText(translatedLabel)).toBeInTheDocument();
378
+ });
379
+
380
+ it("merges correctly the labels", () => {
381
+ // Arrange
382
+
383
+ // Only passing some of the labels to verify if the others are
384
+ // merged correctly.
385
+ const partialLabels: $Shape<Labels> = {
386
+ month: "Mes",
387
+ year: "Año",
388
+ };
389
+
390
+ // Act
391
+ render(
392
+ <BirthdayPicker onChange={() => {}} labels={partialLabels} />,
393
+ );
394
+
395
+ // Assert
396
+ expect(screen.getByText("Mes")).toBeInTheDocument(); // es
397
+ expect(screen.getByText("Año")).toBeInTheDocument(); // es
398
+ expect(screen.getByText("Day")).toBeInTheDocument(); // EN
399
+ });
400
+
401
+ it("renders a translated error with an invalid default value", async () => {
402
+ // Arrange
403
+ const defaultValue = "2021-02-31"; // There is no Feb 31st
404
+
405
+ // Act
406
+ render(
407
+ <BirthdayPicker
408
+ defaultValue={defaultValue}
409
+ onChange={() => {}}
410
+ labels={translatedLabels}
411
+ />,
412
+ );
413
+
414
+ // Assert
415
+ expect(
416
+ screen.getByText(translatedLabels.errorMessage),
417
+ ).toBeInTheDocument();
418
+ });
419
+ });
420
+ });