@khanacademy/wonder-blocks-form 2.3.0 → 2.4.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/index.js CHANGED
@@ -254,6 +254,7 @@ function _extends() { _extends = Object.assign || function (target) { for (var i
254
254
 
255
255
 
256
256
 
257
+ const defaultErrorMessage = "This field is required.";
257
258
 
258
259
  // TODO(WB-1081): Change class name back to TextField after Styleguidist is gone.
259
260
 
@@ -271,7 +272,8 @@ class TextFieldInternal extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
271
272
  this.maybeValidate = newValue => {
272
273
  const {
273
274
  validate,
274
- onValidate
275
+ onValidate,
276
+ required
275
277
  } = this.props;
276
278
 
277
279
  if (validate) {
@@ -283,6 +285,16 @@ class TextFieldInternal extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
283
285
  onValidate(maybeError);
284
286
  }
285
287
  });
288
+ } else if (required) {
289
+ const requiredString = typeof required === "string" ? required : defaultErrorMessage;
290
+ const maybeError = newValue ? null : requiredString;
291
+ this.setState({
292
+ error: maybeError
293
+ }, () => {
294
+ if (onValidate) {
295
+ onValidate(maybeError);
296
+ }
297
+ });
286
298
  }
287
299
  };
288
300
 
@@ -321,14 +333,16 @@ class TextFieldInternal extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
321
333
  });
322
334
  };
323
335
 
324
- if (props.validate) {
336
+ if (props.validate && props.value !== "") {
325
337
  // Ensures error is updated on unmounted server-side renders
326
338
  this.state.error = props.validate(props.value) || null;
327
339
  }
328
340
  }
329
341
 
330
342
  componentDidMount() {
331
- this.maybeValidate(this.props.value);
343
+ if (this.props.value !== "") {
344
+ this.maybeValidate(this.props.value);
345
+ }
332
346
  }
333
347
 
334
348
  render() {
@@ -339,7 +353,6 @@ class TextFieldInternal extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
339
353
  disabled,
340
354
  onKeyDown,
341
355
  placeholder,
342
- required,
343
356
  light,
344
357
  style,
345
358
  testId,
@@ -355,6 +368,7 @@ class TextFieldInternal extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
355
368
  onValidate,
356
369
  validate,
357
370
  onChange,
371
+ required,
358
372
 
359
373
  /* eslint-enable no-unused-vars */
360
374
  // Should only include Aria related props
@@ -372,7 +386,6 @@ class TextFieldInternal extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
372
386
  onKeyDown: onKeyDown,
373
387
  onFocus: this.handleFocus,
374
388
  onBlur: this.handleBlur,
375
- required: required,
376
389
  "data-test-id": testId,
377
390
  readOnly: readOnly,
378
391
  autoComplete: autoComplete,
@@ -926,6 +939,7 @@ class LabeledTextFieldInternal extends react__WEBPACK_IMPORTED_MODULE_0__["Compo
926
939
  description,
927
940
  value,
928
941
  disabled,
942
+ required,
929
943
  validate,
930
944
  onChange,
931
945
  onKeyDown,
@@ -949,6 +963,8 @@ class LabeledTextFieldInternal extends react__WEBPACK_IMPORTED_MODULE_0__["Compo
949
963
  id: `${uniqueId}-field`,
950
964
  "aria-describedby": ariaDescribedby ? ariaDescribedby : `${uniqueId}-error`,
951
965
  "aria-invalid": this.state.error ? "true" : "false",
966
+ "aria-required": required ? "true" : "false",
967
+ required: required,
952
968
  testId: testId && `${testId}-field`,
953
969
  type: type,
954
970
  value: value,
@@ -967,6 +983,7 @@ class LabeledTextFieldInternal extends react__WEBPACK_IMPORTED_MODULE_0__["Compo
967
983
  }),
968
984
  label: label,
969
985
  description: description,
986
+ required: !!required,
970
987
  error: !this.state.focused && this.state.error || ""
971
988
  }));
972
989
  }
@@ -1396,24 +1413,30 @@ const _generateStyles = (checked, error) => {
1396
1413
 
1397
1414
 
1398
1415
 
1399
-
1416
+ const StyledSpan = Object(_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_2__["addStyle"])("span");
1400
1417
  /**
1401
1418
  * A FieldHeading is an element that provides a label, description, and error element
1402
1419
  * to present better context and hints to any type of form field component.
1403
1420
  */
1421
+
1404
1422
  class FieldHeading extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
1405
1423
  renderLabel() {
1406
1424
  const {
1407
1425
  label,
1408
1426
  id,
1427
+ required,
1409
1428
  testId
1410
1429
  } = this.props;
1430
+ const requiredIcon = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](StyledSpan, {
1431
+ style: styles.required,
1432
+ "aria-hidden": true
1433
+ }, " ", "*");
1411
1434
  return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](react__WEBPACK_IMPORTED_MODULE_0__["Fragment"], null, typeof label === "string" ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_khanacademy_wonder_blocks_typography__WEBPACK_IMPORTED_MODULE_6__["LabelMedium"], {
1412
1435
  style: styles.label,
1413
1436
  tag: "label",
1414
1437
  htmlFor: id && `${id}-field`,
1415
1438
  testId: testId && `${testId}-label`
1416
- }, label) : label, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_khanacademy_wonder_blocks_layout__WEBPACK_IMPORTED_MODULE_4__["Strut"], {
1439
+ }, label, required && requiredIcon) : label, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_khanacademy_wonder_blocks_layout__WEBPACK_IMPORTED_MODULE_4__["Strut"], {
1417
1440
  size: _khanacademy_wonder_blocks_spacing__WEBPACK_IMPORTED_MODULE_5___default.a.xxxSmall_4
1418
1441
  }));
1419
1442
  }
@@ -1479,6 +1502,9 @@ const styles = aphrodite__WEBPACK_IMPORTED_MODULE_1__["StyleSheet"].create({
1479
1502
  },
1480
1503
  error: {
1481
1504
  color: _khanacademy_wonder_blocks_color__WEBPACK_IMPORTED_MODULE_3___default.a.red
1505
+ },
1506
+ required: {
1507
+ color: _khanacademy_wonder_blocks_color__WEBPACK_IMPORTED_MODULE_3___default.a.red
1482
1508
  }
1483
1509
  });
1484
1510
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-form",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "design": "v1",
5
5
  "description": "Form components for Wonder Blocks.",
6
6
  "main": "dist/index.js",
@@ -15,21 +15,20 @@
15
15
  "access": "public"
16
16
  },
17
17
  "dependencies": {
18
- "@babel/runtime": "^7.13.10",
19
- "@khanacademy/wonder-blocks-clickable": "^2.2.0",
20
- "@khanacademy/wonder-blocks-color": "^1.1.19",
21
- "@khanacademy/wonder-blocks-core": "^3.2.0",
22
- "@khanacademy/wonder-blocks-icon": "^1.2.23",
23
- "@khanacademy/wonder-blocks-layout": "^1.4.5",
24
- "@khanacademy/wonder-blocks-spacing": "^3.0.4",
25
- "@khanacademy/wonder-blocks-typography": "^1.1.27"
18
+ "@babel/runtime": "^7.16.3",
19
+ "@khanacademy/wonder-blocks-clickable": "^2.2.3",
20
+ "@khanacademy/wonder-blocks-color": "^1.1.20",
21
+ "@khanacademy/wonder-blocks-core": "^4.2.1",
22
+ "@khanacademy/wonder-blocks-icon": "^1.2.25",
23
+ "@khanacademy/wonder-blocks-layout": "^1.4.7",
24
+ "@khanacademy/wonder-blocks-spacing": "^3.0.5",
25
+ "@khanacademy/wonder-blocks-typography": "^1.1.29"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "aphrodite": "^1.2.5",
29
29
  "react": "16.14.0"
30
30
  },
31
31
  "devDependencies": {
32
- "wb-dev-build-settings": "^0.1.2"
33
- },
34
- "gitHead": "61090a61b6e9d2a735976d5fd53a15d06f10c853"
32
+ "wb-dev-build-settings": "^0.3.0"
33
+ }
35
34
  }
@@ -1,6 +1,7 @@
1
1
  //@flow
2
2
  import * as React from "react";
3
3
  import {mount} from "enzyme";
4
+ import "jest-enzyme";
4
5
 
5
6
  import CheckboxGroup from "../checkbox-group.js";
6
7
  import Choice from "../choice.js";
@@ -1,6 +1,7 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import {mount} from "enzyme";
4
+ import "jest-enzyme";
4
5
  import {StyleSheet} from "aphrodite";
5
6
 
6
7
  import FieldHeading from "../field-heading.js";
@@ -1,7 +1,9 @@
1
1
  //@flow
2
2
  import * as React from "react";
3
3
  import {mount} from "enzyme";
4
+ import "jest-enzyme";
4
5
  import {render, screen} from "@testing-library/react";
6
+ import userEvent from "@testing-library/user-event";
5
7
 
6
8
  import {StyleSheet} from "aphrodite";
7
9
  import LabeledTextField from "../labeled-text-field.js";
@@ -484,3 +486,141 @@ describe("LabeledTextField", () => {
484
486
  expect(textField).toHaveProp("autoComplete", autoComplete);
485
487
  });
486
488
  });
489
+
490
+ describe("Required LabeledTextField", () => {
491
+ test("has * when `required` prop is true", () => {
492
+ // Arrange
493
+
494
+ // Act
495
+ render(
496
+ <LabeledTextField
497
+ label="Label"
498
+ value=""
499
+ onChange={() => {}}
500
+ required={true}
501
+ />,
502
+ );
503
+
504
+ // Assert
505
+ expect(screen.getByText("*")).toBeInTheDocument();
506
+ });
507
+
508
+ test("does not have * when `required` prop is false", () => {
509
+ // Arrange
510
+
511
+ // Act
512
+ render(
513
+ <LabeledTextField
514
+ label="Label"
515
+ value=""
516
+ onChange={() => {}}
517
+ required={false}
518
+ />,
519
+ );
520
+
521
+ // Assert
522
+ expect(screen.queryByText("*")).not.toBeInTheDocument();
523
+ });
524
+
525
+ test("aria-required is true when `required` prop is true", () => {
526
+ // Arrange
527
+
528
+ // Act
529
+ render(
530
+ <LabeledTextField
531
+ label="Label"
532
+ value=""
533
+ onChange={() => {}}
534
+ testId="foo-labeled-text-field"
535
+ required={true}
536
+ />,
537
+ );
538
+
539
+ const textField = screen.getByTestId("foo-labeled-text-field-field");
540
+
541
+ // Assert
542
+ expect(textField).toHaveAttribute("aria-required", "true");
543
+ });
544
+
545
+ test("aria-required is false when `required` prop is false", () => {
546
+ // Arrange
547
+
548
+ // Act
549
+ render(
550
+ <LabeledTextField
551
+ label="Label"
552
+ value=""
553
+ onChange={() => {}}
554
+ testId="foo-labeled-text-field"
555
+ required={false}
556
+ />,
557
+ );
558
+
559
+ const textField = screen.getByTestId("foo-labeled-text-field-field");
560
+
561
+ // Assert
562
+ expect(textField).toHaveAttribute("aria-required", "false");
563
+ });
564
+
565
+ test("displays the default message when the `required` prop is `true`", () => {
566
+ // Arrange
567
+ const TextFieldWrapper = () => {
568
+ const [value, setValue] = React.useState("");
569
+ return (
570
+ <LabeledTextField
571
+ label="Label"
572
+ value={value}
573
+ onChange={setValue}
574
+ required={true}
575
+ testId="test-labeled-text-field"
576
+ />
577
+ );
578
+ };
579
+
580
+ render(<TextFieldWrapper />);
581
+
582
+ const textField = screen.getByTestId("test-labeled-text-field-field");
583
+ textField.focus();
584
+ userEvent.paste(textField, "a");
585
+ userEvent.clear(textField);
586
+
587
+ // Act
588
+ textField.blur();
589
+
590
+ // Assert
591
+ expect(screen.getByRole("alert")).toHaveTextContent(
592
+ "This field is required.",
593
+ );
594
+ });
595
+
596
+ test("displays the string passed into `required`", () => {
597
+ // Arrange
598
+ const errorMessage = "This is an example error message.";
599
+
600
+ const TextFieldWrapper = () => {
601
+ const [value, setValue] = React.useState("");
602
+ return (
603
+ <LabeledTextField
604
+ label="Label"
605
+ value={value}
606
+ onChange={setValue}
607
+ required={errorMessage}
608
+ testId="test-labeled-text-field"
609
+ />
610
+ );
611
+ };
612
+
613
+ render(<TextFieldWrapper />);
614
+
615
+ const textField = screen.getByTestId("test-labeled-text-field-field");
616
+ textField.focus();
617
+ userEvent.paste(textField, "a");
618
+ userEvent.clear(textField);
619
+
620
+ // Act
621
+ textField.blur();
622
+
623
+ // Assert
624
+ expect(screen.getByRole("alert")).toHaveTextContent(errorMessage);
625
+ });
626
+ });
@@ -1,6 +1,7 @@
1
1
  //@flow
2
2
  import * as React from "react";
3
3
  import {mount} from "enzyme";
4
+ import "jest-enzyme";
4
5
 
5
6
  import RadioGroup from "../radio-group.js";
6
7
  import Choice from "../choice.js";
@@ -1,6 +1,7 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import {mount} from "enzyme";
4
+ import "jest-enzyme";
4
5
 
5
6
  import TextField from "../text-field.js";
6
7
 
@@ -329,24 +330,6 @@ describe("TextField", () => {
329
330
  );
330
331
  });
331
332
 
332
- it("required prop is passed to the input element", () => {
333
- // Arrange
334
- const wrapper = mount(
335
- <TextField
336
- id={"tf-1"}
337
- value="Text"
338
- onChange={() => {}}
339
- required={true}
340
- />,
341
- );
342
-
343
- // Act
344
-
345
- // Assert
346
- const input = wrapper.find("input");
347
- expect(input).toContainMatchingElement("[required=true]");
348
- });
349
-
350
333
  it("testId is passed to the input element", () => {
351
334
  // Arrange
352
335
  const testId = "some-test-id";
@@ -24,8 +24,7 @@ const {blue, red, white, offWhite, offBlack16, offBlack32, offBlack50} = Color;
24
24
  const StyledInput = addStyle("input");
25
25
 
26
26
  const checkboxCheck: IconAsset = {
27
- small:
28
- "M11.263 4.324a1 1 0 1 1 1.474 1.352l-5.5 6a1 1 0 0 1-1.505-.036l-2.5-3a1 1 0 1 1 1.536-1.28L6.536 9.48l4.727-5.157z",
27
+ small: "M11.263 4.324a1 1 0 1 1 1.474 1.352l-5.5 6a1 1 0 0 1-1.505-.036l-2.5-3a1 1 0 1 1 1.536-1.28L6.536 9.48l4.727-5.157z",
29
28
  };
30
29
 
31
30
  /**
@@ -2,7 +2,7 @@
2
2
  import * as React from "react";
3
3
  import {StyleSheet} from "aphrodite";
4
4
 
5
- import {View, type StyleType} from "@khanacademy/wonder-blocks-core";
5
+ import {View, addStyle, type StyleType} from "@khanacademy/wonder-blocks-core";
6
6
  import Color from "@khanacademy/wonder-blocks-color";
7
7
  import {Strut} from "@khanacademy/wonder-blocks-layout";
8
8
  import Spacing from "@khanacademy/wonder-blocks-spacing";
@@ -28,6 +28,11 @@ type Props = {|
28
28
  */
29
29
  description?: string | React.Element<Typography>,
30
30
 
31
+ /**
32
+ * Whether this field is required to continue.
33
+ */
34
+ required?: boolean,
35
+
31
36
  /**
32
37
  * The message for the error element.
33
38
  */
@@ -52,13 +57,22 @@ type Props = {|
52
57
  testId?: string,
53
58
  |};
54
59
 
60
+ const StyledSpan = addStyle("span");
61
+
55
62
  /**
56
63
  * A FieldHeading is an element that provides a label, description, and error element
57
64
  * to present better context and hints to any type of form field component.
58
65
  */
59
66
  export default class FieldHeading extends React.Component<Props> {
60
67
  renderLabel(): React.Node {
61
- const {label, id, testId} = this.props;
68
+ const {label, id, required, testId} = this.props;
69
+
70
+ const requiredIcon = (
71
+ <StyledSpan style={styles.required} aria-hidden={true}>
72
+ {" "}
73
+ *
74
+ </StyledSpan>
75
+ );
62
76
 
63
77
  return (
64
78
  <React.Fragment>
@@ -70,6 +84,7 @@ export default class FieldHeading extends React.Component<Props> {
70
84
  testId={testId && `${testId}-label`}
71
85
  >
72
86
  {label}
87
+ {required && requiredIcon}
73
88
  </LabelMedium>
74
89
  ) : (
75
90
  label
@@ -154,4 +169,7 @@ const styles = StyleSheet.create({
154
169
  error: {
155
170
  color: Color.red,
156
171
  },
172
+ required: {
173
+ color: Color.red,
174
+ },
157
175
  });
@@ -41,6 +41,30 @@ type Props = {|
41
41
  */
42
42
  disabled: boolean,
43
43
 
44
+ /**
45
+ * Whether this field is required to to continue, or the error message to
46
+ * render if this field is left blank.
47
+ *
48
+ * This can be a boolean or a string.
49
+ *
50
+ * String:
51
+ * Please pass in a translated string to use as the error message that will
52
+ * render if the user leaves this field blank. If this field is required,
53
+ * and a string is not passed in, a default untranslated string will render
54
+ * upon error.
55
+ * Note: The string will not be used if a `validate` prop is passed in.
56
+ *
57
+ * Example message: i18n._("A password is required to log in.")
58
+ *
59
+ * Boolean:
60
+ * True/false indicating whether this field is required. Please do not pass
61
+ * in `true` if possible - pass in the error string instead.
62
+ * If `true` is passed, and a `validate` prop is not passed, that means
63
+ * there is no corresponding message and the default untranlsated message
64
+ * will be used.
65
+ */
66
+ required?: boolean | string,
67
+
44
68
  /**
45
69
  * Identifies the element or elements that describes this text field.
46
70
  */
@@ -202,6 +226,7 @@ class LabeledTextFieldInternal extends React.Component<
202
226
  description,
203
227
  value,
204
228
  disabled,
229
+ required,
205
230
  validate,
206
231
  onChange,
207
232
  onKeyDown,
@@ -233,6 +258,8 @@ class LabeledTextFieldInternal extends React.Component<
233
258
  aria-invalid={
234
259
  this.state.error ? "true" : "false"
235
260
  }
261
+ aria-required={required ? "true" : "false"}
262
+ required={required}
236
263
  testId={testId && `${testId}-field`}
237
264
  type={type}
238
265
  value={value}
@@ -252,6 +279,7 @@ class LabeledTextFieldInternal extends React.Component<
252
279
  }
253
280
  label={label}
254
281
  description={description}
282
+ required={!!required}
255
283
  error={(!this.state.focused && this.state.error) || ""}
256
284
  />
257
285
  )}
@@ -265,11 +293,9 @@ type ExportProps = $Diff<
265
293
  WithForwardRef,
266
294
  >;
267
295
 
268
- const LabeledTextField: React.AbstractComponent<
269
- ExportProps,
270
- HTMLInputElement,
271
- > = React.forwardRef<ExportProps, HTMLInputElement>((props, ref) => (
272
- <LabeledTextFieldInternal {...props} forwardedRef={ref} />
273
- ));
296
+ const LabeledTextField: React.AbstractComponent<ExportProps, HTMLInputElement> =
297
+ React.forwardRef<ExportProps, HTMLInputElement>((props, ref) => (
298
+ <LabeledTextFieldInternal {...props} forwardedRef={ref} />
299
+ ));
274
300
 
275
301
  export default LabeledTextField;