@khanacademy/wonder-blocks-button 4.1.9 → 4.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.
@@ -1,25 +1,25 @@
1
1
  import * as React from "react";
2
- import {StyleSheet} from "aphrodite";
2
+ import {CSSProperties, StyleSheet} from "aphrodite";
3
3
  import {Link} from "react-router-dom";
4
4
  import {__RouterContext} from "react-router";
5
5
 
6
6
  import {LabelLarge, LabelSmall} from "@khanacademy/wonder-blocks-typography";
7
- import Color, {
8
- SemanticColor,
9
- mix,
10
- fade,
11
- } from "@khanacademy/wonder-blocks-color";
12
7
  import {addStyle} from "@khanacademy/wonder-blocks-core";
13
8
  import {CircularSpinner} from "@khanacademy/wonder-blocks-progress-spinner";
14
9
  import Icon from "@khanacademy/wonder-blocks-icon";
15
- import Spacing from "@khanacademy/wonder-blocks-spacing";
16
10
  import {isClientSideUrl} from "@khanacademy/wonder-blocks-clickable";
11
+ import {
12
+ ThemedStylesFn,
13
+ useScopedTheme,
14
+ useStyles,
15
+ } from "@khanacademy/wonder-blocks-theming";
17
16
 
18
17
  import type {
19
18
  ChildrenProps,
20
19
  ClickableState,
21
20
  } from "@khanacademy/wonder-blocks-clickable";
22
21
  import type {SharedProps} from "./button";
22
+ import {ButtonThemeContext, ButtonThemeContract} from "../themes/themed-button";
23
23
 
24
24
  type Props = SharedProps & ChildrenProps & ClickableState;
25
25
 
@@ -34,6 +34,9 @@ const ButtonCore: React.ForwardRefExoticComponent<
34
34
  typeof Link | HTMLButtonElement | HTMLAnchorElement,
35
35
  Props
36
36
  >(function ButtonCore(props: Props, ref) {
37
+ const {theme, themeName} = useScopedTheme(ButtonThemeContext);
38
+ const sharedStyles = useStyles(themedSharedStyles, theme);
39
+
37
40
  const renderInner = (router: any): React.ReactNode => {
38
41
  const {
39
42
  children,
@@ -57,18 +60,19 @@ const ButtonCore: React.ForwardRefExoticComponent<
57
60
  ...restProps
58
61
  } = props;
59
62
 
60
- const buttonColor =
61
- color === "destructive"
62
- ? SemanticColor.controlDestructive
63
- : SemanticColor.controlDefault;
64
-
65
- const iconWidth = icon ? (size === "small" ? 16 : 24) + 8 : 0;
63
+ const iconWidth = icon
64
+ ? size === "small"
65
+ ? theme.size.width.medium
66
+ : theme.size.width.large
67
+ : 0;
66
68
  const buttonStyles = _generateStyles(
67
- buttonColor,
69
+ color,
68
70
  kind,
69
71
  light,
70
72
  iconWidth,
71
73
  size,
74
+ theme,
75
+ themeName,
72
76
  );
73
77
 
74
78
  const disabled = spinner || disabledProp;
@@ -200,19 +204,19 @@ const ButtonCore: React.ForwardRefExoticComponent<
200
204
 
201
205
  export default ButtonCore;
202
206
 
203
- const sharedStyles = StyleSheet.create({
207
+ const themedSharedStyles: ThemedStylesFn<ButtonThemeContract> = (theme) => ({
204
208
  shared: {
205
209
  position: "relative",
206
210
  display: "inline-flex",
207
211
  alignItems: "center",
208
212
  justifyContent: "center",
209
- height: 40,
213
+ height: theme.size.height.medium,
210
214
  paddingTop: 0,
211
215
  paddingBottom: 0,
212
- paddingLeft: 16,
213
- paddingRight: 16,
216
+ paddingLeft: theme.padding.large,
217
+ paddingRight: theme.padding.large,
214
218
  border: "none",
215
- borderRadius: 4,
219
+ borderRadius: theme.border.radius.default,
216
220
  cursor: "pointer",
217
221
  outline: "none",
218
222
  textDecoration: "none",
@@ -228,21 +232,22 @@ const sharedStyles = StyleSheet.create({
228
232
  },
229
233
  withIcon: {
230
234
  // The left padding for the button with icon should have 4px less padding
231
- paddingLeft: 12,
235
+ paddingLeft: theme.padding.medium,
232
236
  },
233
237
  disabled: {
234
238
  cursor: "auto",
235
239
  },
236
240
  small: {
237
- height: 32,
241
+ borderRadius: theme.border.radius.small,
242
+ height: theme.size.height.small,
238
243
  },
239
244
  large: {
240
- borderRadius: Spacing.xxSmall_6,
241
- height: 56,
245
+ borderRadius: theme.border.radius.large,
246
+ height: theme.size.height.large,
242
247
  },
243
248
  text: {
244
249
  alignItems: "center",
245
- fontWeight: "bold",
250
+ fontWeight: theme.font.weight.default,
246
251
  whiteSpace: "nowrap",
247
252
  overflow: "hidden",
248
253
  textOverflow: "ellipsis",
@@ -250,8 +255,8 @@ const sharedStyles = StyleSheet.create({
250
255
  pointerEvents: "none", // fix Safari bug where the browser was eating mouse events
251
256
  },
252
257
  largeText: {
253
- fontSize: 18,
254
- lineHeight: "20px",
258
+ fontSize: theme.font.size.large,
259
+ lineHeight: theme.font.lineHeight.large,
255
260
  },
256
261
  textWithFocus: {
257
262
  position: "relative", // allows the tertiary button border to use the label width
@@ -263,36 +268,53 @@ const sharedStyles = StyleSheet.create({
263
268
  position: "absolute",
264
269
  },
265
270
  icon: {
266
- paddingRight: Spacing.xSmall_8,
271
+ paddingRight: theme.padding.small,
267
272
  },
268
273
  });
269
274
 
270
275
  const styles: Record<string, any> = {};
271
276
 
272
277
  const _generateStyles = (
273
- color: string,
278
+ buttonColor = "default",
274
279
  kind: "primary" | "secondary" | "tertiary",
275
280
  light: boolean,
276
281
  iconWidth: number,
277
282
  size: "large" | "medium" | "small",
283
+ theme: ButtonThemeContract,
284
+ themeName: string,
278
285
  ) => {
279
- const buttonType =
280
- color + kind + light.toString() + iconWidth.toString() + size;
286
+ const color: string =
287
+ buttonColor === "destructive"
288
+ ? theme.color.bg.critical.default
289
+ : theme.color.bg.action.default;
290
+
291
+ const buttonType = `${color}-${kind}-${light}-${iconWidth}-${size}-${themeName}`;
292
+
281
293
  if (styles[buttonType]) {
282
294
  return styles[buttonType];
283
295
  }
284
296
 
285
- const {white, white50, white64, offBlack32, offBlack50, darkBlue} = Color;
286
- const fadedColor = mix(fade(color, 0.32), white);
287
- const activeColor = mix(offBlack32, color);
288
- const padding = size === "large" ? Spacing.xLarge_32 : Spacing.medium_16;
297
+ const fadedColor =
298
+ buttonColor === "destructive"
299
+ ? theme.color.bg.critical.inverse
300
+ : theme.color.bg.action.inverse;
301
+ const activeColor =
302
+ buttonColor === "destructive"
303
+ ? theme.color.bg.critical.active
304
+ : theme.color.bg.action.active;
305
+ const padding =
306
+ size === "large" ? theme.padding.xLarge : theme.padding.large;
289
307
 
290
- let newStyles: Record<string, any> = {};
308
+ let newStyles: Record<string, CSSProperties> = {};
291
309
  if (kind === "primary") {
310
+ const boxShadowInnerColor: string = light
311
+ ? theme.color.bg.primary.inverse
312
+ : theme.color.bg.primary.default;
313
+
292
314
  newStyles = {
293
315
  default: {
294
- background: light ? white : color,
295
- color: light ? color : white,
316
+ background: light ? theme.color.bg.primary.default : color,
317
+ color: light ? color : theme.color.text.inverse,
296
318
  paddingLeft: padding,
297
319
  paddingRight: padding,
298
320
  },
@@ -301,63 +323,90 @@ const _generateStyles = (
301
323
  // a background of darkBlue for the light version. The inner
302
324
  // box shadow/ring is also small enough for a slight variation
303
325
  // in the background color not to matter too much.
304
- boxShadow: `0 0 0 1px ${light ? darkBlue : white}, 0 0 0 3px ${
305
- light ? white : color
326
+ boxShadow: `0 0 0 1px ${boxShadowInnerColor}, 0 0 0 3px ${
327
+ light ? theme.color.bg.primary.default : color
306
328
  }`,
307
329
  },
308
330
  active: {
309
- boxShadow: `0 0 0 1px ${light ? darkBlue : white}, 0 0 0 3px ${
331
+ boxShadow: `0 0 0 1px ${boxShadowInnerColor}, 0 0 0 3px ${
310
332
  light ? fadedColor : activeColor
311
333
  }`,
312
334
  background: light ? fadedColor : activeColor,
313
335
  color: light ? activeColor : fadedColor,
314
336
  },
315
337
  disabled: {
316
- background: light ? fadedColor : offBlack32,
317
- color: light ? color : white64,
338
+ background: light
339
+ ? fadedColor
340
+ : theme.color.bg.primary.disabled,
341
+ color: light ? color : theme.color.text.primary.disabled,
318
342
  cursor: "default",
319
343
  ":focus": {
320
344
  boxShadow: `0 0 0 1px ${
321
- light ? offBlack32 : white
322
- }, 0 0 0 3px ${light ? fadedColor : offBlack32}`,
345
+ light
346
+ ? theme.color.bg.primary.disabled
347
+ : theme.color.bg.primary.default
348
+ }, 0 0 0 3px ${
349
+ light ? fadedColor : theme.color.bg.primary.disabled
350
+ }`,
323
351
  },
324
352
  },
325
353
  };
326
354
  } else if (kind === "secondary") {
355
+ const horizontalPadding = padding - (theme.border.width.focused - 1);
356
+ const secondaryBorderColor =
357
+ buttonColor === "destructive"
358
+ ? theme.color.border.secondary.critical
359
+ : theme.color.border.secondary.action;
360
+ const secondaryActiveColor =
361
+ buttonColor === "destructive"
362
+ ? theme.color.bg.secondary.active.critical
363
+ : theme.color.bg.secondary.active.action;
364
+
327
365
  newStyles = {
328
366
  default: {
329
- background: "none",
330
- color: light ? white : color,
331
- borderColor: light ? white50 : offBlack50,
367
+ background: light
368
+ ? theme.color.bg.secondary.inverse
369
+ : theme.color.bg.secondary.default,
370
+ color: light ? theme.color.text.inverse : color,
371
+ borderColor: light
372
+ ? theme.color.border.secondary.inverse
373
+ : secondaryBorderColor,
332
374
  borderStyle: "solid",
333
- borderWidth: 1,
375
+ borderWidth: theme.border.width.secondary,
334
376
  paddingLeft: padding,
335
377
  paddingRight: padding,
336
378
  },
337
379
  focus: {
338
- background: light ? "transparent" : white,
339
- borderColor: light ? white : color,
340
- borderWidth: 2,
341
- paddingLeft: padding - 1,
342
- paddingRight: padding - 1,
380
+ background: light
381
+ ? theme.color.bg.secondary.inverse
382
+ : theme.color.bg.secondary.focus,
383
+ borderColor: light ? theme.color.border.primary.inverse : color,
384
+ borderWidth: theme.border.width.focused,
385
+ paddingLeft: horizontalPadding,
386
+ paddingRight: horizontalPadding,
343
387
  },
388
+
344
389
  active: {
345
- background: light ? activeColor : fadedColor,
390
+ background: light ? activeColor : secondaryActiveColor,
346
391
  color: light ? fadedColor : activeColor,
347
392
  borderColor: light ? fadedColor : activeColor,
348
- borderWidth: 2,
393
+ borderWidth: theme.border.width.focused,
349
394
  // We need to reduce padding to offset the difference
350
395
  // caused by the border becoming thicker on focus.
351
- paddingLeft: padding - 1,
352
- paddingRight: padding - 1,
396
+ paddingLeft: horizontalPadding,
397
+ paddingRight: horizontalPadding,
353
398
  },
354
399
  disabled: {
355
- color: light ? white50 : offBlack32,
356
- borderColor: light ? fadedColor : offBlack32,
400
+ color: light
401
+ ? theme.color.text.secondary.inverse
402
+ : theme.color.text.disabled,
403
+ borderColor: light ? fadedColor : theme.color.border.disabled,
357
404
  cursor: "default",
358
405
  ":focus": {
359
- borderColor: light ? white50 : offBlack32,
360
- borderWidth: 2,
406
+ borderColor: light
407
+ ? theme.color.border.secondary.inverse
408
+ : theme.color.border.disabled,
409
+ borderWidth: theme.border.width.disabled,
361
410
  // We need to reduce padding to offset the difference
362
411
  // caused by the border becoming thicker on focus.
363
412
  paddingLeft: padding - 1,
@@ -369,7 +418,7 @@ const _generateStyles = (
369
418
  newStyles = {
370
419
  default: {
371
420
  background: "none",
372
- color: light ? white : color,
421
+ color: light ? theme.color.text.inverse : color,
373
422
  paddingLeft: 0,
374
423
  paddingRight: 0,
375
424
  },
@@ -377,12 +426,12 @@ const _generateStyles = (
377
426
  ":after": {
378
427
  content: "''",
379
428
  position: "absolute",
380
- height: 2,
429
+ height: theme.size.height.tertiaryHover,
381
430
  width: "100%",
382
431
  right: 0,
383
432
  bottom: 0,
384
- background: light ? white : color,
385
- borderRadius: 2,
433
+ background: light ? theme.color.bg.tertiary.hover : color,
434
+ borderRadius: theme.border.radius.tertiary,
386
435
  },
387
436
  },
388
437
  focus: {
@@ -392,12 +441,18 @@ const _generateStyles = (
392
441
  // calculate the width/height and use absolute position to
393
442
  // prevent other elements from being shifted around.
394
443
  position: "absolute",
395
- width: `calc(100% + ${Spacing.xxxSmall_4}px)`,
396
- height: `calc(100% - ${Spacing.xxxSmall_4}px)`,
444
+ // Keeps the button at the same size when applying the
445
+ // borderWidth property, so we can apply the correct value
446
+ // per theme for each side (left and right).
447
+ width: `calc(100% + ${theme.border.width.focused * 2}px)`,
448
+ // Same as above, but for the height (top and bottom).
449
+ height: `calc(100% - ${theme.border.width.focused * 2}px)`,
397
450
  borderStyle: "solid",
398
- borderColor: light ? white : color,
399
- borderWidth: Spacing.xxxxSmall_2,
400
- borderRadius: Spacing.xxxSmall_4,
451
+ borderColor: light
452
+ ? theme.color.border.tertiary.inverse
453
+ : color,
454
+ borderWidth: theme.border.width.focused,
455
+ borderRadius: theme.border.radius.default,
401
456
  },
402
457
  },
403
458
  active: {
@@ -408,12 +463,14 @@ const _generateStyles = (
408
463
  },
409
464
  },
410
465
  disabled: {
411
- color: light ? fadedColor : offBlack32,
466
+ color: light ? fadedColor : theme.color.text.disabled,
412
467
  cursor: "default",
413
468
  },
414
469
  disabledFocus: {
415
470
  ":after": {
416
- borderColor: light ? white50 : offBlack32,
471
+ borderColor: light
472
+ ? theme.color.border.tertiary.inverse
473
+ : theme.color.border.disabled,
417
474
  },
418
475
  },
419
476
  };
@@ -10,6 +10,7 @@ import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
10
10
  import type {IconAsset} from "@khanacademy/wonder-blocks-icon";
11
11
  import {Link} from "react-router-dom";
12
12
  import ButtonCore from "./button-core";
13
+ import ThemedButton from "../themes/themed-button";
13
14
 
14
15
  export type SharedProps =
15
16
  /**
@@ -287,9 +288,11 @@ const Button: React.ForwardRefExoticComponent<
287
288
  };
288
289
 
289
290
  return (
290
- <__RouterContext.Consumer>
291
- {(router) => renderClickableBehavior(router)}
292
- </__RouterContext.Consumer>
291
+ <ThemedButton>
292
+ <__RouterContext.Consumer>
293
+ {(router) => renderClickableBehavior(router)}
294
+ </__RouterContext.Consumer>
295
+ </ThemedButton>
293
296
  );
294
297
  });
295
298
 
@@ -0,0 +1,139 @@
1
+ import {tokens} from "@khanacademy/wonder-blocks-theming";
2
+
3
+ const theme = {
4
+ color: {
5
+ bg: {
6
+ /**
7
+ * Color
8
+ */
9
+ // color="default"
10
+ action: {
11
+ default: tokens.color.blue,
12
+ active: tokens.color.activeBlue,
13
+ inverse: tokens.color.fadedBlue,
14
+ },
15
+ // color="destructive"
16
+ critical: {
17
+ default: tokens.color.red,
18
+ active: tokens.color.activeRed,
19
+ inverse: tokens.color.fadedRed,
20
+ },
21
+
22
+ /**
23
+ * Kind
24
+ */
25
+ primary: {
26
+ default: tokens.color.white,
27
+ disabled: tokens.color.offBlack32,
28
+ // used in boxShadow
29
+ inverse: tokens.color.darkBlue,
30
+ },
31
+
32
+ secondary: {
33
+ default: "none",
34
+ inverse: "none",
35
+ focus: tokens.color.white,
36
+ active: {
37
+ action: tokens.color.fadedBlue,
38
+ critical: tokens.color.fadedRed,
39
+ },
40
+ },
41
+
42
+ tertiary: {
43
+ hover: tokens.color.white,
44
+ },
45
+ },
46
+ text: {
47
+ /**
48
+ * Default
49
+ */
50
+ // kind="secondary, tertiary", disabled=true, light=false
51
+ disabled: tokens.color.offBlack32,
52
+ // kind="primary", light=false | kind="secondary, tertiary", light=true
53
+ inverse: tokens.color.white,
54
+ /**
55
+ * Kind
56
+ */
57
+ primary: {
58
+ disabled: tokens.color.white64,
59
+ },
60
+ secondary: {
61
+ inverse: tokens.color.white50,
62
+ },
63
+ },
64
+ border: {
65
+ /**
66
+ * Default
67
+ */
68
+ // kind="secondary", light=false | kind="tertiary", light=false
69
+ disabled: tokens.color.offBlack32,
70
+ /**
71
+ * Kind
72
+ */
73
+ primary: {
74
+ inverse: tokens.color.white,
75
+ },
76
+ secondary: {
77
+ action: tokens.color.offBlack50,
78
+ critical: tokens.color.offBlack50,
79
+ inverse: tokens.color.white50,
80
+ },
81
+ tertiary: {
82
+ inverse: tokens.color.white,
83
+ },
84
+ },
85
+ },
86
+ border: {
87
+ width: {
88
+ // secondary (resting)
89
+ secondary: tokens.border.width.hairline,
90
+ // secondary (resting, focus, active), tertiary (focus)
91
+ focused: tokens.border.width.thin,
92
+ // secondary (disabled)
93
+ disabled: tokens.border.width.thin,
94
+ },
95
+ radius: {
96
+ // default
97
+ default: tokens.border.radius.medium_4,
98
+ // tertiary
99
+ tertiary: tokens.border.radius.xSmall_2,
100
+ // small button
101
+ small: tokens.border.radius.small_3,
102
+ // large button
103
+ large: tokens.border.radius.large_6,
104
+ },
105
+ },
106
+ size: {
107
+ width: {
108
+ medium: tokens.spacing.large_24,
109
+ large: tokens.spacing.xLarge_32,
110
+ },
111
+ height: {
112
+ tertiaryHover: tokens.spacing.xxxxSmall_2,
113
+ small: tokens.spacing.xLarge_32,
114
+ // NOTE: These height tokens are specific to this component.
115
+ medium: 40,
116
+ large: 56,
117
+ },
118
+ },
119
+ padding: {
120
+ small: tokens.spacing.xSmall_8,
121
+ medium: tokens.spacing.small_12,
122
+ large: tokens.spacing.medium_16,
123
+ xLarge: tokens.spacing.xLarge_32,
124
+ },
125
+ font: {
126
+ size: {
127
+ // NOTE: This token is specific to this button size.
128
+ large: 18,
129
+ },
130
+ lineHeight: {
131
+ large: tokens.font.lineHeight.medium,
132
+ },
133
+ weight: {
134
+ default: tokens.font.weight.bold,
135
+ },
136
+ },
137
+ };
138
+
139
+ export default theme;
@@ -0,0 +1,43 @@
1
+ import {mergeTheme, tokens} from "@khanacademy/wonder-blocks-theming";
2
+ import defaultTheme from "./default";
3
+
4
+ /**
5
+ * The overrides for the Khanmigo theme.
6
+ */
7
+ const theme = mergeTheme(defaultTheme, {
8
+ color: {
9
+ bg: {
10
+ secondary: {
11
+ default: tokens.color.offWhite,
12
+ active: {
13
+ action: tokens.color.fadedBlue8,
14
+ critical: tokens.color.fadedRed8,
15
+ },
16
+ focus: tokens.color.offWhite,
17
+ },
18
+ },
19
+ border: {
20
+ secondary: {
21
+ action: tokens.color.fadedBlue,
22
+ critical: tokens.color.fadedRed,
23
+ },
24
+ },
25
+ },
26
+ border: {
27
+ radius: {
28
+ default: tokens.border.radius.xLarge_12,
29
+ small: tokens.border.radius.large_6,
30
+ large: tokens.border.radius.xLarge_12,
31
+ },
32
+ width: {
33
+ focused: tokens.border.width.hairline,
34
+ },
35
+ },
36
+ font: {
37
+ weight: {
38
+ default: tokens.font.weight.regular,
39
+ },
40
+ },
41
+ });
42
+
43
+ export default theme;
@@ -0,0 +1,42 @@
1
+ import * as React from "react";
2
+ import {
3
+ createThemeContext,
4
+ ThemeSwitcherContext,
5
+ } from "@khanacademy/wonder-blocks-theming";
6
+
7
+ import defaultTheme from "./default";
8
+ import khanmigoTheme from "./khanmigo";
9
+
10
+ type Props = {
11
+ children: React.ReactNode;
12
+ };
13
+
14
+ /**
15
+ * The themes available to the Button component.
16
+ */
17
+ const themes = {
18
+ default: defaultTheme,
19
+ khanmigo: khanmigoTheme,
20
+ };
21
+
22
+ export type ButtonThemeContract = typeof defaultTheme;
23
+
24
+ /**
25
+ * The context that provides the theme to the Button component.
26
+ * This is generally consumed via the `useScopedTheme` hook.
27
+ */
28
+ export const ButtonThemeContext = createThemeContext(defaultTheme);
29
+
30
+ /**
31
+ * ThemedButton is a component that provides a theme to the <Button/> component.
32
+ */
33
+ export default function ThemedButton(props: Props) {
34
+ const currentTheme = React.useContext(ThemeSwitcherContext);
35
+
36
+ const theme = themes[currentTheme as keyof typeof themes] || defaultTheme;
37
+ return (
38
+ <ButtonThemeContext.Provider value={theme}>
39
+ {props.children}
40
+ </ButtonThemeContext.Provider>
41
+ );
42
+ }
@@ -12,6 +12,7 @@
12
12
  {"path": "../wonder-blocks-icon/tsconfig-build.json"},
13
13
  {"path": "../wonder-blocks-progress-spinner/tsconfig-build.json"},
14
14
  {"path": "../wonder-blocks-spacing/tsconfig-build.json"},
15
+ {"path": "../wonder-blocks-theming/tsconfig-build.json"},
15
16
  {"path": "../wonder-blocks-typography/tsconfig-build.json"},
16
17
  ]
17
18
  }