@khanacademy/wonder-blocks-button 2.11.7 → 3.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.
@@ -38,7 +38,7 @@ describe("Button", () => {
38
38
  describe("ButtonCore", () => {
39
39
  for (const kind of ["primary", "secondary", "tertiary"]) {
40
40
  for (const color of ["default", "destructive"]) {
41
- for (const size of ["medium", "small"]) {
41
+ for (const size of ["medium", "small", "large"]) {
42
42
  for (const light of [true, false]) {
43
43
  for (const state of [
44
44
  "disabled",
@@ -5,7 +5,7 @@ import Button from "@khanacademy/wonder-blocks-button";
5
5
  import {View} from "@khanacademy/wonder-blocks-core";
6
6
 
7
7
  <Meta
8
- title="Navigation/Button/Accessibility"
8
+ title="Button / Accessibility"
9
9
  component={Button}
10
10
  parameters={{
11
11
  previewTabs: {
@@ -5,7 +5,7 @@ import Button from "@khanacademy/wonder-blocks-button";
5
5
  import {View} from "@khanacademy/wonder-blocks-core";
6
6
 
7
7
  <Meta
8
- title="Navigation/Button/Best practices"
8
+ title="Button / Best practices"
9
9
  component={Button}
10
10
  parameters={{
11
11
  previewTabs: {
@@ -66,15 +66,15 @@ export default {
66
66
  },
67
67
  size: {
68
68
  description: "The size of the button.",
69
- options: ["small", "medium", "xlarge"],
69
+ options: ["small", "medium", "large"],
70
70
  control: {type: "select"},
71
71
  table: {
72
72
  category: "Layout",
73
73
  defaultValue: {
74
- detail: `"medium" = height: 40; "small" = height: 32; "xlarge" = height: 60;`,
74
+ detail: `"medium" = height: 40; "small" = height: 32; "large" = height: 56;`,
75
75
  },
76
76
  type: {
77
- summary: `"medium" | "small" | "xlarge"`,
77
+ summary: `"medium" | "small" | "large"`,
78
78
  },
79
79
  },
80
80
  },
@@ -11,15 +11,18 @@ import {View} from "@khanacademy/wonder-blocks-core";
11
11
  import {icons} from "@khanacademy/wonder-blocks-icon";
12
12
  import {Strut} from "@khanacademy/wonder-blocks-layout";
13
13
  import Spacing from "@khanacademy/wonder-blocks-spacing";
14
+ import {LabelMedium} from "@khanacademy/wonder-blocks-typography";
14
15
  import type {StoryComponentType} from "@storybook/react";
15
- import Button from "./button.js";
16
+ import type {StyleDeclaration} from "aphrodite";
16
17
 
17
- import ComponentInfo from "../../../../.storybook/components/component-info.js";
18
- import ButtonArgTypes from "./__docs__/button.argtypes.js";
19
- import {name, version} from "../../package.json";
18
+ import Button from "../button.js";
19
+
20
+ import ComponentInfo from "../../../../../.storybook/components/component-info.js";
21
+ import ButtonArgTypes from "./button.argtypes.js";
22
+ import {name, version} from "../../../package.json";
20
23
 
21
24
  export default {
22
- title: "Navigation/Button",
25
+ title: "Button",
23
26
  component: Button,
24
27
  parameters: {
25
28
  componentSubtitle: ((
@@ -28,13 +31,14 @@ export default {
28
31
  },
29
32
  decorators: [withDesign],
30
33
  argTypes: ButtonArgTypes,
34
+ excludeStories: ["styles"],
31
35
  };
32
36
 
33
37
  const Template = (args) => <Button {...args} />;
34
38
 
35
- export const DefaultButton: StoryComponentType = Template.bind({});
39
+ export const Default: StoryComponentType = Template.bind({});
36
40
 
37
- DefaultButton.args = {
41
+ Default.args = {
38
42
  children: "Hello, world!",
39
43
  kind: "primary",
40
44
  color: "default",
@@ -45,7 +49,7 @@ DefaultButton.args = {
45
49
  onClick: () => {},
46
50
  };
47
51
 
48
- DefaultButton.parameters = {
52
+ Default.parameters = {
49
53
  design: {
50
54
  type: "figma",
51
55
  url: "https://www.figma.com/file/VbVu3h2BpBhH80niq101MHHE/Wonder-Blocks-(Web)?node-id=401%3A307",
@@ -56,7 +60,25 @@ DefaultButton.parameters = {
56
60
  },
57
61
  };
58
62
 
59
- export const BasicButtons: StoryComponentType = () => (
63
+ export const styles: StyleDeclaration = StyleSheet.create({
64
+ row: {
65
+ flexDirection: "row",
66
+ alignItems: "center",
67
+ marginBottom: Spacing.xSmall_8,
68
+ },
69
+ button: {
70
+ marginRight: Spacing.xSmall_8,
71
+ },
72
+ fillSpace: {
73
+ minWidth: 140,
74
+ },
75
+ example: {
76
+ background: Color.offWhite,
77
+ padding: Spacing.medium_16,
78
+ },
79
+ });
80
+
81
+ export const Variants: StoryComponentType = () => (
60
82
  <View>
61
83
  <View style={{flexDirection: "row"}}>
62
84
  <Button onClick={() => {}}>Hello, world!</Button>
@@ -100,14 +122,14 @@ export const BasicButtons: StoryComponentType = () => (
100
122
  </View>
101
123
  );
102
124
 
103
- BasicButtons.parameters = {
125
+ Variants.parameters = {
104
126
  docs: {
105
127
  storyDescription:
106
128
  "There are three kinds of buttons: `primary` (default), `secondary`, and `tertiary`.",
107
129
  },
108
130
  };
109
131
 
110
- export const ButtonsWithColors: StoryComponentType = () => (
132
+ export const WithColor: StoryComponentType = () => (
111
133
  <View style={styles.row}>
112
134
  <Button style={styles.button} onClick={() => {}} color="destructive">
113
135
  Primary
@@ -131,14 +153,16 @@ export const ButtonsWithColors: StoryComponentType = () => (
131
153
  </View>
132
154
  );
133
155
 
134
- ButtonsWithColors.parameters = {
156
+ WithColor.storyName = "Color";
157
+
158
+ WithColor.parameters = {
135
159
  docs: {
136
160
  storyDescription:
137
161
  "Buttons have a `color` that is either `default` (the default, as shown above) or `destructive` (as can seen below):",
138
162
  },
139
163
  };
140
164
 
141
- export const DarkBackgroundButtons: StoryComponentType = () => (
165
+ export const Dark: StoryComponentType = () => (
142
166
  <View style={{backgroundColor: Color.darkBlue}}>
143
167
  <View style={{flexDirection: "row"}}>
144
168
  <Button onClick={() => {}} light={true}>
@@ -204,7 +228,7 @@ export const DarkBackgroundButtons: StoryComponentType = () => (
204
228
  </View>
205
229
  );
206
230
 
207
- DarkBackgroundButtons.parameters = {
231
+ Dark.parameters = {
208
232
  backgrounds: {
209
233
  default: "darkBlue",
210
234
  },
@@ -214,87 +238,135 @@ DarkBackgroundButtons.parameters = {
214
238
  },
215
239
  };
216
240
 
217
- export const ButtonsWithSize: StoryComponentType = () => (
241
+ const kinds = ["primary", "secondary", "tertiary"];
242
+
243
+ export const Icon: StoryComponentType = () => (
218
244
  <View>
219
245
  <View style={styles.row}>
220
- <Button style={styles.button} onClick={() => {}} size="small">
221
- Label
222
- </Button>
223
- <Button
224
- style={styles.button}
225
- onClick={() => {}}
226
- kind="secondary"
227
- size="small"
228
- >
229
- Label
230
- </Button>
231
- <Button
232
- style={styles.button}
233
- onClick={() => {}}
234
- kind="tertiary"
235
- size="small"
236
- >
237
- Label
238
- </Button>
246
+ {kinds.map((kind, idx) => (
247
+ <Button
248
+ kind={kind}
249
+ icon={icons.contentExercise}
250
+ style={styles.button}
251
+ key={idx}
252
+ >
253
+ {kind}
254
+ </Button>
255
+ ))}
239
256
  </View>
240
257
  <View style={styles.row}>
241
- <Button style={styles.button} onClick={() => {}} size="medium">
242
- Label
243
- </Button>
244
- <Button
245
- style={styles.button}
246
- onClick={() => {}}
247
- kind="secondary"
248
- size="medium"
249
- >
250
- Label
251
- </Button>
252
- <Button
253
- style={styles.button}
254
- onClick={() => {}}
255
- kind="tertiary"
256
- size="medium"
257
- >
258
- Label
259
- </Button>
258
+ {kinds.map((kind, idx) => (
259
+ <Button
260
+ kind={kind}
261
+ icon={icons.contentExercise}
262
+ style={styles.button}
263
+ key={idx}
264
+ size="small"
265
+ >
266
+ {`${kind} small`}
267
+ </Button>
268
+ ))}
260
269
  </View>
270
+ </View>
271
+ );
272
+
273
+ Icon.parameters = {
274
+ docs: {
275
+ storyDescription: "Buttons can have an icon on it's left side.",
276
+ },
277
+ };
278
+
279
+ export const Size: StoryComponentType = () => (
280
+ <View>
261
281
  <View style={styles.row}>
262
- <Button style={styles.button} onClick={() => {}} size="xlarge">
263
- Label
264
- </Button>
265
- <Button
266
- style={styles.button}
267
- onClick={() => {}}
268
- kind="secondary"
269
- size="xlarge"
270
- >
271
- Label
272
- </Button>
273
- <Button
274
- style={styles.button}
275
- onClick={() => {}}
276
- kind="tertiary"
277
- size="xlarge"
278
- >
279
- Label
280
- </Button>
282
+ <LabelMedium style={styles.fillSpace}>small</LabelMedium>
283
+ <View style={[styles.row, styles.example]}>
284
+ <Button style={styles.button} onClick={() => {}} size="small">
285
+ Label
286
+ </Button>
287
+ <Button
288
+ style={styles.button}
289
+ onClick={() => {}}
290
+ kind="secondary"
291
+ size="small"
292
+ >
293
+ Label
294
+ </Button>
295
+ <Button
296
+ style={styles.button}
297
+ onClick={() => {}}
298
+ kind="tertiary"
299
+ size="small"
300
+ >
301
+ Label
302
+ </Button>
303
+ </View>
304
+ </View>
305
+ <View style={styles.row}>
306
+ <LabelMedium style={styles.fillSpace}>medium (default)</LabelMedium>
307
+
308
+ <View style={[styles.row, styles.example]}>
309
+ <Button style={styles.button} onClick={() => {}} size="medium">
310
+ Label
311
+ </Button>
312
+ <Button
313
+ style={styles.button}
314
+ onClick={() => {}}
315
+ kind="secondary"
316
+ size="medium"
317
+ >
318
+ Label
319
+ </Button>
320
+ <Button
321
+ style={styles.button}
322
+ onClick={() => {}}
323
+ kind="tertiary"
324
+ size="medium"
325
+ >
326
+ Label
327
+ </Button>
328
+ </View>
329
+ </View>
330
+ <View style={styles.row}>
331
+ <LabelMedium style={styles.fillSpace}>large</LabelMedium>
332
+ <View style={[styles.row, styles.example]}>
333
+ <Button style={styles.button} onClick={() => {}} size="large">
334
+ Label
335
+ </Button>
336
+ <Button
337
+ style={styles.button}
338
+ onClick={() => {}}
339
+ kind="secondary"
340
+ size="large"
341
+ >
342
+ Label
343
+ </Button>
344
+ <Button
345
+ style={styles.button}
346
+ onClick={() => {}}
347
+ kind="tertiary"
348
+ size="large"
349
+ >
350
+ Label
351
+ </Button>
352
+ </View>
281
353
  </View>
282
354
  </View>
283
355
  );
284
356
 
285
- ButtonsWithSize.parameters = {
357
+ Size.parameters = {
286
358
  docs: {
287
359
  storyDescription:
288
- "Buttons have a size that's either `medium` (default), `small`, or `xlarge`.",
360
+ "Buttons have a size that's either `medium` (default), `small`, or `large`.",
289
361
  },
290
362
  };
291
363
 
292
- export const ButtonWithSpinner: StoryComponentType = () => (
364
+ export const Spinner: StoryComponentType = () => (
293
365
  <View style={{flexDirection: "row"}}>
294
366
  <Button
295
367
  onClick={() => {}}
296
368
  spinner={true}
297
- size="xlarge"
369
+ size="large"
298
370
  aria-label={"waiting"}
299
371
  >
300
372
  Hello, world
@@ -315,139 +387,71 @@ export const ButtonWithSpinner: StoryComponentType = () => (
315
387
  </View>
316
388
  );
317
389
 
318
- ButtonWithSpinner.parameters = {
390
+ Spinner.parameters = {
319
391
  docs: {
320
392
  storyDescription:
321
393
  "Buttons can show a spinner. This is useful when indicating to a user that their input has been recognized but that the operation will take some time. While the spinner property is set to true the button is disabled.",
322
394
  },
323
395
  };
324
396
 
325
- export const ButtonsWithRouter: StoryComponentType = () => (
326
- <MemoryRouter>
327
- <View style={styles.row}>
328
- <Button href="/foo" style={styles.button}>
329
- Uses Client-side Nav
330
- </Button>
331
- <Button href="/foo" style={styles.button} skipClientNav>
332
- Avoids Client-side Nav
333
- </Button>
334
- <Switch>
335
- <Route path="/foo">
336
- <View id="foo">Hello, world!</View>
337
- </Route>
338
- </Switch>
339
- </View>
340
- </MemoryRouter>
397
+ export const TruncatingLabels: StoryComponentType = () => (
398
+ <Button onClick={() => {}} style={{maxWidth: 200}}>
399
+ label too long for the parent container
400
+ </Button>
341
401
  );
342
402
 
343
- ButtonsWithRouter.storyName = "Navigation with React Router";
344
-
345
- ButtonsWithRouter.parameters = {
403
+ TruncatingLabels.parameters = {
346
404
  docs: {
347
405
  storyDescription:
348
- "Buttons do client-side navigation by default, if React Router exists:",
349
- },
350
- chromatic: {
351
- disableSnapshot: true,
406
+ "If the label is too long for the button width, the text will be truncated.",
352
407
  },
353
408
  };
354
409
 
355
- export const BeforeNavCallbacks: StoryComponentType = () => (
356
- <MemoryRouter>
357
- <View style={styles.row}>
358
- <Button
359
- href="/foo"
360
- style={styles.button}
361
- beforeNav={() =>
362
- new Promise((resolve, reject) => {
363
- setTimeout(resolve, 1000);
364
- })
365
- }
366
- >
367
- beforeNav, client-side nav
368
- </Button>
369
- <Button
370
- href="/foo"
371
- style={styles.button}
372
- skipClientNav={true}
373
- beforeNav={() =>
374
- new Promise((resolve, reject) => {
375
- setTimeout(resolve, 1000);
376
- })
377
- }
378
- >
379
- beforeNav, server-side nav
380
- </Button>
381
- <Button
382
- href="https://google.com"
383
- style={styles.button}
384
- skipClientNav={true}
385
- beforeNav={() =>
386
- new Promise((resolve, reject) => {
387
- setTimeout(resolve, 1000);
388
- })
389
- }
390
- >
391
- beforeNav, open URL in new tab
392
- </Button>
393
- <Switch>
394
- <Route path="/foo">
395
- <View id="foo">Hello, world!</View>
396
- </Route>
397
- </Switch>
410
+ TruncatingLabels.storyName = "Truncating labels";
411
+
412
+ export const SubmittingForms: StoryComponentType = () => (
413
+ <form
414
+ onSubmit={(e) => {
415
+ e.preventDefault();
416
+ window.alert("form submitted"); // eslint-disable-line no-alert
417
+ }}
418
+ >
419
+ <View>
420
+ Foo: <input id="foo" value="bar" />
421
+ <Button type="submit">Submit</Button>
398
422
  </View>
399
- </MemoryRouter>
423
+ </form>
400
424
  );
401
425
 
402
- BeforeNavCallbacks.storyName = "beforeNav Callbacks";
403
-
404
- BeforeNavCallbacks.parameters = {
426
+ SubmittingForms.parameters = {
405
427
  docs: {
406
428
  storyDescription:
407
- "These buttons always wait until the async callback code completes before starting navigation.",
429
+ 'If the button is inside a form, you can use the `type="submit"` variant, so the form will be submitted on click.',
430
+ },
431
+ options: {
432
+ showAddonPanel: true,
408
433
  },
409
434
  chromatic: {
435
+ // We already have screenshots of other stories that cover more of the
436
+ // button states.
410
437
  disableSnapshot: true,
411
438
  },
412
439
  };
413
440
 
414
- export const SafeWithNavCallbacks: StoryComponentType = () => (
441
+ SubmittingForms.storyName = "Submitting forms";
442
+
443
+ export const PreventNavigation: StoryComponentType = () => (
415
444
  <MemoryRouter>
416
445
  <View style={styles.row}>
417
446
  <Button
418
447
  href="/foo"
419
448
  style={styles.button}
420
- safeWithNav={() =>
421
- new Promise((resolve, reject) => {
422
- setTimeout(resolve, 1000);
423
- })
424
- }
425
- >
426
- safeWithNav, client-side nav
427
- </Button>
428
- <Button
429
- href="/foo"
430
- style={styles.button}
431
- skipClientNav={true}
432
- safeWithNav={() =>
433
- new Promise((resolve, reject) => {
434
- setTimeout(resolve, 1000);
435
- })
436
- }
437
- >
438
- safeWithNav, server-side nav
439
- </Button>
440
- <Button
441
- href="https://google.com"
442
- style={styles.button}
443
- skipClientNav={true}
444
- safeWithNav={() =>
445
- new Promise((resolve, reject) => {
446
- setTimeout(resolve, 1000);
447
- })
448
- }
449
+ onClick={(e) => {
450
+ action("clicked")(e);
451
+ e.preventDefault();
452
+ }}
449
453
  >
450
- safeWithNav, open URL in new tab
454
+ This button prevents navigation.
451
455
  </Button>
452
456
  <Switch>
453
457
  <Route path="/foo">
@@ -458,30 +462,26 @@ export const SafeWithNavCallbacks: StoryComponentType = () => (
458
462
  </MemoryRouter>
459
463
  );
460
464
 
461
- SafeWithNavCallbacks.storyName = "safeWithNav Callbacks";
465
+ PreventNavigation.storyName = "Preventing navigation";
462
466
 
463
- SafeWithNavCallbacks.parameters = {
467
+ PreventNavigation.parameters = {
464
468
  docs: {
465
469
  storyDescription:
466
- "If the `onClick` callback calls `preventDefault()`, then navigation will not occur.",
470
+ "Sometimes you may need to perform an async action either before or during navigation. This can be accomplished with `beforeNav` and `safeWithNav` respectively.",
467
471
  },
468
472
  chromatic: {
469
473
  disableSnapshot: true,
470
474
  },
471
475
  };
472
476
 
473
- export const PreventNavigation: StoryComponentType = () => (
477
+ export const WithRouter: StoryComponentType = () => (
474
478
  <MemoryRouter>
475
479
  <View style={styles.row}>
476
- <Button
477
- href="/foo"
478
- style={styles.button}
479
- onClick={(e) => {
480
- action("clicked")(e);
481
- e.preventDefault();
482
- }}
483
- >
484
- This button prevents navigation.
480
+ <Button href="/foo" style={styles.button}>
481
+ Uses Client-side Nav
482
+ </Button>
483
+ <Button href="/foo" style={styles.button} skipClientNav>
484
+ Avoids Client-side Nav
485
485
  </Button>
486
486
  <Switch>
487
487
  <Route path="/foo">
@@ -492,94 +492,14 @@ export const PreventNavigation: StoryComponentType = () => (
492
492
  </MemoryRouter>
493
493
  );
494
494
 
495
- PreventNavigation.storyName =
496
- "Prevent navigation by calling e.preventDefault()";
495
+ WithRouter.storyName = "Navigation with React Router";
497
496
 
498
- PreventNavigation.parameters = {
497
+ WithRouter.parameters = {
499
498
  docs: {
500
499
  storyDescription:
501
- "Sometimes you may need to perform an async action either before or during navigation. This can be accomplished with `beforeNav` and `safeWithNav` respectively.",
502
- },
503
- chromatic: {
504
- disableSnapshot: true,
505
- },
506
- };
507
-
508
- const kinds = ["primary", "secondary", "tertiary"];
509
-
510
- export const ButtonsWithIcons: StoryComponentType = () => (
511
- <View>
512
- <View style={styles.row}>
513
- {kinds.map((kind, idx) => (
514
- <Button
515
- kind={kind}
516
- icon={icons.contentExercise}
517
- style={styles.button}
518
- key={idx}
519
- >
520
- {kind}
521
- </Button>
522
- ))}
523
- </View>
524
- <View style={styles.row}>
525
- {kinds.map((kind, idx) => (
526
- <Button
527
- kind={kind}
528
- icon={icons.contentExercise}
529
- style={styles.button}
530
- key={idx}
531
- size="small"
532
- >
533
- {`${kind} small`}
534
- </Button>
535
- ))}
536
- </View>
537
- </View>
538
- );
539
-
540
- ButtonsWithIcons.parameters = {
541
- docs: {
542
- storyDescription: "Buttons can have an icon on it's left side.",
543
- },
544
- };
545
-
546
- export const LongLabelsAreEllipsized: StoryComponentType = () => (
547
- <Button onClick={() => {}} style={{maxWidth: 200}}>
548
- label too long for the parent container
549
- </Button>
550
- );
551
-
552
- export const SubmitButtonInForm: StoryComponentType = () => (
553
- <form
554
- onSubmit={(e) => {
555
- e.preventDefault();
556
- window.alert("form submitted"); // eslint-disable-line no-alert
557
- }}
558
- >
559
- <View>
560
- Foo: <input id="foo" value="bar" />
561
- <Button type="submit">Submit</Button>
562
- </View>
563
- </form>
564
- );
565
-
566
- SubmitButtonInForm.parameters = {
567
- options: {
568
- showAddonPanel: true,
500
+ "Buttons do client-side navigation by default, if React Router exists:",
569
501
  },
570
502
  chromatic: {
571
- // We already have screenshots of other stories that cover more of the
572
- // button states.
573
503
  disableSnapshot: true,
574
504
  },
575
505
  };
576
-
577
- const styles = StyleSheet.create({
578
- row: {
579
- flexDirection: "row",
580
- marginBottom: Spacing.xSmall_8,
581
- },
582
- button: {
583
- marginRight: Spacing.xSmall_8,
584
- },
585
- });