@khanacademy/wonder-blocks-button 2.10.1 → 2.10.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-button",
3
- "version": "2.10.1",
3
+ "version": "2.10.2",
4
4
  "design": "v1",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -15,14 +15,14 @@
15
15
  "author": "",
16
16
  "license": "MIT",
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-progress-spinner": "^1.1.27",
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.1",
20
+ "@khanacademy/wonder-blocks-color": "^1.1.20",
21
+ "@khanacademy/wonder-blocks-core": "^4.0.0",
22
+ "@khanacademy/wonder-blocks-icon": "^1.2.24",
23
+ "@khanacademy/wonder-blocks-progress-spinner": "^1.1.28",
24
+ "@khanacademy/wonder-blocks-spacing": "^3.0.5",
25
+ "@khanacademy/wonder-blocks-typography": "^1.1.28"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "aphrodite": "^1.2.5",
@@ -31,7 +31,7 @@
31
31
  "react-router-dom": "5.3.0"
32
32
  },
33
33
  "devDependencies": {
34
- "wb-dev-build-settings": "^0.1.2"
34
+ "wb-dev-build-settings": "^0.2.0"
35
35
  },
36
- "gitHead": "61090a61b6e9d2a735976d5fd53a15d06f10c853"
36
+ "gitHead": "9ebea88533e702011165072f090a377e02fa3f0f"
37
37
  }
@@ -2850,7 +2850,7 @@ exports[`wonder-blocks-button example 9 1`] = `
2850
2850
  }
2851
2851
  }
2852
2852
  >
2853
- Async action, client-side nav
2853
+ beforeNav, client-side nav
2854
2854
  </span>
2855
2855
  </a>
2856
2856
  <a
@@ -2918,7 +2918,7 @@ exports[`wonder-blocks-button example 9 1`] = `
2918
2918
  }
2919
2919
  }
2920
2920
  >
2921
- Async action, server-side nav
2921
+ beforeNav, server-side nav
2922
2922
  </span>
2923
2923
  </a>
2924
2924
  <a
@@ -2987,7 +2987,7 @@ exports[`wonder-blocks-button example 9 1`] = `
2987
2987
  }
2988
2988
  }
2989
2989
  >
2990
- Async action, open URL in new tab
2990
+ beforeNav, open URL in new tab
2991
2991
  </span>
2992
2992
  </a>
2993
2993
  </div>
@@ -3078,13 +3078,242 @@ exports[`wonder-blocks-button example 10 1`] = `
3078
3078
  }
3079
3079
  }
3080
3080
  >
3081
- This button prevent navigation.
3081
+ safeWithNav, client-side nav
3082
+ </span>
3083
+ </a>
3084
+ <a
3085
+ className=""
3086
+ href="/foo"
3087
+ onBlur={[Function]}
3088
+ onClick={[Function]}
3089
+ onDragStart={[Function]}
3090
+ onFocus={[Function]}
3091
+ onKeyDown={[Function]}
3092
+ onKeyUp={[Function]}
3093
+ onMouseDown={[Function]}
3094
+ onMouseEnter={[Function]}
3095
+ onMouseLeave={[Function]}
3096
+ onMouseUp={[Function]}
3097
+ onTouchCancel={[Function]}
3098
+ onTouchEnd={[Function]}
3099
+ onTouchStart={[Function]}
3100
+ role="button"
3101
+ style={
3102
+ Object {
3103
+ ":focus": Object {
3104
+ "WebkitTapHighlightColor": "rgba(0,0,0,0)",
3105
+ },
3106
+ "alignItems": "center",
3107
+ "background": "#1865f2",
3108
+ "border": "none",
3109
+ "borderRadius": 4,
3110
+ "boxSizing": "border-box",
3111
+ "color": "#ffffff",
3112
+ "cursor": "pointer",
3113
+ "display": "inline-flex",
3114
+ "height": 40,
3115
+ "justifyContent": "center",
3116
+ "marginRight": 10,
3117
+ "outline": "none",
3118
+ "paddingBottom": 0,
3119
+ "paddingLeft": 16,
3120
+ "paddingRight": 16,
3121
+ "paddingTop": 0,
3122
+ "position": "relative",
3123
+ "textDecoration": "none",
3124
+ "touchAction": "manipulation",
3125
+ "userSelect": "none",
3126
+ }
3127
+ }
3128
+ tabIndex={0}
3129
+ >
3130
+ <span
3131
+ className=""
3132
+ style={
3133
+ Object {
3134
+ "MozOsxFontSmoothing": "grayscale",
3135
+ "WebkitFontSmoothing": "antialiased",
3136
+ "alignItems": "center",
3137
+ "display": "inline-block",
3138
+ "fontFamily": "Lato, \\"Noto Sans\\", sans-serif",
3139
+ "fontSize": 16,
3140
+ "fontWeight": "bold",
3141
+ "lineHeight": "20px",
3142
+ "overflow": "hidden",
3143
+ "pointerEvents": "none",
3144
+ "textOverflow": "ellipsis",
3145
+ "whiteSpace": "nowrap",
3146
+ }
3147
+ }
3148
+ >
3149
+ safeWithNav, server-side nav
3150
+ </span>
3151
+ </a>
3152
+ <a
3153
+ className=""
3154
+ href="https://google.com"
3155
+ onBlur={[Function]}
3156
+ onClick={[Function]}
3157
+ onDragStart={[Function]}
3158
+ onFocus={[Function]}
3159
+ onKeyDown={[Function]}
3160
+ onKeyUp={[Function]}
3161
+ onMouseDown={[Function]}
3162
+ onMouseEnter={[Function]}
3163
+ onMouseLeave={[Function]}
3164
+ onMouseUp={[Function]}
3165
+ onTouchCancel={[Function]}
3166
+ onTouchEnd={[Function]}
3167
+ onTouchStart={[Function]}
3168
+ rel="noopener noreferrer"
3169
+ role="button"
3170
+ style={
3171
+ Object {
3172
+ ":focus": Object {
3173
+ "WebkitTapHighlightColor": "rgba(0,0,0,0)",
3174
+ },
3175
+ "alignItems": "center",
3176
+ "background": "#1865f2",
3177
+ "border": "none",
3178
+ "borderRadius": 4,
3179
+ "boxSizing": "border-box",
3180
+ "color": "#ffffff",
3181
+ "cursor": "pointer",
3182
+ "display": "inline-flex",
3183
+ "height": 40,
3184
+ "justifyContent": "center",
3185
+ "marginRight": 10,
3186
+ "outline": "none",
3187
+ "paddingBottom": 0,
3188
+ "paddingLeft": 16,
3189
+ "paddingRight": 16,
3190
+ "paddingTop": 0,
3191
+ "position": "relative",
3192
+ "textDecoration": "none",
3193
+ "touchAction": "manipulation",
3194
+ "userSelect": "none",
3195
+ }
3196
+ }
3197
+ tabIndex={0}
3198
+ target="_blank"
3199
+ >
3200
+ <span
3201
+ className=""
3202
+ style={
3203
+ Object {
3204
+ "MozOsxFontSmoothing": "grayscale",
3205
+ "WebkitFontSmoothing": "antialiased",
3206
+ "alignItems": "center",
3207
+ "display": "inline-block",
3208
+ "fontFamily": "Lato, \\"Noto Sans\\", sans-serif",
3209
+ "fontSize": 16,
3210
+ "fontWeight": "bold",
3211
+ "lineHeight": "20px",
3212
+ "overflow": "hidden",
3213
+ "pointerEvents": "none",
3214
+ "textOverflow": "ellipsis",
3215
+ "whiteSpace": "nowrap",
3216
+ }
3217
+ }
3218
+ >
3219
+ safeWithNav, open URL in new tab
3082
3220
  </span>
3083
3221
  </a>
3084
3222
  </div>
3085
3223
  `;
3086
3224
 
3087
3225
  exports[`wonder-blocks-button example 11 1`] = `
3226
+ <div
3227
+ className=""
3228
+ style={
3229
+ Object {
3230
+ "alignItems": "center",
3231
+ "borderStyle": "solid",
3232
+ "borderWidth": 0,
3233
+ "boxSizing": "border-box",
3234
+ "display": "flex",
3235
+ "flexDirection": "row",
3236
+ "margin": 0,
3237
+ "minHeight": 0,
3238
+ "minWidth": 0,
3239
+ "padding": 0,
3240
+ "position": "relative",
3241
+ "zIndex": 0,
3242
+ }
3243
+ }
3244
+ >
3245
+ <a
3246
+ className=""
3247
+ href="/foo"
3248
+ onBlur={[Function]}
3249
+ onClick={[Function]}
3250
+ onDragStart={[Function]}
3251
+ onFocus={[Function]}
3252
+ onKeyDown={[Function]}
3253
+ onKeyUp={[Function]}
3254
+ onMouseDown={[Function]}
3255
+ onMouseEnter={[Function]}
3256
+ onMouseLeave={[Function]}
3257
+ onMouseUp={[Function]}
3258
+ onTouchCancel={[Function]}
3259
+ onTouchEnd={[Function]}
3260
+ onTouchStart={[Function]}
3261
+ role="button"
3262
+ style={
3263
+ Object {
3264
+ ":focus": Object {
3265
+ "WebkitTapHighlightColor": "rgba(0,0,0,0)",
3266
+ },
3267
+ "alignItems": "center",
3268
+ "background": "#1865f2",
3269
+ "border": "none",
3270
+ "borderRadius": 4,
3271
+ "boxSizing": "border-box",
3272
+ "color": "#ffffff",
3273
+ "cursor": "pointer",
3274
+ "display": "inline-flex",
3275
+ "height": 40,
3276
+ "justifyContent": "center",
3277
+ "marginRight": 10,
3278
+ "outline": "none",
3279
+ "paddingBottom": 0,
3280
+ "paddingLeft": 16,
3281
+ "paddingRight": 16,
3282
+ "paddingTop": 0,
3283
+ "position": "relative",
3284
+ "textDecoration": "none",
3285
+ "touchAction": "manipulation",
3286
+ "userSelect": "none",
3287
+ }
3288
+ }
3289
+ tabIndex={0}
3290
+ >
3291
+ <span
3292
+ className=""
3293
+ style={
3294
+ Object {
3295
+ "MozOsxFontSmoothing": "grayscale",
3296
+ "WebkitFontSmoothing": "antialiased",
3297
+ "alignItems": "center",
3298
+ "display": "inline-block",
3299
+ "fontFamily": "Lato, \\"Noto Sans\\", sans-serif",
3300
+ "fontSize": 16,
3301
+ "fontWeight": "bold",
3302
+ "lineHeight": "20px",
3303
+ "overflow": "hidden",
3304
+ "pointerEvents": "none",
3305
+ "textOverflow": "ellipsis",
3306
+ "whiteSpace": "nowrap",
3307
+ }
3308
+ }
3309
+ >
3310
+ This button prevents navigation.
3311
+ </span>
3312
+ </a>
3313
+ </div>
3314
+ `;
3315
+
3316
+ exports[`wonder-blocks-button example 12 1`] = `
3088
3317
  <div
3089
3318
  className=""
3090
3319
  style={
@@ -3718,7 +3947,7 @@ exports[`wonder-blocks-button example 11 1`] = `
3718
3947
  </div>
3719
3948
  `;
3720
3949
 
3721
- exports[`wonder-blocks-button example 12 1`] = `
3950
+ exports[`wonder-blocks-button example 13 1`] = `
3722
3951
  <div
3723
3952
  className=""
3724
3953
  style={
@@ -3817,7 +4046,7 @@ exports[`wonder-blocks-button example 12 1`] = `
3817
4046
  </div>
3818
4047
  `;
3819
4048
 
3820
- exports[`wonder-blocks-button example 13 1`] = `
4049
+ exports[`wonder-blocks-button example 14 1`] = `
3821
4050
  <div
3822
4051
  className=""
3823
4052
  style={
@@ -3912,7 +4141,7 @@ exports[`wonder-blocks-button example 13 1`] = `
3912
4141
  </div>
3913
4142
  `;
3914
4143
 
3915
- exports[`wonder-blocks-button example 14 1`] = `
4144
+ exports[`wonder-blocks-button example 15 1`] = `
3916
4145
  <div
3917
4146
  className=""
3918
4147
  style={
@@ -4139,7 +4368,7 @@ exports[`wonder-blocks-button example 14 1`] = `
4139
4368
  </div>
4140
4369
  `;
4141
4370
 
4142
- exports[`wonder-blocks-button example 15 1`] = `
4371
+ exports[`wonder-blocks-button example 16 1`] = `
4143
4372
  <div
4144
4373
  className=""
4145
4374
  style={
@@ -4313,7 +4542,7 @@ exports[`wonder-blocks-button example 15 1`] = `
4313
4542
  </div>
4314
4543
  `;
4315
4544
 
4316
- exports[`wonder-blocks-button example 16 1`] = `
4545
+ exports[`wonder-blocks-button example 17 1`] = `
4317
4546
  <div
4318
4547
  className=""
4319
4548
  style={
@@ -4488,7 +4717,7 @@ exports[`wonder-blocks-button example 16 1`] = `
4488
4717
  </div>
4489
4718
  `;
4490
4719
 
4491
- exports[`wonder-blocks-button example 17 1`] = `
4720
+ exports[`wonder-blocks-button example 18 1`] = `
4492
4721
  <div
4493
4722
  className=""
4494
4723
  style={
@@ -4677,7 +4906,7 @@ exports[`wonder-blocks-button example 17 1`] = `
4677
4906
  </div>
4678
4907
  `;
4679
4908
 
4680
- exports[`wonder-blocks-button example 18 1`] = `
4909
+ exports[`wonder-blocks-button example 19 1`] = `
4681
4910
  <div
4682
4911
  className=""
4683
4912
  style={
@@ -426,7 +426,7 @@ describe("wonder-blocks-button", () => {
426
426
  })
427
427
  }
428
428
  >
429
- Async action, client-side nav
429
+ beforeNav, client-side nav
430
430
  </Button>
431
431
  <Button
432
432
  href="/foo"
@@ -438,7 +438,7 @@ describe("wonder-blocks-button", () => {
438
438
  })
439
439
  }
440
440
  >
441
- Async action, server-side nav
441
+ beforeNav, server-side nav
442
442
  </Button>
443
443
  <Button
444
444
  href="https://google.com"
@@ -451,7 +451,7 @@ describe("wonder-blocks-button", () => {
451
451
  })
452
452
  }
453
453
  >
454
- Async action, open URL in new tab
454
+ beforeNav, open URL in new tab
455
455
  </Button>
456
456
  <Switch>
457
457
  <Route path="/foo">
@@ -482,9 +482,38 @@ describe("wonder-blocks-button", () => {
482
482
  <Button
483
483
  href="/foo"
484
484
  style={styles.button}
485
- onClick={(e) => e.preventDefault()}
485
+ safeWithNav={() =>
486
+ new Promise((resolve, reject) => {
487
+ setTimeout(resolve, 1000);
488
+ })
489
+ }
490
+ >
491
+ safeWithNav, client-side nav
492
+ </Button>
493
+ <Button
494
+ href="/foo"
495
+ style={styles.button}
496
+ skipClientNav={true}
497
+ safeWithNav={() =>
498
+ new Promise((resolve, reject) => {
499
+ setTimeout(resolve, 1000);
500
+ })
501
+ }
502
+ >
503
+ safeWithNav, server-side nav
504
+ </Button>
505
+ <Button
506
+ href="https://google.com"
507
+ target="_blank"
508
+ style={styles.button}
509
+ skipClientNav={true}
510
+ safeWithNav={() =>
511
+ new Promise((resolve, reject) => {
512
+ setTimeout(resolve, 1000);
513
+ })
514
+ }
486
515
  >
487
- This button prevent navigation.
516
+ safeWithNav, open URL in new tab
488
517
  </Button>
489
518
  <Switch>
490
519
  <Route path="/foo">
@@ -499,6 +528,39 @@ describe("wonder-blocks-button", () => {
499
528
  });
500
529
 
501
530
  it("example 11", () => {
531
+ const styles = StyleSheet.create({
532
+ row: {
533
+ flexDirection: "row",
534
+ alignItems: "center",
535
+ },
536
+ button: {
537
+ marginRight: 10,
538
+ },
539
+ }); // NOTE: In actual code you would use BrowserRouter instead
540
+
541
+ const example = (
542
+ <MemoryRouter>
543
+ <View style={styles.row}>
544
+ <Button
545
+ href="/foo"
546
+ style={styles.button}
547
+ onClick={(e) => e.preventDefault()}
548
+ >
549
+ This button prevents navigation.
550
+ </Button>
551
+ <Switch>
552
+ <Route path="/foo">
553
+ <View id="foo">Hello, world!</View>
554
+ </Route>
555
+ </Switch>
556
+ </View>
557
+ </MemoryRouter>
558
+ );
559
+ const tree = renderer.create(example).toJSON();
560
+ expect(tree).toMatchSnapshot();
561
+ });
562
+
563
+ it("example 12", () => {
502
564
  const styles = StyleSheet.create({
503
565
  row: {
504
566
  flexDirection: "row",
@@ -542,7 +604,7 @@ describe("wonder-blocks-button", () => {
542
604
  expect(tree).toMatchSnapshot();
543
605
  });
544
606
 
545
- it("example 12", () => {
607
+ it("example 13", () => {
546
608
  const example = (
547
609
  <View>
548
610
  <form onSubmit={() => alert("the form was submitted")}>
@@ -554,7 +616,7 @@ describe("wonder-blocks-button", () => {
554
616
  expect(tree).toMatchSnapshot();
555
617
  });
556
618
 
557
- it("example 13", () => {
619
+ it("example 14", () => {
558
620
  const example = (
559
621
  <View>
560
622
  <Button>Label</Button>
@@ -564,7 +626,7 @@ describe("wonder-blocks-button", () => {
564
626
  expect(tree).toMatchSnapshot();
565
627
  });
566
628
 
567
- it("example 14", () => {
629
+ it("example 15", () => {
568
630
  const styles = StyleSheet.create({
569
631
  column: {
570
632
  alignItems: "flex-start",
@@ -594,7 +656,7 @@ describe("wonder-blocks-button", () => {
594
656
  expect(tree).toMatchSnapshot();
595
657
  });
596
658
 
597
- it("example 15", () => {
659
+ it("example 16", () => {
598
660
  const styles = StyleSheet.create({
599
661
  row: {
600
662
  flexDirection: "row",
@@ -621,7 +683,7 @@ describe("wonder-blocks-button", () => {
621
683
  expect(tree).toMatchSnapshot();
622
684
  });
623
685
 
624
- it("example 16", () => {
686
+ it("example 17", () => {
625
687
  const styles = StyleSheet.create({
626
688
  row: {
627
689
  flexDirection: "row",
@@ -649,7 +711,7 @@ describe("wonder-blocks-button", () => {
649
711
  expect(tree).toMatchSnapshot();
650
712
  });
651
713
 
652
- it("example 17", () => {
714
+ it("example 18", () => {
653
715
  const styles = StyleSheet.create({
654
716
  row: {
655
717
  flexDirection: "row",
@@ -672,7 +734,7 @@ describe("wonder-blocks-button", () => {
672
734
  expect(tree).toMatchSnapshot();
673
735
  });
674
736
 
675
- it("example 18", () => {
737
+ it("example 19", () => {
676
738
  const styles = StyleSheet.create({
677
739
  row: {
678
740
  flexDirection: "row",
@@ -60,8 +60,7 @@ export default {
60
60
  category: "Theming",
61
61
  type: {
62
62
  summary: "boolean",
63
- detail:
64
- "Sets primary button background color to white, and secondary and tertiary button title to color.",
63
+ detail: "Sets primary button background color to white, and secondary and tertiary button title to color.",
65
64
  },
66
65
  },
67
66
  },
@@ -161,8 +160,7 @@ export default {
161
160
  category: "Navigation",
162
161
  type: {
163
162
  summary: "Note",
164
- detail:
165
- "All URLs containing a protocol are considered external, e.g. https://khanacademy.org/math/algebra/eval-exprs will trigger a full page reload.",
163
+ detail: "All URLs containing a protocol are considered external, e.g. https://khanacademy.org/math/algebra/eval-exprs will trigger a full page reload.",
166
164
  },
167
165
  },
168
166
  },
@@ -199,12 +197,11 @@ export default {
199
197
  },
200
198
  },
201
199
  beforeNav: {
202
- description: `Run async code before navigating. If the promise returned rejects then navigation will not occur.`,
200
+ description: `Run async code before navigating. If the promise returned rejects then navigation will not occur. If both safeWithNav and beforeNav are provided, beforeNav will be run first and safeWithNav will only be run if beforeNav does not reject.`,
203
201
  table: {
204
202
  category: "Navigation",
205
203
  type: {
206
204
  summary: "() => Promise<mixed>",
207
- detail: `If both safeWithNav and beforeNav are provided, beforeNav will be run first and safeWithNav will only be run if beforeNav does not reject.`,
208
205
  },
209
206
  },
210
207
  },
@@ -0,0 +1,68 @@
1
+ import {Meta, Story, Canvas} from "@storybook/addon-docs";
2
+ import {StyleSheet} from "aphrodite";
3
+
4
+ import Button from "@khanacademy/wonder-blocks-button";
5
+ import {View} from "@khanacademy/wonder-blocks-core";
6
+
7
+ <Meta
8
+ title="Navigation/Button/Navigation Callbacks"
9
+ component={Button}
10
+ parameters={{
11
+ previewTabs: {
12
+ canvas: {hidden: true},
13
+ },
14
+ viewMode: "docs",
15
+ chromatic: {
16
+ // Disables chromatic testing for these stories.
17
+ disableSnapshot: true,
18
+ },
19
+ }}
20
+ />
21
+
22
+ ## Running Callbacks on Navigation
23
+
24
+ Sometimes you may need to run some code and also navigate when the user
25
+ clicks the button. For example, you might want to send a request to the
26
+ server and also send the user to a different page. You can do this by
27
+ passing in a URL to the `href` prop and also passing in a callback
28
+ function to either the `onClick`, `beforeNav`, or `safeWithNav` prop.
29
+ Which prop you choose depends on your use case.
30
+
31
+ - `onClick` is guaranteed to run to completion before navigation starts,
32
+ but it is not async aware, so it should only be used if all of the code
33
+ in your callback function executes synchronously.
34
+
35
+ - `beforeNav` is guaranteed to run async operations before navigation
36
+ starts. You must return a promise from the callback function passed in
37
+ to this prop, and the navigation will happen after the promise
38
+ resolves. If the promise rejects, the navigation will not occur.
39
+ This prop should be used if it's important that the async code
40
+ completely finishes before the next URL starts loading.
41
+
42
+ - `safeWithNav` runs async code concurrently with navigation when safe,
43
+ but delays navigation until the async code is finished when
44
+ concurrent execution is not safe. You must return a promise from the
45
+ callback function passed in to this prop, and Wonder Blocks will run
46
+ the async code in parallel with client-side navigation or while opening
47
+ a new tab, but will wait until the async code finishes to start a
48
+ server-side navigation. If the promise rejects the navigation will
49
+ happen anyway. This prop should be used when it's okay to load
50
+ the next URL while the async callback code is running.
51
+
52
+ This table gives an overview of the options:
53
+
54
+ | Prop | Async safe? | Completes before navigation? |
55
+ |-------------|-------------|------------------------------|
56
+ | onClick | no | yes |
57
+ | beforeNav | yes | yes |
58
+ | safeWithNav | yes | no |
59
+
60
+ It is possible to use more than one of these props on the same element.
61
+ If multiple props are used, they will run in this order: first `onClick`,
62
+ then `beforeNav`, then `safeWithNav`. If both `beforeNav` and `safeWithNav`
63
+ are used, the `safeWithNav` callback will not be called until the
64
+ `beforeNav` promise resolves successfully. If the `beforeNav` promise
65
+ rejects, `safeWithNav` will not be run.
66
+
67
+ If the `onClick` handler calls `preventDefault()`, then `beforeNav`
68
+ and `safeWithNav` will still run, but navigation will not occur.
@@ -139,11 +139,10 @@ export type SharedProps = {|
139
139
  /**
140
140
  * Function to call when button is clicked.
141
141
  *
142
- * This callback should be used for things like marking BigBingo
143
- * conversions. It should NOT be used to redirect to a different URL or to
144
- * prevent navigation via e.preventDefault(). The event passed to this
145
- * handler will have its preventDefault() and stopPropagation() methods
146
- * stubbed out.
142
+ * This callback should be used for running synchronous code, like
143
+ * dispatching a Redux action. For asynchronous code see the
144
+ * beforeNav and safeWithNav props. It should NOT be used to redirect
145
+ * to a different URL.
147
146
  *
148
147
  * Note: onClick is optional if href is present, but must be defined if
149
148
  * href is not
@@ -1,4 +1,4 @@
1
- #### Example: kind
1
+ ### Example: kind
2
2
 
3
3
  There are three `kind`s of buttons: `"primary"` (default), `"secondary"`, and
4
4
  `"tertiary"`:
@@ -40,7 +40,7 @@ const styles = StyleSheet.create({
40
40
  </View>
41
41
  ```
42
42
 
43
- #### Example: color
43
+ ### Example: color
44
44
 
45
45
  Buttons have a `color` that is either `"default"` (the default, as shown above) or `"destructive"` (as can seen below):
46
46
 
@@ -85,7 +85,7 @@ const styles = StyleSheet.create({
85
85
  </View>
86
86
  ```
87
87
 
88
- #### Example: disabled
88
+ ### Example: disabled
89
89
 
90
90
  Buttons can be `disabled`:
91
91
 
@@ -139,7 +139,7 @@ const styles = StyleSheet.create({
139
139
  </View>
140
140
  ```
141
141
 
142
- #### Example: dark
142
+ ### Example: dark
143
143
 
144
144
  Buttons on a `darkBlue` background should set `light` to `true`.
145
145
  ```jsx
@@ -212,7 +212,7 @@ const styles = StyleSheet.create({
212
212
  </View>
213
213
  ```
214
214
 
215
- #### Example: size
215
+ ### Example: size
216
216
 
217
217
  Buttons have a `size` that's either `"medium"` (default), `"small"`, or `"xlarge"`.
218
218
  ```js
@@ -309,7 +309,7 @@ const styles = StyleSheet.create({
309
309
  </View>
310
310
  ```
311
311
 
312
- #### Example: spinner
312
+ ### Example: spinner
313
313
 
314
314
  Buttons can show a `spinner`. This is useful when indicating to a user that
315
315
  their input has been recognized but that the operation will take some time.
@@ -343,7 +343,7 @@ const styles = StyleSheet.create({
343
343
  </View>
344
344
  ```
345
345
 
346
- #### Example: Navigation
346
+ ### Example: Navigation
347
347
 
348
348
  ```jsx
349
349
  import Button from "@khanacademy/wonder-blocks-button";
@@ -384,7 +384,7 @@ const styles = StyleSheet.create({
384
384
  </View>
385
385
  ```
386
386
 
387
- #### Example: Navigation with React Router
387
+ ### Example: Navigation with React Router
388
388
 
389
389
  Buttons do client-side navigation by default, if React Router exists:
390
390
  ```jsx
@@ -420,12 +420,59 @@ const styles = StyleSheet.create({
420
420
  </View>
421
421
  </MemoryRouter>
422
422
  ```
423
+ ### Running callbacks on navigation
424
+
425
+ Sometimes you may need to run some code and also navigate when the user
426
+ clicks the button. For example, you might want to send a request to the
427
+ server and also send the user to a different page. You can do this by
428
+ passing in a URL to the `href` prop and also passing in a callback
429
+ function to either the `onClick`, `beforeNav`, or `safeWithNav` prop.
430
+ Which prop you choose depends on your use case.
431
+
432
+ - `onClick` is guaranteed to run to completion before navigation starts,
433
+ but it is not async aware, so it should only be used if all of the code
434
+ in your callback function executes synchronously.
435
+
436
+ - `beforeNav` is guaranteed to run async operations before navigation
437
+ starts. You must return a promise from the callback function passed in
438
+ to this prop, and the navigation will happen after the promise
439
+ resolves. If the promise rejects, the navigation will not occur.
440
+ This prop should be used if it's important that the async code
441
+ completely finishes before the next URL starts loading.
442
+
443
+ - `safeWithNav` runs async code concurrently with navigation when safe,
444
+ but delays navigation until the async code is finished when
445
+ concurrent execution is not safe. You must return a promise from the
446
+ callback function passed in to this prop, and Wonder Blocks will run
447
+ the async code in parallel with client-side navigation or while opening
448
+ a new tab, but will wait until the async code finishes to start a
449
+ server-side navigation. If the promise rejects the navigation will
450
+ happen anyway. This prop should be used when it's okay to load
451
+ the next URL while the async callback code is running.
452
+
453
+ This table gives an overview of the options:
454
+
455
+ | Prop | Async safe? | Completes before navigation? |
456
+ |-------------|-------------|------------------------------|
457
+ | onClick | no | yes |
458
+ | beforeNav | yes | yes |
459
+ | safeWithNav | yes | no |
460
+
461
+ It is possible to use more than one of these props on the same element.
462
+ If multiple props are used, they will run in this order: first `onClick`,
463
+ then `beforeNav`, then `safeWithNav`. If both `beforeNav` and `safeWithNav`
464
+ are used, the `safeWithNav` callback will not be called until the
465
+ `beforeNav` promise resolves successfully. If the `beforeNav` promise
466
+ rejects, `safeWithNav` will not be run.
467
+
468
+ If the `onClick` handler calls `preventDefault()`, then `beforeNav`
469
+ and `safeWithNav` will still run, but navigation will not occur.
470
+
471
+ ### Example: beforeNav callbacks
472
+
473
+ These buttons always wait until the async callback code completes before
474
+ starting navigation.
423
475
 
424
- #### Example: Navigation with async action
425
-
426
- Sometimes you may need to perform an async action either before or during
427
- navigation. This can be accomplished with `beforeNav` and `safeWithNav`
428
- respectively.
429
476
  ```jsx
430
477
  import Button from "@khanacademy/wonder-blocks-button";
431
478
  import {View} from "@khanacademy/wonder-blocks-core";
@@ -452,7 +499,7 @@ const styles = StyleSheet.create({
452
499
  setTimeout(resolve, 1000);
453
500
  })}
454
501
  >
455
- Async action, client-side nav
502
+ beforeNav, client-side nav
456
503
  </Button>
457
504
  <Button
458
505
  href="/foo"
@@ -462,7 +509,7 @@ const styles = StyleSheet.create({
462
509
  setTimeout(resolve, 1000);
463
510
  })}
464
511
  >
465
- Async action, server-side nav
512
+ beforeNav, server-side nav
466
513
  </Button>
467
514
  <Button
468
515
  href="https://google.com"
@@ -473,7 +520,71 @@ const styles = StyleSheet.create({
473
520
  setTimeout(resolve, 1000);
474
521
  })}
475
522
  >
476
- Async action, open URL in new tab
523
+ beforeNav, open URL in new tab
524
+ </Button>
525
+ <Switch>
526
+ <Route path="/foo">
527
+ <View id="foo">Hello, world!</View>
528
+ </Route>
529
+ </Switch>
530
+ </View>
531
+ </MemoryRouter>
532
+ ```
533
+
534
+ ### Example: safeWithNav callbacks
535
+
536
+ These buttons navigate immediately when doing client-side navigation
537
+ or when opening a new tab, but wait until the async callback code
538
+ completes before starting server-side navigation.
539
+
540
+ ```jsx
541
+ import Button from "@khanacademy/wonder-blocks-button";
542
+ import {View} from "@khanacademy/wonder-blocks-core";
543
+ import {StyleSheet} from "aphrodite";
544
+ import {MemoryRouter, Route, Switch} from "react-router-dom";
545
+
546
+ const styles = StyleSheet.create({
547
+ row: {
548
+ flexDirection: "row",
549
+ alignItems: "center",
550
+ },
551
+ button: {
552
+ marginRight: 10,
553
+ }
554
+ });
555
+
556
+ // NOTE: In actual code you would use BrowserRouter instead
557
+ <MemoryRouter>
558
+ <View style={styles.row}>
559
+ <Button
560
+ href="/foo"
561
+ style={styles.button}
562
+ safeWithNav={() => new Promise((resolve, reject) => {
563
+ setTimeout(resolve, 1000);
564
+ })}
565
+ >
566
+ safeWithNav, client-side nav
567
+ </Button>
568
+ <Button
569
+ href="/foo"
570
+ style={styles.button}
571
+ skipClientNav={true}
572
+ safeWithNav={() => new Promise((resolve, reject) => {
573
+ setTimeout(resolve, 1000);
574
+ })}
575
+ >
576
+ safeWithNav, server-side nav
577
+ </Button>
578
+ <Button
579
+ href="https://google.com"
580
+ target="_blank"
581
+ style={styles.button}
582
+ skipClientNav={true}
583
+ safeWithNav={() => new Promise((resolve, reject) => {
584
+ setTimeout(resolve, 1000);
585
+ })}
586
+ >
587
+ safeWithNav, open URL in new tab
477
588
  </Button>
478
589
  <Switch>
479
590
  <Route path="/foo">
@@ -484,11 +595,11 @@ const styles = StyleSheet.create({
484
595
  </MemoryRouter>
485
596
  ```
486
597
 
487
- #### Example: Prevent navigation by calling e.preventDefault()
598
+ ### Example: Prevent navigation by calling e.preventDefault()
599
+
600
+ If the `onClick` callback calls `preventDefault()`, then navigation
601
+ will not occur.
488
602
 
489
- Sometimes you may need to perform an async action either before or during
490
- navigation. This can be accomplished with `beforeNav` and `safeWithNav`
491
- respectively.
492
603
  ```jsx
493
604
  import Button from "@khanacademy/wonder-blocks-button";
494
605
  import {View} from "@khanacademy/wonder-blocks-core";
@@ -513,7 +624,7 @@ const styles = StyleSheet.create({
513
624
  style={styles.button}
514
625
  onClick={e => e.preventDefault()}
515
626
  >
516
- This button prevent navigation.
627
+ This button prevents navigation.
517
628
  </Button>
518
629
  <Switch>
519
630
  <Route path="/foo">
@@ -524,7 +635,7 @@ const styles = StyleSheet.create({
524
635
  </MemoryRouter>
525
636
  ```
526
637
 
527
- #### Example: style
638
+ ### Example: style
528
639
 
529
640
  Buttons can have a `style` props which supports width, position, margin,
530
641
  and flex styles.
@@ -582,7 +693,7 @@ const kinds = ["primary", "secondary", "tertiary"];
582
693
  </View>
583
694
  ```
584
695
 
585
- #### Example: "submit" buttons in forms
696
+ ### Example: "submit" buttons in forms
586
697
 
587
698
  ```jsx
588
699
  import Button from "@khanacademy/wonder-blocks-button";
@@ -48,8 +48,7 @@ DefaultButton.args = {
48
48
  DefaultButton.parameters = {
49
49
  design: {
50
50
  type: "figma",
51
- url:
52
- "https://www.figma.com/file/VbVu3h2BpBhH80niq101MHHE/Wonder-Blocks-(Web)?node-id=401%3A307",
51
+ url: "https://www.figma.com/file/VbVu3h2BpBhH80niq101MHHE/Wonder-Blocks-(Web)?node-id=401%3A307",
53
52
  },
54
53
  chromatic: {
55
54
  // We already have screenshots of other stories that cover more of the button states
@@ -353,7 +352,7 @@ ButtonsWithRouter.parameters = {
353
352
  },
354
353
  };
355
354
 
356
- export const NavigationWithAsyncAction: StoryComponentType = () => (
355
+ export const BeforeNavCallbacks: StoryComponentType = () => (
357
356
  <MemoryRouter>
358
357
  <View style={styles.row}>
359
358
  <Button
@@ -365,7 +364,7 @@ export const NavigationWithAsyncAction: StoryComponentType = () => (
365
364
  })
366
365
  }
367
366
  >
368
- Async action, client-side nav
367
+ beforeNav, client-side nav
369
368
  </Button>
370
369
  <Button
371
370
  href="/foo"
@@ -377,7 +376,7 @@ export const NavigationWithAsyncAction: StoryComponentType = () => (
377
376
  })
378
377
  }
379
378
  >
380
- Async action, server-side nav
379
+ beforeNav, server-side nav
381
380
  </Button>
382
381
  <Button
383
382
  href="https://google.com"
@@ -389,7 +388,7 @@ export const NavigationWithAsyncAction: StoryComponentType = () => (
389
388
  })
390
389
  }
391
390
  >
392
- Async action, open URL
391
+ beforeNav, open URL in new tab
393
392
  </Button>
394
393
  <Switch>
395
394
  <Route path="/foo">
@@ -400,10 +399,71 @@ export const NavigationWithAsyncAction: StoryComponentType = () => (
400
399
  </MemoryRouter>
401
400
  );
402
401
 
403
- NavigationWithAsyncAction.parameters = {
402
+ BeforeNavCallbacks.storyName = "beforeNav Callbacks";
403
+
404
+ BeforeNavCallbacks.parameters = {
404
405
  docs: {
405
406
  storyDescription:
406
- "Sometimes you may need to perform an async action either before or during navigation. This can be accomplished with `beforeNav` and `safeWithNav` respectively.",
407
+ "These buttons always wait until the async callback code completes before starting navigation.",
408
+ },
409
+ chromatic: {
410
+ disableSnapshot: true,
411
+ },
412
+ };
413
+
414
+ export const SafeWithNavCallbacks: StoryComponentType = () => (
415
+ <MemoryRouter>
416
+ <View style={styles.row}>
417
+ <Button
418
+ href="/foo"
419
+ 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
+ >
450
+ safeWithNav, open URL in new tab
451
+ </Button>
452
+ <Switch>
453
+ <Route path="/foo">
454
+ <View id="foo">Hello, world!</View>
455
+ </Route>
456
+ </Switch>
457
+ </View>
458
+ </MemoryRouter>
459
+ );
460
+
461
+ SafeWithNavCallbacks.storyName = "safeWithNav Callbacks";
462
+
463
+ SafeWithNavCallbacks.parameters = {
464
+ docs: {
465
+ storyDescription:
466
+ "If the `onClick` callback calls `preventDefault()`, then navigation will not occur.",
407
467
  },
408
468
  chromatic: {
409
469
  disableSnapshot: true,
@@ -421,7 +481,7 @@ export const PreventNavigation: StoryComponentType = () => (
421
481
  e.preventDefault();
422
482
  }}
423
483
  >
424
- This button prevent navigation.
484
+ This button prevents navigation.
425
485
  </Button>
426
486
  <Switch>
427
487
  <Route path="/foo">