@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.
@@ -16,7 +16,7 @@ export default {
16
16
  title: "Form / LabeledTextField",
17
17
  };
18
18
 
19
- export const text: StoryComponentType = () => {
19
+ export const Text: StoryComponentType = () => {
20
20
  const [value, setValue] = React.useState("Khan");
21
21
 
22
22
  const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
@@ -37,7 +37,56 @@ export const text: StoryComponentType = () => {
37
37
  );
38
38
  };
39
39
 
40
- export const number: StoryComponentType = () => {
40
+ export const RequiredWithDefaultText: StoryComponentType = () => {
41
+ const [value, setValue] = React.useState("");
42
+
43
+ const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
44
+ if (event.key === "Enter") {
45
+ event.currentTarget.blur();
46
+ }
47
+ };
48
+
49
+ return (
50
+ <LabeledTextField
51
+ label="Name"
52
+ description="Please enter your name"
53
+ value={value}
54
+ onChange={(newValue) => setValue(newValue)}
55
+ onKeyDown={handleKeyDown}
56
+ required={true}
57
+ />
58
+ );
59
+ };
60
+
61
+ export const RequiredWithSpecifiedText: StoryComponentType = () => {
62
+ const [value, setValue] = React.useState("");
63
+
64
+ const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
65
+ if (event.key === "Enter") {
66
+ event.currentTarget.blur();
67
+ }
68
+ };
69
+
70
+ return (
71
+ <LabeledTextField
72
+ label="Name"
73
+ description="Please enter your name"
74
+ value={value}
75
+ onChange={(newValue) => setValue(newValue)}
76
+ onKeyDown={handleKeyDown}
77
+ required="This specific field is super required."
78
+ />
79
+ );
80
+ };
81
+
82
+ RequiredWithSpecifiedText.parameters = {
83
+ chromatic: {
84
+ // We have screenshots of other stories that cover this case.
85
+ disableSnapshot: true,
86
+ },
87
+ };
88
+
89
+ export const Number: StoryComponentType = () => {
41
90
  const [value, setValue] = React.useState("18");
42
91
 
43
92
  const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
@@ -59,7 +108,7 @@ export const number: StoryComponentType = () => {
59
108
  );
60
109
  };
61
110
 
62
- export const password: StoryComponentType = () => {
111
+ export const Password: StoryComponentType = () => {
63
112
  const [value, setValue] = React.useState("Password123");
64
113
 
65
114
  const validate = (value: string) => {
@@ -91,7 +140,7 @@ export const password: StoryComponentType = () => {
91
140
  );
92
141
  };
93
142
 
94
- export const email: StoryComponentType = () => {
143
+ export const Email: StoryComponentType = () => {
95
144
  const [value, setValue] = React.useState("khan@khan.org");
96
145
 
97
146
  const validate = (value: string) => {
@@ -121,7 +170,44 @@ export const email: StoryComponentType = () => {
121
170
  );
122
171
  };
123
172
 
124
- export const telephone: StoryComponentType = () => {
173
+ export const EmailRequired: StoryComponentType = () => {
174
+ const [value, setValue] = React.useState("");
175
+
176
+ const validate = (value: string) => {
177
+ const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/;
178
+ if (!emailRegex.test(value)) {
179
+ return "Please enter a valid email";
180
+ }
181
+ };
182
+
183
+ const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
184
+ if (event.key === "Enter") {
185
+ event.currentTarget.blur();
186
+ }
187
+ };
188
+
189
+ return (
190
+ <LabeledTextField
191
+ label="Email"
192
+ type="email"
193
+ onChange={(newValue) => setValue(newValue)}
194
+ description="Please provide your personal email"
195
+ value={value}
196
+ validate={validate}
197
+ onKeyDown={handleKeyDown}
198
+ required={true}
199
+ />
200
+ );
201
+ };
202
+
203
+ EmailRequired.parameters = {
204
+ chromatic: {
205
+ // We have screenshots of other stories that cover this case.
206
+ disableSnapshot: true,
207
+ },
208
+ };
209
+
210
+ export const Telephone: StoryComponentType = () => {
125
211
  const [value, setValue] = React.useState("123-456-7890");
126
212
 
127
213
  const validate = (value: string) => {
@@ -151,7 +237,7 @@ export const telephone: StoryComponentType = () => {
151
237
  );
152
238
  };
153
239
 
154
- export const error: StoryComponentType = () => {
240
+ export const Error: StoryComponentType = () => {
155
241
  const [value, setValue] = React.useState("khan");
156
242
 
157
243
  const validate = (value: string) => {
@@ -181,7 +267,7 @@ export const error: StoryComponentType = () => {
181
267
  );
182
268
  };
183
269
 
184
- export const disabled: StoryComponentType = () => (
270
+ export const Disabled: StoryComponentType = () => (
185
271
  <LabeledTextField
186
272
  label="Name"
187
273
  description="Please enter your name"
@@ -192,7 +278,7 @@ export const disabled: StoryComponentType = () => (
192
278
  />
193
279
  );
194
280
 
195
- export const light: StoryComponentType = () => {
281
+ export const Light: StoryComponentType = () => {
196
282
  const [value, setValue] = React.useState("");
197
283
 
198
284
  const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
@@ -222,7 +308,7 @@ export const light: StoryComponentType = () => {
222
308
  );
223
309
  };
224
310
 
225
- export const customStyle: StoryComponentType = () => {
311
+ export const CustomStyle: StoryComponentType = () => {
226
312
  const [firstName, setFirstName] = React.useState("");
227
313
  const [lastName, setLastName] = React.useState("");
228
314
 
@@ -257,7 +343,7 @@ export const customStyle: StoryComponentType = () => {
257
343
  );
258
344
  };
259
345
 
260
- export const ref: StoryComponentType = () => {
346
+ export const Ref: StoryComponentType = () => {
261
347
  const [value, setValue] = React.useState("Khan");
262
348
  const inputRef = React.createRef();
263
349
 
@@ -292,7 +378,7 @@ export const ref: StoryComponentType = () => {
292
378
  );
293
379
  };
294
380
 
295
- export const readOnly: StoryComponentType = () => {
381
+ export const ReadOnly: StoryComponentType = () => {
296
382
  const [value, setValue] = React.useState("Khan");
297
383
 
298
384
  const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
@@ -314,7 +400,7 @@ export const readOnly: StoryComponentType = () => {
314
400
  );
315
401
  };
316
402
 
317
- export const autoComplete: StoryComponentType = () => {
403
+ export const AutoComplete: StoryComponentType = () => {
318
404
  const [value, setValue] = React.useState("");
319
405
 
320
406
  const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
@@ -12,6 +12,8 @@ export type TextFieldType = "text" | "password" | "email" | "number" | "tel";
12
12
 
13
13
  type WithForwardRef = {|forwardedRef: React.Ref<"input">|};
14
14
 
15
+ const defaultErrorMessage = "This field is required.";
16
+
15
17
  type Props = {|
16
18
  ...AriaProps,
17
19
 
@@ -72,9 +74,28 @@ type Props = {|
72
74
  placeholder?: string,
73
75
 
74
76
  /**
75
- * Whether this component is required.
77
+ * Whether this field is required to to continue, or the error message to
78
+ * render if this field is left blank.
79
+ *
80
+ * This can be a boolean or a string.
81
+ *
82
+ * String:
83
+ * Please pass in a translated string to use as the error message that will
84
+ * render if the user leaves this field blank. If this field is required,
85
+ * and a string is not passed in, a default untranslated string will render
86
+ * upon error.
87
+ * Note: The string will not be used if a `validate` prop is passed in.
88
+ *
89
+ * Example message: i18n._("A password is required to log in.")
90
+ *
91
+ * Boolean:
92
+ * True/false indicating whether this field is required. Please do not pass
93
+ * in `true` if possible - pass in the error string instead.
94
+ * If `true` is passed, and a `validate` prop is not passed, that means
95
+ * there is no corresponding message and the default untranlsated message
96
+ * will be used.
76
97
  */
77
- required?: boolean,
98
+ required?: boolean | string,
78
99
 
79
100
  /**
80
101
  * Change the default focus ring color to fit a dark background.
@@ -138,7 +159,7 @@ class TextFieldInternal extends React.Component<PropsWithForwardRef, State> {
138
159
 
139
160
  constructor(props: PropsWithForwardRef) {
140
161
  super(props);
141
- if (props.validate) {
162
+ if (props.validate && props.value !== "") {
142
163
  // Ensures error is updated on unmounted server-side renders
143
164
  this.state.error = props.validate(props.value) || null;
144
165
  }
@@ -150,11 +171,14 @@ class TextFieldInternal extends React.Component<PropsWithForwardRef, State> {
150
171
  };
151
172
 
152
173
  componentDidMount() {
153
- this.maybeValidate(this.props.value);
174
+ if (this.props.value !== "") {
175
+ this.maybeValidate(this.props.value);
176
+ }
154
177
  }
155
178
 
156
179
  maybeValidate: (newValue: string) => void = (newValue) => {
157
- const {validate, onValidate} = this.props;
180
+ const {validate, onValidate, required} = this.props;
181
+
158
182
  if (validate) {
159
183
  const maybeError = validate(newValue) || null;
160
184
  this.setState({error: maybeError}, () => {
@@ -162,6 +186,15 @@ class TextFieldInternal extends React.Component<PropsWithForwardRef, State> {
162
186
  onValidate(maybeError);
163
187
  }
164
188
  });
189
+ } else if (required) {
190
+ const requiredString =
191
+ typeof required === "string" ? required : defaultErrorMessage;
192
+ const maybeError = newValue ? null : requiredString;
193
+ this.setState({error: maybeError}, () => {
194
+ if (onValidate) {
195
+ onValidate(maybeError);
196
+ }
197
+ });
165
198
  }
166
199
  };
167
200
 
@@ -204,7 +237,6 @@ class TextFieldInternal extends React.Component<PropsWithForwardRef, State> {
204
237
  disabled,
205
238
  onKeyDown,
206
239
  placeholder,
207
- required,
208
240
  light,
209
241
  style,
210
242
  testId,
@@ -219,6 +251,7 @@ class TextFieldInternal extends React.Component<PropsWithForwardRef, State> {
219
251
  onValidate,
220
252
  validate,
221
253
  onChange,
254
+ required,
222
255
  /* eslint-enable no-unused-vars */
223
256
  // Should only include Aria related props
224
257
  ...otherProps
@@ -249,7 +282,6 @@ class TextFieldInternal extends React.Component<PropsWithForwardRef, State> {
249
282
  onKeyDown={onKeyDown}
250
283
  onFocus={this.handleFocus}
251
284
  onBlur={this.handleBlur}
252
- required={required}
253
285
  data-test-id={testId}
254
286
  readOnly={readOnly}
255
287
  autoComplete={autoComplete}
@@ -316,11 +348,9 @@ type ExportProps = $Diff<
316
348
  WithForwardRef,
317
349
  >;
318
350
 
319
- const TextField: React.AbstractComponent<
320
- ExportProps,
321
- HTMLInputElement,
322
- > = React.forwardRef<ExportProps, HTMLInputElement>((props, ref) => (
323
- <TextFieldInternal {...props} forwardedRef={ref} />
324
- ));
351
+ const TextField: React.AbstractComponent<ExportProps, HTMLInputElement> =
352
+ React.forwardRef<ExportProps, HTMLInputElement>((props, ref) => (
353
+ <TextFieldInternal {...props} forwardedRef={ref} />
354
+ ));
325
355
 
326
356
  export default TextField;
@@ -2,7 +2,7 @@
2
2
  import * as React from "react";
3
3
  import {StyleSheet} from "aphrodite";
4
4
 
5
- import {View, Text} from "@khanacademy/wonder-blocks-core";
5
+ import {View, Text as _Text} 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";
@@ -15,7 +15,7 @@ export default {
15
15
  title: "Form / TextField",
16
16
  };
17
17
 
18
- export const text: StoryComponentType = () => {
18
+ export const Text: StoryComponentType = () => {
19
19
  const [value, setValue] = React.useState("");
20
20
 
21
21
  const handleChange = (newValue: string) => {
@@ -40,7 +40,32 @@ export const text: StoryComponentType = () => {
40
40
  );
41
41
  };
42
42
 
43
- export const number: StoryComponentType = () => {
43
+ export const Required: StoryComponentType = () => {
44
+ const [value, setValue] = React.useState("");
45
+
46
+ const handleChange = (newValue: string) => {
47
+ setValue(newValue);
48
+ };
49
+
50
+ const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
51
+ if (event.key === "Enter") {
52
+ event.currentTarget.blur();
53
+ }
54
+ };
55
+
56
+ return (
57
+ <TextField
58
+ id="tf-1"
59
+ type="text"
60
+ value={value}
61
+ onChange={handleChange}
62
+ onKeyDown={handleKeyDown}
63
+ required={true}
64
+ />
65
+ );
66
+ };
67
+
68
+ export const Number: StoryComponentType = () => {
44
69
  const [value, setValue] = React.useState("12345");
45
70
 
46
71
  const handleChange = (newValue: string) => {
@@ -65,7 +90,7 @@ export const number: StoryComponentType = () => {
65
90
  );
66
91
  };
67
92
 
68
- export const password: StoryComponentType = () => {
93
+ export const Password: StoryComponentType = () => {
69
94
  const [value, setValue] = React.useState("Password123");
70
95
  const [errorMessage, setErrorMessage] = React.useState();
71
96
  const [focused, setFocused] = React.useState(false);
@@ -118,14 +143,14 @@ export const password: StoryComponentType = () => {
118
143
  {!focused && errorMessage && (
119
144
  <View>
120
145
  <Strut size={Spacing.xSmall_8} />
121
- <Text style={styles.errorMessage}>{errorMessage}</Text>
146
+ <_Text style={styles.errorMessage}>{errorMessage}</_Text>
122
147
  </View>
123
148
  )}
124
149
  </View>
125
150
  );
126
151
  };
127
152
 
128
- export const email: StoryComponentType = () => {
153
+ export const Email: StoryComponentType = () => {
129
154
  const [value, setValue] = React.useState("khan@khanacademy.org");
130
155
  const [errorMessage, setErrorMessage] = React.useState();
131
156
  const [focused, setFocused] = React.useState(false);
@@ -176,14 +201,14 @@ export const email: StoryComponentType = () => {
176
201
  {!focused && errorMessage && (
177
202
  <View>
178
203
  <Strut size={Spacing.xSmall_8} />
179
- <Text style={styles.errorMessage}>{errorMessage}</Text>
204
+ <_Text style={styles.errorMessage}>{errorMessage}</_Text>
180
205
  </View>
181
206
  )}
182
207
  </View>
183
208
  );
184
209
  };
185
210
 
186
- export const telephone: StoryComponentType = () => {
211
+ export const Telephone: StoryComponentType = () => {
187
212
  const [value, setValue] = React.useState("123-456-7890");
188
213
  const [errorMessage, setErrorMessage] = React.useState();
189
214
  const [focused, setFocused] = React.useState(false);
@@ -234,14 +259,14 @@ export const telephone: StoryComponentType = () => {
234
259
  {!focused && errorMessage && (
235
260
  <View>
236
261
  <Strut size={Spacing.xSmall_8} />
237
- <Text style={styles.errorMessage}>{errorMessage}</Text>
262
+ <_Text style={styles.errorMessage}>{errorMessage}</_Text>
238
263
  </View>
239
264
  )}
240
265
  </View>
241
266
  );
242
267
  };
243
268
 
244
- export const error: StoryComponentType = () => {
269
+ export const Error: StoryComponentType = () => {
245
270
  const [value, setValue] = React.useState("khan");
246
271
  const [errorMessage, setErrorMessage] = React.useState();
247
272
  const [focused, setFocused] = React.useState(false);
@@ -292,14 +317,14 @@ export const error: StoryComponentType = () => {
292
317
  {!focused && errorMessage && (
293
318
  <View>
294
319
  <Strut size={Spacing.xSmall_8} />
295
- <Text style={styles.errorMessage}>{errorMessage}</Text>
320
+ <_Text style={styles.errorMessage}>{errorMessage}</_Text>
296
321
  </View>
297
322
  )}
298
323
  </View>
299
324
  );
300
325
  };
301
326
 
302
- export const disabled: StoryComponentType = () => (
327
+ export const Disabled: StoryComponentType = () => (
303
328
  <TextField
304
329
  id="tf-1"
305
330
  value=""
@@ -309,7 +334,7 @@ export const disabled: StoryComponentType = () => (
309
334
  />
310
335
  );
311
336
 
312
- export const light: StoryComponentType = () => {
337
+ export const Light: StoryComponentType = () => {
313
338
  const [value, setValue] = React.useState("khan@khanacademy.org");
314
339
  const [errorMessage, setErrorMessage] = React.useState();
315
340
  const [focused, setFocused] = React.useState(false);
@@ -361,14 +386,16 @@ export const light: StoryComponentType = () => {
361
386
  {!focused && errorMessage && (
362
387
  <View>
363
388
  <Strut size={Spacing.xSmall_8} />
364
- <Text style={styles.errorMessageLight}>{errorMessage}</Text>
389
+ <_Text style={styles.errorMessageLight}>
390
+ {errorMessage}
391
+ </_Text>
365
392
  </View>
366
393
  )}
367
394
  </View>
368
395
  );
369
396
  };
370
397
 
371
- export const customStyle: StoryComponentType = () => {
398
+ export const CustomStyle: StoryComponentType = () => {
372
399
  const [value, setValue] = React.useState("");
373
400
 
374
401
  const handleChange = (newValue: string) => {
@@ -394,7 +421,7 @@ export const customStyle: StoryComponentType = () => {
394
421
  );
395
422
  };
396
423
 
397
- export const ref: StoryComponentType = () => {
424
+ export const Ref: StoryComponentType = () => {
398
425
  const [value, setValue] = React.useState("");
399
426
  const inputRef: RefObject<typeof HTMLInputElement> = React.createRef();
400
427
 
@@ -433,7 +460,7 @@ export const ref: StoryComponentType = () => {
433
460
  );
434
461
  };
435
462
 
436
- export const readOnly: StoryComponentType = () => {
463
+ export const ReadOnly: StoryComponentType = () => {
437
464
  const [value, setValue] = React.useState("Khan");
438
465
 
439
466
  const handleChange = (newValue: string) => {
@@ -459,7 +486,7 @@ export const readOnly: StoryComponentType = () => {
459
486
  );
460
487
  };
461
488
 
462
- export const autoComplete: StoryComponentType = () => {
489
+ export const AutoComplete: StoryComponentType = () => {
463
490
  const [value, setValue] = React.useState("");
464
491
 
465
492
  const handleChange = (newValue: string) => {
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2018 Khan Academy
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.