@khanacademy/wonder-blocks-form 2.2.1

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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/dist/es/index.js +1100 -0
  3. package/dist/index.js +1419 -0
  4. package/dist/index.js.flow +2 -0
  5. package/docs.md +1 -0
  6. package/package.json +35 -0
  7. package/src/__tests__/__snapshots__/custom-snapshot.test.js.snap +1349 -0
  8. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +6126 -0
  9. package/src/__tests__/custom-snapshot.test.js +66 -0
  10. package/src/__tests__/generated-snapshot.test.js +654 -0
  11. package/src/components/__tests__/checkbox-group.test.js +84 -0
  12. package/src/components/__tests__/field-heading.test.js +182 -0
  13. package/src/components/__tests__/labeled-text-field.test.js +442 -0
  14. package/src/components/__tests__/radio-group.test.js +84 -0
  15. package/src/components/__tests__/text-field.test.js +424 -0
  16. package/src/components/checkbox-core.js +201 -0
  17. package/src/components/checkbox-group.js +161 -0
  18. package/src/components/checkbox-group.md +200 -0
  19. package/src/components/checkbox.js +94 -0
  20. package/src/components/checkbox.md +134 -0
  21. package/src/components/choice-internal.js +206 -0
  22. package/src/components/choice.js +104 -0
  23. package/src/components/field-heading.js +157 -0
  24. package/src/components/field-heading.md +43 -0
  25. package/src/components/group-styles.js +35 -0
  26. package/src/components/labeled-text-field.js +265 -0
  27. package/src/components/labeled-text-field.md +535 -0
  28. package/src/components/labeled-text-field.stories.js +359 -0
  29. package/src/components/radio-core.js +176 -0
  30. package/src/components/radio-group.js +142 -0
  31. package/src/components/radio-group.md +129 -0
  32. package/src/components/radio.js +93 -0
  33. package/src/components/radio.md +26 -0
  34. package/src/components/text-field.js +326 -0
  35. package/src/components/text-field.md +770 -0
  36. package/src/components/text-field.stories.js +513 -0
  37. package/src/index.js +18 -0
  38. package/src/util/types.js +77 -0
@@ -0,0 +1,1100 @@
1
+ import _extends from '@babel/runtime/helpers/extends';
2
+ import { Component, createElement, Fragment, Children, cloneElement, forwardRef } from 'react';
3
+ import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutPropertiesLoose';
4
+ import { StyleSheet, css } from 'aphrodite';
5
+ import Color, { mix, fade } from '@khanacademy/wonder-blocks-color';
6
+ import { addStyle, UniqueIDProvider, View, IDProvider } from '@khanacademy/wonder-blocks-core';
7
+ import { getClickableBehavior } from '@khanacademy/wonder-blocks-clickable';
8
+ import { Strut } from '@khanacademy/wonder-blocks-layout';
9
+ import Spacing from '@khanacademy/wonder-blocks-spacing';
10
+ import { LabelMedium, LabelSmall, styles as styles$6 } from '@khanacademy/wonder-blocks-typography';
11
+ import Icon from '@khanacademy/wonder-blocks-icon';
12
+
13
+ const _excluded = ["checked", "disabled", "error", "groupName", "id", "testId", "hovered", "focused", "pressed", "waiting"];
14
+ const {
15
+ blue,
16
+ red,
17
+ white,
18
+ offWhite,
19
+ offBlack16,
20
+ offBlack32,
21
+ offBlack50
22
+ } = Color;
23
+ const StyledInput = addStyle("input");
24
+ const checkboxCheck = {
25
+ 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"
26
+ };
27
+ /**
28
+ * The internal stateless ☑️ Checkbox
29
+ */
30
+
31
+ class CheckboxCore extends Component {
32
+ constructor(...args) {
33
+ super(...args);
34
+
35
+ this.handleChange = () => {
36
+ // Empty because change is handled by ClickableBehavior
37
+ return;
38
+ };
39
+ }
40
+
41
+ render() {
42
+ const _this$props = this.props,
43
+ {
44
+ checked,
45
+ disabled,
46
+ error,
47
+ groupName,
48
+ id,
49
+ testId,
50
+ hovered,
51
+ focused,
52
+ pressed
53
+ } = _this$props,
54
+ sharedProps = _objectWithoutPropertiesLoose(_this$props, _excluded);
55
+
56
+ const stateStyles = _generateStyles(checked, error);
57
+
58
+ const defaultStyle = [sharedStyles.inputReset, sharedStyles.default, stateStyles.default, !disabled && (pressed ? stateStyles.active : (hovered || focused) && stateStyles.focus), disabled && sharedStyles.disabled];
59
+ const props = {
60
+ "data-test-id": testId
61
+ };
62
+ return /*#__PURE__*/createElement(Fragment, null, /*#__PURE__*/createElement(StyledInput, _extends({}, sharedProps, {
63
+ type: "checkbox",
64
+ "aria-invalid": error,
65
+ checked: checked,
66
+ disabled: disabled,
67
+ id: id,
68
+ name: groupName // Need to specify because this is a controlled React form
69
+ // component, but we handle the click via ClickableBehavior
70
+ ,
71
+ onChange: this.handleChange,
72
+ style: defaultStyle
73
+ }, props)), checked && /*#__PURE__*/createElement(Icon, {
74
+ color: disabled ? offBlack32 : white,
75
+ icon: checkboxCheck,
76
+ size: "small",
77
+ style: sharedStyles.checkIcon
78
+ }));
79
+ }
80
+
81
+ }
82
+ const size = 16;
83
+ const sharedStyles = StyleSheet.create({
84
+ // Reset the default styled input element
85
+ inputReset: {
86
+ appearance: "none",
87
+ WebkitAppearance: "none",
88
+ MozAppearance: "none"
89
+ },
90
+ default: {
91
+ height: size,
92
+ width: size,
93
+ minHeight: size,
94
+ minWidth: size,
95
+ margin: 0,
96
+ outline: "none",
97
+ boxSizing: "border-box",
98
+ borderStyle: "solid",
99
+ borderWidth: 1,
100
+ borderRadius: 3
101
+ },
102
+ disabled: {
103
+ cursor: "auto",
104
+ backgroundColor: offWhite,
105
+ borderColor: offBlack16,
106
+ borderWidth: 1
107
+ },
108
+ checkIcon: {
109
+ position: "absolute",
110
+ pointerEvents: "none"
111
+ }
112
+ });
113
+ const fadedBlue = mix(fade(blue, 0.16), white);
114
+ const activeBlue = mix(offBlack32, blue);
115
+ const fadedRed = mix(fade(red, 0.08), white);
116
+ const activeRed = mix(offBlack32, red);
117
+ const colors = {
118
+ default: {
119
+ faded: fadedBlue,
120
+ base: blue,
121
+ active: activeBlue
122
+ },
123
+ error: {
124
+ faded: fadedRed,
125
+ base: red,
126
+ active: activeRed
127
+ }
128
+ };
129
+ const styles = {};
130
+
131
+ const _generateStyles = (checked, error) => {
132
+ // "hash" the parameters
133
+ const styleKey = `${String(checked)}-${String(error)}`;
134
+
135
+ if (styles[styleKey]) {
136
+ return styles[styleKey];
137
+ }
138
+
139
+ const palette = error ? colors.error : colors.default;
140
+ let newStyles = {};
141
+
142
+ if (checked) {
143
+ newStyles = {
144
+ default: {
145
+ backgroundColor: palette.base,
146
+ borderWidth: 0
147
+ },
148
+ focus: {
149
+ boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`
150
+ },
151
+ active: {
152
+ boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.active}`,
153
+ background: palette.active
154
+ }
155
+ };
156
+ } else {
157
+ newStyles = {
158
+ default: {
159
+ backgroundColor: error ? fadedRed : white,
160
+ borderColor: error ? red : offBlack50
161
+ },
162
+ focus: {
163
+ backgroundColor: error ? fadedRed : white,
164
+ borderColor: palette.base,
165
+ borderWidth: 2
166
+ },
167
+ active: {
168
+ backgroundColor: palette.faded,
169
+ borderColor: error ? activeRed : blue,
170
+ borderWidth: 2
171
+ }
172
+ };
173
+ }
174
+
175
+ styles[styleKey] = StyleSheet.create(newStyles);
176
+ return styles[styleKey];
177
+ };
178
+
179
+ const _excluded$1 = ["checked", "disabled", "error", "groupName", "id", "testId", "hovered", "focused", "pressed", "waiting"];
180
+ const {
181
+ blue: blue$1,
182
+ red: red$1,
183
+ white: white$1,
184
+ offWhite: offWhite$1,
185
+ offBlack16: offBlack16$1,
186
+ offBlack32: offBlack32$1,
187
+ offBlack50: offBlack50$1
188
+ } = Color;
189
+ const StyledInput$1 = addStyle("input");
190
+ /**
191
+ * The internal stateless 🔘 Radio button
192
+ */
193
+
194
+ class RadioCore extends Component {
195
+ constructor(...args) {
196
+ super(...args);
197
+
198
+ this.handleChange = () => {
199
+ // Empty because change is handled by ClickableBehavior
200
+ return;
201
+ };
202
+ }
203
+
204
+ render() {
205
+ const _this$props = this.props,
206
+ {
207
+ checked,
208
+ disabled,
209
+ error,
210
+ groupName,
211
+ id,
212
+ testId,
213
+ hovered,
214
+ focused,
215
+ pressed
216
+ } = _this$props,
217
+ sharedProps = _objectWithoutPropertiesLoose(_this$props, _excluded$1);
218
+
219
+ const stateStyles = _generateStyles$1(checked, error);
220
+
221
+ const defaultStyle = [sharedStyles$1.inputReset, sharedStyles$1.default, stateStyles.default, !disabled && (pressed ? stateStyles.active : (hovered || focused) && stateStyles.focus), disabled && sharedStyles$1.disabled];
222
+ const props = {
223
+ "data-test-id": testId
224
+ };
225
+ return /*#__PURE__*/createElement(Fragment, null, /*#__PURE__*/createElement(StyledInput$1, _extends({}, sharedProps, {
226
+ type: "radio",
227
+ "aria-invalid": error,
228
+ checked: checked,
229
+ disabled: disabled,
230
+ id: id,
231
+ name: groupName // Need to specify because this is a controlled React form
232
+ // component, but we handle the click via ClickableBehavior
233
+ ,
234
+ onChange: this.handleChange,
235
+ style: defaultStyle
236
+ }, props)), disabled && checked && /*#__PURE__*/createElement("span", {
237
+ style: disabledChecked
238
+ }));
239
+ }
240
+
241
+ }
242
+ const size$1 = 16; // circle with a different color. Here, we add that center circle. // If the checkbox is disabled and selected, it has a border but also an inner
243
+
244
+ const disabledChecked = {
245
+ position: "absolute",
246
+ top: size$1 / 4,
247
+ left: size$1 / 4,
248
+ height: size$1 / 2,
249
+ width: size$1 / 2,
250
+ borderRadius: "50%",
251
+ backgroundColor: offBlack32$1
252
+ };
253
+ const sharedStyles$1 = StyleSheet.create({
254
+ // Reset the default styled input element
255
+ inputReset: {
256
+ appearance: "none",
257
+ WebkitAppearance: "none",
258
+ MozAppearance: "none"
259
+ },
260
+ default: {
261
+ height: size$1,
262
+ width: size$1,
263
+ minHeight: size$1,
264
+ minWidth: size$1,
265
+ margin: 0,
266
+ outline: "none",
267
+ boxSizing: "border-box",
268
+ borderStyle: "solid",
269
+ borderWidth: 1,
270
+ borderRadius: "50%"
271
+ },
272
+ disabled: {
273
+ cursor: "auto",
274
+ backgroundColor: offWhite$1,
275
+ borderColor: offBlack16$1,
276
+ borderWidth: 1
277
+ }
278
+ });
279
+ const fadedBlue$1 = mix(fade(blue$1, 0.16), white$1);
280
+ const activeBlue$1 = mix(offBlack32$1, blue$1);
281
+ const fadedRed$1 = mix(fade(red$1, 0.08), white$1);
282
+ const activeRed$1 = mix(offBlack32$1, red$1);
283
+ const colors$1 = {
284
+ default: {
285
+ faded: fadedBlue$1,
286
+ base: blue$1,
287
+ active: activeBlue$1
288
+ },
289
+ error: {
290
+ faded: fadedRed$1,
291
+ base: red$1,
292
+ active: activeRed$1
293
+ }
294
+ };
295
+ const styles$1 = {};
296
+
297
+ const _generateStyles$1 = (checked, error) => {
298
+ // "hash" the parameters
299
+ const styleKey = `${String(checked)}-${String(error)}`;
300
+
301
+ if (styles$1[styleKey]) {
302
+ return styles$1[styleKey];
303
+ }
304
+
305
+ const palette = error ? colors$1.error : colors$1.default;
306
+ let newStyles = {};
307
+
308
+ if (checked) {
309
+ newStyles = {
310
+ default: {
311
+ backgroundColor: white$1,
312
+ borderColor: palette.base,
313
+ borderWidth: size$1 / 4
314
+ },
315
+ focus: {
316
+ boxShadow: `0 0 0 1px ${white$1}, 0 0 0 3px ${palette.base}`
317
+ },
318
+ active: {
319
+ boxShadow: `0 0 0 1px ${white$1}, 0 0 0 3px ${palette.active}`,
320
+ borderColor: palette.active
321
+ }
322
+ };
323
+ } else {
324
+ newStyles = {
325
+ default: {
326
+ backgroundColor: error ? fadedRed$1 : white$1,
327
+ borderColor: error ? red$1 : offBlack50$1
328
+ },
329
+ focus: {
330
+ backgroundColor: error ? fadedRed$1 : white$1,
331
+ borderColor: palette.base,
332
+ borderWidth: 2
333
+ },
334
+ active: {
335
+ backgroundColor: palette.faded,
336
+ borderColor: error ? activeRed$1 : blue$1,
337
+ borderWidth: 2
338
+ }
339
+ };
340
+ }
341
+
342
+ styles$1[styleKey] = StyleSheet.create(newStyles);
343
+ return styles$1[styleKey];
344
+ };
345
+
346
+ const _excluded$2 = ["label", "description", "onChange", "style", "className", "variant"];
347
+
348
+ /**
349
+ * This is a potentially labeled 🔘 or ☑️ item. This is an internal component
350
+ * that's wrapped by Checkbox and Radio. Choice is a wrapper for Checkbox and
351
+ * Radio with many of its props auto-populated, to be used with CheckboxGroup
352
+ * and RadioGroup. This design allows for more explicit prop typing. For
353
+ * example, we can make onChange a required prop on Checkbox but not on Choice
354
+ * (because for Choice, that prop would be auto-populated by CheckboxGroup).
355
+ */
356
+ class ChoiceInternal extends Component {
357
+ constructor(...args) {
358
+ super(...args);
359
+
360
+ this.handleLabelClick = event => {
361
+ // Browsers automatically use the for attribute to select the input,
362
+ // but we use ClickableBehavior to handle this.
363
+ event.preventDefault();
364
+ };
365
+
366
+ this.handleClick = () => {
367
+ const {
368
+ checked,
369
+ onChange,
370
+ variant
371
+ } = this.props; // Radio buttons cannot be unchecked
372
+
373
+ if (variant === "radio" && checked) {
374
+ return;
375
+ }
376
+
377
+ onChange(!checked);
378
+ };
379
+ }
380
+
381
+ getChoiceCoreComponent() {
382
+ if (this.props.variant === "radio") {
383
+ return RadioCore;
384
+ } else {
385
+ return CheckboxCore;
386
+ }
387
+ }
388
+
389
+ getLabel() {
390
+ const {
391
+ disabled,
392
+ id,
393
+ label
394
+ } = this.props;
395
+ return /*#__PURE__*/createElement(LabelMedium, {
396
+ style: [styles$2.label, disabled && styles$2.disabledLabel]
397
+ }, /*#__PURE__*/createElement("label", {
398
+ htmlFor: id,
399
+ onClick: this.handleLabelClick
400
+ }, label));
401
+ }
402
+
403
+ getDescription(id) {
404
+ const {
405
+ description
406
+ } = this.props;
407
+ return /*#__PURE__*/createElement(LabelSmall, {
408
+ style: styles$2.description,
409
+ id: id
410
+ }, description);
411
+ }
412
+
413
+ render() {
414
+ const _this$props = this.props,
415
+ {
416
+ label,
417
+ description,
418
+ style,
419
+ className,
420
+ variant
421
+ } = _this$props,
422
+ coreProps = _objectWithoutPropertiesLoose(_this$props, _excluded$2);
423
+
424
+ const ChoiceCore = this.getChoiceCoreComponent();
425
+ const ClickableBehavior = getClickableBehavior();
426
+ return /*#__PURE__*/createElement(UniqueIDProvider, {
427
+ mockOnFirstRender: true,
428
+ scope: "choice"
429
+ }, ids => {
430
+ const descriptionId = description && ids.get("description");
431
+ return /*#__PURE__*/createElement(View, {
432
+ style: style,
433
+ className: className
434
+ }, /*#__PURE__*/createElement(ClickableBehavior, {
435
+ disabled: coreProps.disabled,
436
+ onClick: this.handleClick,
437
+ role: variant
438
+ }, (state, childrenProps) => {
439
+ return /*#__PURE__*/createElement(View, _extends({
440
+ style: styles$2.wrapper
441
+ }, childrenProps, {
442
+ // We are resetting the tabIndex=0 from handlers
443
+ // because the ChoiceCore component will receive
444
+ // focus on basis of it being an input element.
445
+ tabIndex: -1
446
+ }), /*#__PURE__*/createElement(ChoiceCore, _extends({}, coreProps, state, {
447
+ "aria-describedby": descriptionId
448
+ })), /*#__PURE__*/createElement(Strut, {
449
+ size: Spacing.xSmall_8
450
+ }), label && this.getLabel());
451
+ }), description && this.getDescription(descriptionId));
452
+ });
453
+ }
454
+
455
+ }
456
+ ChoiceInternal.defaultProps = {
457
+ checked: false,
458
+ disabled: false,
459
+ error: false
460
+ };
461
+ const styles$2 = StyleSheet.create({
462
+ wrapper: {
463
+ flexDirection: "row",
464
+ alignItems: "flex-start",
465
+ outline: "none"
466
+ },
467
+ label: {
468
+ userSelect: "none",
469
+ // NOTE: The checkbox/radio button (height 16px) should be center
470
+ // aligned with the first line of the label. However, LabelMedium has a
471
+ // declared line height of 20px, so we need to adjust the top to get the
472
+ // desired alignment.
473
+ marginTop: -2
474
+ },
475
+ disabledLabel: {
476
+ color: Color.offBlack32
477
+ },
478
+ description: {
479
+ // 16 for icon + 8 for spacing strut
480
+ marginLeft: Spacing.medium_16 + Spacing.xSmall_8,
481
+ marginTop: Spacing.xxxSmall_4,
482
+ color: Color.offBlack64
483
+ }
484
+ });
485
+
486
+ /**
487
+ * ☑️ A nicely styled checkbox for all your checking needs. Can optionally take
488
+ * label and description props.
489
+ *
490
+ * If you want a whole group of Checkbox[es] that are related, see the Choice
491
+ * and CheckboxGroup components.
492
+ */
493
+ class Checkbox extends Component {
494
+ render() {
495
+ return /*#__PURE__*/createElement(ChoiceInternal, _extends({
496
+ variant: "checkbox"
497
+ }, this.props));
498
+ }
499
+
500
+ }
501
+ Checkbox.defaultProps = {
502
+ disabled: false,
503
+ error: false
504
+ };
505
+
506
+ /**
507
+ * 🔘 A nicely styled radio button for all your non-AMFM radio button needs. Can
508
+ * optionally take label and description props.
509
+ *
510
+ * This component should not really be used by itself because radio buttons are
511
+ * often grouped together. See RadioGroup.
512
+ */
513
+ class Radio extends Component {
514
+ render() {
515
+ return /*#__PURE__*/createElement(ChoiceInternal, _extends({
516
+ variant: "radio"
517
+ }, this.props));
518
+ }
519
+
520
+ }
521
+ Radio.defaultProps = {
522
+ disabled: false,
523
+ error: false
524
+ };
525
+
526
+ const _excluded$3 = ["value", "variant"];
527
+
528
+ /**
529
+ * This is a labeled 🔘 or ☑️ item. Choice is meant to be used as children of
530
+ * CheckboxGroup and RadioGroup because many of its props are auto-populated
531
+ * and not shown in the documentation here. See those components for usage
532
+ * examples.
533
+ *
534
+ * If you wish to use just a single field, use Checkbox or Radio with the
535
+ * optional label and description props.
536
+ */
537
+ class Choice extends Component {
538
+ getChoiceComponent(variant) {
539
+ if (variant === "checkbox") {
540
+ return Checkbox;
541
+ } else {
542
+ return Radio;
543
+ }
544
+ }
545
+
546
+ render() {
547
+ // we don't need this going into the ChoiceComponent
548
+ // eslint-disable-next-line no-unused-vars
549
+ const _this$props = this.props,
550
+ {
551
+ variant
552
+ } = _this$props,
553
+ remainingProps = _objectWithoutPropertiesLoose(_this$props, _excluded$3);
554
+
555
+ const ChoiceComponent = this.getChoiceComponent(variant);
556
+ return /*#__PURE__*/createElement(ChoiceComponent, remainingProps);
557
+ }
558
+
559
+ }
560
+ Choice.defaultProps = {
561
+ checked: false,
562
+ disabled: false,
563
+ onChange: () => {}
564
+ };
565
+
566
+ const styles$3 = StyleSheet.create({
567
+ fieldset: {
568
+ border: "none",
569
+ padding: 0,
570
+ margin: 0
571
+ },
572
+ legend: {
573
+ padding: 0
574
+ },
575
+ description: {
576
+ marginTop: Spacing.xxxSmall_4,
577
+ color: Color.offBlack64
578
+ },
579
+ error: {
580
+ marginTop: Spacing.xxxSmall_4,
581
+ color: Color.red
582
+ },
583
+ defaultLineGap: {
584
+ marginTop: Spacing.xSmall_8
585
+ }
586
+ });
587
+
588
+ const StyledFieldset = addStyle("fieldset");
589
+ const StyledLegend = addStyle("legend");
590
+ /**
591
+ * A checkbox group allows multiple selection. This component auto-populates
592
+ * many props for its children Choice components. The Choice component is
593
+ * exposed for the user to apply custom styles or to indicate which choices are
594
+ * disabled.
595
+ */
596
+
597
+ class CheckboxGroup extends Component {
598
+ handleChange(changedValue, originalCheckedState) {
599
+ const {
600
+ onChange,
601
+ selectedValues
602
+ } = this.props;
603
+
604
+ if (originalCheckedState) {
605
+ const index = selectedValues.indexOf(changedValue);
606
+ const updatedSelection = [].concat(selectedValues.slice(0, index), selectedValues.slice(index + 1));
607
+ onChange(updatedSelection);
608
+ } else {
609
+ onChange([].concat(selectedValues, [changedValue]));
610
+ }
611
+ }
612
+
613
+ render() {
614
+ const {
615
+ children,
616
+ label,
617
+ description,
618
+ errorMessage,
619
+ groupName,
620
+ selectedValues,
621
+ style,
622
+ testId
623
+ } = this.props;
624
+ return /*#__PURE__*/createElement(StyledFieldset, {
625
+ "data-test-id": testId,
626
+ style: styles$3.fieldset
627
+ }, /*#__PURE__*/createElement(View, {
628
+ style: style
629
+ }, typeof label === "string" ? /*#__PURE__*/createElement(StyledLegend, {
630
+ style: styles$3.legend
631
+ }, /*#__PURE__*/createElement(LabelMedium, null, label)) : label && label, typeof description === "string" ? /*#__PURE__*/createElement(LabelSmall, {
632
+ style: styles$3.description
633
+ }, description) : description && description, errorMessage && /*#__PURE__*/createElement(LabelSmall, {
634
+ style: styles$3.error
635
+ }, errorMessage), (label || description || errorMessage) && /*#__PURE__*/createElement(Strut, {
636
+ size: Spacing.small_12
637
+ }), Children.map(children, (child, index) => {
638
+ const {
639
+ style,
640
+ value
641
+ } = child.props;
642
+ const checked = selectedValues.includes(value);
643
+ return /*#__PURE__*/createElement(Fragment, null, /*#__PURE__*/cloneElement(child, {
644
+ checked: checked,
645
+ error: !!errorMessage,
646
+ groupName: groupName,
647
+ id: `${groupName}-${value}`,
648
+ key: value,
649
+ onChange: () => this.handleChange(value, checked),
650
+ style: [index > 0 && styles$3.defaultLineGap, style],
651
+ variant: "checkbox"
652
+ }));
653
+ })));
654
+ }
655
+
656
+ }
657
+
658
+ const StyledFieldset$1 = addStyle("fieldset");
659
+ const StyledLegend$1 = addStyle("legend");
660
+ /**
661
+ * A radio group allows only single selection. Like CheckboxGroup, this
662
+ * component auto-populates many props for its children Choice components. The
663
+ * Choice component is exposed for the user to apply custom styles or to
664
+ * indicate which choices are disabled. The use of the groupName prop is
665
+ * important to maintain expected keyboard navigation behavior for
666
+ * accessibility.
667
+ */
668
+
669
+ class RadioGroup extends Component {
670
+ handleChange(changedValue) {
671
+ this.props.onChange(changedValue);
672
+ }
673
+
674
+ render() {
675
+ const {
676
+ children,
677
+ label,
678
+ description,
679
+ errorMessage,
680
+ groupName,
681
+ selectedValue,
682
+ style,
683
+ testId
684
+ } = this.props;
685
+ return /*#__PURE__*/createElement(StyledFieldset$1, {
686
+ "data-test-id": testId,
687
+ style: styles$3.fieldset
688
+ }, /*#__PURE__*/createElement(View, {
689
+ style: style
690
+ }, label && /*#__PURE__*/createElement(StyledLegend$1, {
691
+ style: styles$3.legend
692
+ }, /*#__PURE__*/createElement(LabelMedium, null, label)), description && /*#__PURE__*/createElement(LabelSmall, {
693
+ style: styles$3.description
694
+ }, description), errorMessage && /*#__PURE__*/createElement(LabelSmall, {
695
+ style: styles$3.error
696
+ }, errorMessage), (label || description || errorMessage) && /*#__PURE__*/createElement(Strut, {
697
+ size: Spacing.small_12
698
+ }), Children.map(children, (child, index) => {
699
+ const {
700
+ style,
701
+ value
702
+ } = child.props;
703
+ const checked = selectedValue === value;
704
+ return /*#__PURE__*/createElement(Fragment, null, /*#__PURE__*/cloneElement(child, {
705
+ checked: checked,
706
+ error: !!errorMessage,
707
+ groupName: groupName,
708
+ id: `${groupName}-${value}`,
709
+ key: value,
710
+ onChange: () => this.handleChange(value),
711
+ style: [index > 0 && styles$3.defaultLineGap, style],
712
+ variant: "radio"
713
+ }));
714
+ })));
715
+ }
716
+
717
+ }
718
+
719
+ const _excluded$4 = ["id", "type", "value", "disabled", "onKeyDown", "placeholder", "required", "light", "style", "testId", "readOnly", "autoComplete", "forwardedRef", "onFocus", "onBlur", "onValidate", "validate", "onChange"];
720
+
721
+ // TODO(WB-1081): Change class name back to TextField after Styleguidist is gone.
722
+
723
+ /**
724
+ * A TextField is an element used to accept a single line of text from the user.
725
+ */
726
+ class TextFieldInternal extends Component {
727
+ constructor(props) {
728
+ super(props);
729
+ this.state = {
730
+ error: null,
731
+ focused: false
732
+ };
733
+
734
+ this.maybeValidate = newValue => {
735
+ const {
736
+ validate,
737
+ onValidate
738
+ } = this.props;
739
+
740
+ if (validate) {
741
+ const maybeError = validate(newValue) || null;
742
+ this.setState({
743
+ error: maybeError
744
+ }, () => {
745
+ if (onValidate) {
746
+ onValidate(maybeError);
747
+ }
748
+ });
749
+ }
750
+ };
751
+
752
+ this.handleChange = event => {
753
+ const {
754
+ onChange
755
+ } = this.props;
756
+ const newValue = event.target.value;
757
+ this.maybeValidate(newValue);
758
+ onChange(newValue);
759
+ };
760
+
761
+ this.handleFocus = event => {
762
+ const {
763
+ onFocus
764
+ } = this.props;
765
+ this.setState({
766
+ focused: true
767
+ }, () => {
768
+ if (onFocus) {
769
+ onFocus(event);
770
+ }
771
+ });
772
+ };
773
+
774
+ this.handleBlur = event => {
775
+ const {
776
+ onBlur
777
+ } = this.props;
778
+ this.setState({
779
+ focused: false
780
+ }, () => {
781
+ if (onBlur) {
782
+ onBlur(event);
783
+ }
784
+ });
785
+ };
786
+
787
+ if (props.validate) {
788
+ // Ensures error is updated on unmounted server-side renders
789
+ this.state.error = props.validate(props.value) || null;
790
+ }
791
+ }
792
+
793
+ componentDidMount() {
794
+ this.maybeValidate(this.props.value);
795
+ }
796
+
797
+ render() {
798
+ const _this$props = this.props,
799
+ {
800
+ id,
801
+ type,
802
+ value,
803
+ disabled,
804
+ onKeyDown,
805
+ placeholder,
806
+ required,
807
+ light,
808
+ style,
809
+ testId,
810
+ readOnly,
811
+ autoComplete,
812
+ forwardedRef
813
+ } = _this$props,
814
+ otherProps = _objectWithoutPropertiesLoose(_this$props, _excluded$4);
815
+
816
+ return /*#__PURE__*/createElement("input", _extends({
817
+ className: css([styles$4.input, styles$6.LabelMedium, styles$4.default, // Prioritizes disabled, then focused, then error (if any)
818
+ disabled ? styles$4.disabled : this.state.focused ? [styles$4.focused, light && styles$4.defaultLight] : this.state.error && [styles$4.error, light && styles$4.errorLight], style && style]),
819
+ id: id,
820
+ type: type,
821
+ placeholder: placeholder,
822
+ value: value,
823
+ disabled: disabled,
824
+ onChange: this.handleChange,
825
+ onKeyDown: onKeyDown,
826
+ onFocus: this.handleFocus,
827
+ onBlur: this.handleBlur,
828
+ required: required,
829
+ "data-test-id": testId,
830
+ readOnly: readOnly,
831
+ autoComplete: autoComplete,
832
+ ref: forwardedRef
833
+ }, otherProps));
834
+ }
835
+
836
+ }
837
+
838
+ TextFieldInternal.defaultProps = {
839
+ type: "text",
840
+ disabled: false,
841
+ light: false
842
+ };
843
+ const styles$4 = StyleSheet.create({
844
+ input: {
845
+ width: "100%",
846
+ height: 40,
847
+ borderRadius: 4,
848
+ boxSizing: "border-box",
849
+ paddingLeft: Spacing.medium_16,
850
+ margin: 0,
851
+ outline: "none",
852
+ boxShadow: "none"
853
+ },
854
+ default: {
855
+ background: Color.white,
856
+ border: `1px solid ${Color.offBlack16}`,
857
+ color: Color.offBlack,
858
+ "::placeholder": {
859
+ color: Color.offBlack64
860
+ }
861
+ },
862
+ error: {
863
+ background: `${mix(fade(Color.red, 0.06), Color.white)}`,
864
+ border: `1px solid ${Color.red}`,
865
+ color: Color.offBlack,
866
+ "::placeholder": {
867
+ color: Color.offBlack64
868
+ }
869
+ },
870
+ disabled: {
871
+ background: Color.offWhite,
872
+ border: `1px solid ${Color.offBlack16}`,
873
+ color: Color.offBlack64,
874
+ "::placeholder": {
875
+ color: Color.offBlack32
876
+ }
877
+ },
878
+ focused: {
879
+ background: Color.white,
880
+ border: `1px solid ${Color.blue}`,
881
+ color: Color.offBlack,
882
+ "::placeholder": {
883
+ color: Color.offBlack64
884
+ }
885
+ },
886
+ defaultLight: {
887
+ boxShadow: `0px 0px 0px 1px ${Color.blue}, 0px 0px 0px 2px ${Color.white}`
888
+ },
889
+ errorLight: {
890
+ boxShadow: `0px 0px 0px 1px ${Color.red}, 0px 0px 0px 2px ${Color.white}`
891
+ }
892
+ });
893
+ const TextField = /*#__PURE__*/forwardRef((props, ref) => /*#__PURE__*/createElement(TextFieldInternal, _extends({}, props, {
894
+ forwardedRef: ref
895
+ })));
896
+
897
+ /**
898
+ * A FieldHeading is an element that provides a label, description, and error element
899
+ * to present better context and hints to any type of form field component.
900
+ */
901
+ class FieldHeading extends Component {
902
+ renderLabel() {
903
+ const {
904
+ label,
905
+ id,
906
+ testId
907
+ } = this.props;
908
+ return /*#__PURE__*/createElement(Fragment, null, typeof label === "string" ? /*#__PURE__*/createElement(LabelMedium, {
909
+ style: styles$5.label,
910
+ tag: "label",
911
+ htmlFor: id && `${id}-field`,
912
+ testId: testId && `${testId}-label`
913
+ }, label) : label, /*#__PURE__*/createElement(Strut, {
914
+ size: Spacing.xxxSmall_4
915
+ }));
916
+ }
917
+
918
+ maybeRenderDescription() {
919
+ const {
920
+ description,
921
+ testId
922
+ } = this.props;
923
+
924
+ if (!description) {
925
+ return null;
926
+ }
927
+
928
+ return /*#__PURE__*/createElement(Fragment, null, typeof description === "string" ? /*#__PURE__*/createElement(LabelSmall, {
929
+ style: styles$5.description,
930
+ testId: testId && `${testId}-description`
931
+ }, description) : description, /*#__PURE__*/createElement(Strut, {
932
+ size: Spacing.xxxSmall_4
933
+ }));
934
+ }
935
+
936
+ maybeRenderError() {
937
+ const {
938
+ error,
939
+ id,
940
+ testId
941
+ } = this.props;
942
+
943
+ if (!error) {
944
+ return null;
945
+ }
946
+
947
+ return /*#__PURE__*/createElement(Fragment, null, /*#__PURE__*/createElement(Strut, {
948
+ size: Spacing.small_12
949
+ }), typeof error === "string" ? /*#__PURE__*/createElement(LabelSmall, {
950
+ style: styles$5.error,
951
+ role: "alert",
952
+ id: id && `${id}-error`,
953
+ testId: testId && `${testId}-error`
954
+ }, error) : error);
955
+ }
956
+
957
+ render() {
958
+ const {
959
+ field,
960
+ style
961
+ } = this.props;
962
+ return /*#__PURE__*/createElement(View, {
963
+ style: style
964
+ }, this.renderLabel(), this.maybeRenderDescription(), /*#__PURE__*/createElement(Strut, {
965
+ size: Spacing.xSmall_8
966
+ }), field, this.maybeRenderError());
967
+ }
968
+
969
+ }
970
+ const styles$5 = StyleSheet.create({
971
+ label: {
972
+ color: Color.offBlack
973
+ },
974
+ description: {
975
+ color: Color.offBlack64
976
+ },
977
+ error: {
978
+ color: Color.red
979
+ }
980
+ });
981
+
982
+ // TODO(WB-1081): Change class name back to LabeledTextField after Styleguidist is gone.
983
+
984
+ /**
985
+ * A LabeledTextField is an element used to accept a single line of text
986
+ * from the user paired with a label, description, and error field elements.
987
+ */
988
+ class LabeledTextFieldInternal extends Component {
989
+ constructor(props) {
990
+ super(props);
991
+
992
+ this.handleValidate = errorMessage => {
993
+ const {
994
+ onValidate
995
+ } = this.props;
996
+ this.setState({
997
+ error: errorMessage
998
+ }, () => {
999
+ if (onValidate) {
1000
+ onValidate(errorMessage);
1001
+ }
1002
+ });
1003
+ };
1004
+
1005
+ this.handleFocus = event => {
1006
+ const {
1007
+ onFocus
1008
+ } = this.props;
1009
+ this.setState({
1010
+ focused: true
1011
+ }, () => {
1012
+ if (onFocus) {
1013
+ onFocus(event);
1014
+ }
1015
+ });
1016
+ };
1017
+
1018
+ this.handleBlur = event => {
1019
+ const {
1020
+ onBlur
1021
+ } = this.props;
1022
+ this.setState({
1023
+ focused: false
1024
+ }, () => {
1025
+ if (onBlur) {
1026
+ onBlur(event);
1027
+ }
1028
+ });
1029
+ };
1030
+
1031
+ this.state = {
1032
+ error: null,
1033
+ focused: false
1034
+ };
1035
+ }
1036
+
1037
+ render() {
1038
+ const {
1039
+ id,
1040
+ type,
1041
+ label,
1042
+ description,
1043
+ value,
1044
+ disabled,
1045
+ validate,
1046
+ onChange,
1047
+ onKeyDown,
1048
+ placeholder,
1049
+ light,
1050
+ style,
1051
+ testId,
1052
+ readOnly,
1053
+ autoComplete,
1054
+ forwardedRef
1055
+ } = this.props;
1056
+ return /*#__PURE__*/createElement(IDProvider, {
1057
+ id: id,
1058
+ scope: "labeled-text-field"
1059
+ }, uniqueId => /*#__PURE__*/createElement(FieldHeading, {
1060
+ id: uniqueId,
1061
+ testId: testId,
1062
+ style: style,
1063
+ field: /*#__PURE__*/createElement(TextField, {
1064
+ id: `${uniqueId}-field`,
1065
+ "aria-describedby": `${uniqueId}-error`,
1066
+ "aria-invalid": this.state.error ? "true" : "false",
1067
+ testId: testId && `${testId}-field`,
1068
+ type: type,
1069
+ value: value,
1070
+ placeholder: placeholder,
1071
+ disabled: disabled,
1072
+ validate: validate,
1073
+ onValidate: this.handleValidate,
1074
+ onChange: onChange,
1075
+ onKeyDown: onKeyDown,
1076
+ onFocus: this.handleFocus,
1077
+ onBlur: this.handleBlur,
1078
+ light: light,
1079
+ readOnly: readOnly,
1080
+ autoComplete: autoComplete,
1081
+ ref: forwardedRef
1082
+ }),
1083
+ label: label,
1084
+ description: description,
1085
+ error: !this.state.focused && this.state.error || ""
1086
+ }));
1087
+ }
1088
+
1089
+ }
1090
+
1091
+ LabeledTextFieldInternal.defaultProps = {
1092
+ type: "text",
1093
+ disabled: false,
1094
+ light: false
1095
+ };
1096
+ const LabeledTextField = /*#__PURE__*/forwardRef((props, ref) => /*#__PURE__*/createElement(LabeledTextFieldInternal, _extends({}, props, {
1097
+ forwardedRef: ref
1098
+ })));
1099
+
1100
+ export { Checkbox, CheckboxGroup, Choice, LabeledTextField, Radio, RadioGroup, TextField };