@khanacademy/wonder-blocks-button 4.1.8 → 4.2.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.
@@ -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,17 +232,18 @@ 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",
@@ -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,88 @@ 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
+
327
361
  newStyles = {
328
362
  default: {
329
- background: "none",
330
- color: light ? white : color,
331
- borderColor: light ? white50 : offBlack50,
363
+ background: light
364
+ ? theme.color.bg.secondary.inverse
365
+ : theme.color.bg.secondary.default,
366
+ color: light ? theme.color.text.inverse : color,
367
+ borderColor: light
368
+ ? theme.color.border.secondary.inverse
369
+ : secondaryBorderColor,
332
370
  borderStyle: "solid",
333
- borderWidth: 1,
371
+ borderWidth: theme.border.width.secondary,
334
372
  paddingLeft: padding,
335
373
  paddingRight: padding,
336
374
  },
337
375
  focus: {
338
- background: light ? "transparent" : white,
339
- borderColor: light ? white : color,
340
- borderWidth: 2,
341
- paddingLeft: padding - 1,
342
- paddingRight: padding - 1,
376
+ background: light
377
+ ? theme.color.bg.secondary.inverse
378
+ : theme.color.bg.secondary.focus,
379
+ borderColor: light ? theme.color.border.primary.inverse : color,
380
+ borderWidth: theme.border.width.focused,
381
+ paddingLeft: horizontalPadding,
382
+ paddingRight: horizontalPadding,
343
383
  },
384
+
344
385
  active: {
345
- background: light ? activeColor : fadedColor,
386
+ background: light
387
+ ? activeColor
388
+ : theme.color.bg.secondary.active,
346
389
  color: light ? fadedColor : activeColor,
347
390
  borderColor: light ? fadedColor : activeColor,
348
- borderWidth: 2,
391
+ borderWidth: theme.border.width.focused,
349
392
  // We need to reduce padding to offset the difference
350
393
  // caused by the border becoming thicker on focus.
351
- paddingLeft: padding - 1,
352
- paddingRight: padding - 1,
394
+ paddingLeft: horizontalPadding,
395
+ paddingRight: horizontalPadding,
353
396
  },
354
397
  disabled: {
355
- color: light ? white50 : offBlack32,
356
- borderColor: light ? fadedColor : offBlack32,
398
+ color: light
399
+ ? theme.color.text.secondary.inverse
400
+ : theme.color.text.disabled,
401
+ borderColor: light ? fadedColor : theme.color.border.disabled,
357
402
  cursor: "default",
358
403
  ":focus": {
359
- borderColor: light ? white50 : offBlack32,
360
- borderWidth: 2,
404
+ borderColor: light
405
+ ? theme.color.border.secondary.inverse
406
+ : theme.color.border.disabled,
407
+ borderWidth: theme.border.width.disabled,
361
408
  // We need to reduce padding to offset the difference
362
409
  // caused by the border becoming thicker on focus.
363
410
  paddingLeft: padding - 1,
@@ -369,7 +416,7 @@ const _generateStyles = (
369
416
  newStyles = {
370
417
  default: {
371
418
  background: "none",
372
- color: light ? white : color,
419
+ color: light ? theme.color.text.inverse : color,
373
420
  paddingLeft: 0,
374
421
  paddingRight: 0,
375
422
  },
@@ -377,12 +424,12 @@ const _generateStyles = (
377
424
  ":after": {
378
425
  content: "''",
379
426
  position: "absolute",
380
- height: 2,
427
+ height: theme.size.height.tertiaryHover,
381
428
  width: "100%",
382
429
  right: 0,
383
430
  bottom: 0,
384
- background: light ? white : color,
385
- borderRadius: 2,
431
+ background: light ? theme.color.bg.tertiary.hover : color,
432
+ borderRadius: theme.border.radius.tertiary,
386
433
  },
387
434
  },
388
435
  focus: {
@@ -392,12 +439,18 @@ const _generateStyles = (
392
439
  // calculate the width/height and use absolute position to
393
440
  // prevent other elements from being shifted around.
394
441
  position: "absolute",
395
- width: `calc(100% + ${Spacing.xxxSmall_4}px)`,
396
- height: `calc(100% - ${Spacing.xxxSmall_4}px)`,
442
+ // Keeps the button at the same size when applying the
443
+ // borderWidth property, so we can apply the correct value
444
+ // per theme for each side (left and right).
445
+ width: `calc(100% + ${theme.border.width.focused * 2}px)`,
446
+ // Same as above, but for the height (top and bottom).
447
+ height: `calc(100% - ${theme.border.width.focused * 2}px)`,
397
448
  borderStyle: "solid",
398
- borderColor: light ? white : color,
399
- borderWidth: Spacing.xxxxSmall_2,
400
- borderRadius: Spacing.xxxSmall_4,
449
+ borderColor: light
450
+ ? theme.color.border.tertiary.inverse
451
+ : color,
452
+ borderWidth: theme.border.width.focused,
453
+ borderRadius: theme.border.radius.default,
401
454
  },
402
455
  },
403
456
  active: {
@@ -408,12 +461,14 @@ const _generateStyles = (
408
461
  },
409
462
  },
410
463
  disabled: {
411
- color: light ? fadedColor : offBlack32,
464
+ color: light ? fadedColor : theme.color.text.disabled,
412
465
  cursor: "default",
413
466
  },
414
467
  disabledFocus: {
415
468
  ":after": {
416
- borderColor: light ? white50 : offBlack32,
469
+ borderColor: light
470
+ ? theme.color.border.tertiary.inverse
471
+ : theme.color.border.disabled,
417
472
  },
418
473
  },
419
474
  };
@@ -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,133 @@
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: tokens.color.fadedBlue,
37
+ },
38
+
39
+ tertiary: {
40
+ hover: tokens.color.white,
41
+ },
42
+ },
43
+ text: {
44
+ /**
45
+ * Default
46
+ */
47
+ // kind="secondary, tertiary", disabled=true, light=false
48
+ disabled: tokens.color.offBlack32,
49
+ // kind="primary", light=false | kind="secondary, tertiary", light=true
50
+ inverse: tokens.color.white,
51
+ /**
52
+ * Kind
53
+ */
54
+ primary: {
55
+ disabled: tokens.color.white64,
56
+ },
57
+ secondary: {
58
+ inverse: tokens.color.white50,
59
+ },
60
+ },
61
+ border: {
62
+ /**
63
+ * Default
64
+ */
65
+ // kind="secondary", light=false | kind="tertiary", light=false
66
+ disabled: tokens.color.offBlack32,
67
+ /**
68
+ * Kind
69
+ */
70
+ primary: {
71
+ inverse: tokens.color.white,
72
+ },
73
+ secondary: {
74
+ action: tokens.color.offBlack50,
75
+ critical: tokens.color.offBlack50,
76
+ inverse: tokens.color.white50,
77
+ },
78
+ tertiary: {
79
+ inverse: tokens.color.white,
80
+ },
81
+ },
82
+ },
83
+ border: {
84
+ width: {
85
+ // secondary (resting)
86
+ secondary: tokens.border.width.hairline,
87
+ // secondary (resting, focus, active), tertiary (focus)
88
+ focused: tokens.border.width.thin,
89
+ // secondary (disabled)
90
+ disabled: tokens.border.width.thin,
91
+ },
92
+ radius: {
93
+ // default
94
+ default: tokens.border.radius.small_3,
95
+ // tertiary
96
+ tertiary: tokens.border.radius.xSmall_2,
97
+ // small button
98
+ small: tokens.border.radius.small_3,
99
+ // large button
100
+ large: tokens.border.radius.large_6,
101
+ },
102
+ },
103
+ size: {
104
+ width: {
105
+ medium: tokens.spacing.large_24,
106
+ large: tokens.spacing.xLarge_32,
107
+ },
108
+ height: {
109
+ tertiaryHover: tokens.spacing.xxxxSmall_2,
110
+ small: tokens.spacing.xLarge_32,
111
+ // NOTE: These height tokens are specific to this component.
112
+ medium: 40,
113
+ large: 56,
114
+ },
115
+ },
116
+ padding: {
117
+ small: tokens.spacing.xSmall_8,
118
+ medium: tokens.spacing.small_12,
119
+ large: tokens.spacing.medium_16,
120
+ xLarge: tokens.spacing.xLarge_32,
121
+ },
122
+ font: {
123
+ size: {
124
+ // NOTE: This token is specific to this button size.
125
+ large: 18,
126
+ },
127
+ lineHeight: {
128
+ large: tokens.font.lineHeight.medium,
129
+ },
130
+ },
131
+ };
132
+
133
+ export default theme;
@@ -0,0 +1,35 @@
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: tokens.color.fadedBlue8,
13
+ focus: tokens.color.offWhite,
14
+ },
15
+ },
16
+ border: {
17
+ secondary: {
18
+ action: tokens.color.fadedBlue,
19
+ critical: tokens.color.fadedRed,
20
+ },
21
+ },
22
+ },
23
+ border: {
24
+ radius: {
25
+ default: tokens.border.radius.xLarge_12,
26
+ small: tokens.border.radius.large_6,
27
+ large: tokens.border.radius.xLarge_12,
28
+ },
29
+ width: {
30
+ focused: tokens.border.width.hairline,
31
+ },
32
+ },
33
+ });
34
+
35
+ 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
  }