@khanacademy/wonder-blocks-button 6.3.9 → 6.3.10

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,48 +0,0 @@
1
- import * as React from "react";
2
-
3
- import Button from "../button";
4
-
5
- <Button beforeNav={() => Promise.resolve()}>Hello, world!</Button>;
6
-
7
- <Button safeWithNav={() => Promise.resolve()}>Hello, world!</Button>;
8
-
9
- // It's okay to use onClick with href
10
- <Button href="/foo" onClick={() => {}}>
11
- Hello, world!
12
- </Button>;
13
-
14
- <Button href="/foo" beforeNav={() => Promise.resolve()}>
15
- Hello, world!
16
- </Button>;
17
-
18
- <Button href="/foo" safeWithNav={() => Promise.resolve()}>
19
- Hello, world!
20
- </Button>;
21
-
22
- // All three of these props can be used together
23
- <Button
24
- href="/foo"
25
- beforeNav={() => Promise.resolve()}
26
- safeWithNav={() => Promise.resolve()}
27
- onClick={() => {}}
28
- >
29
- Hello, world!
30
- </Button>;
31
-
32
- // It's also fine to use href by itself
33
- <Button href="/foo">Hello, world!</Button>;
34
-
35
- const getUrl = () => "/foo";
36
-
37
- // This test purposefully uses a function to get a string to pass with href.
38
- // This can trigger errors if there are ambiguous cases in the disjoint union
39
- // type being used to describe the props. It's unclear why this error isn't
40
- // trigger by passing a string directly as the href.
41
- <Button href={getUrl()}>Hello, world!</Button>;
42
-
43
- <Button href="/foo" type="submit">
44
- Hello, world!
45
- </Button>;
46
-
47
- // type="submit" on its own is fine.
48
- <Button type="submit">Hello, world!</Button>;
@@ -1,519 +0,0 @@
1
- import * as React from "react";
2
- import {CSSProperties, StyleSheet} from "aphrodite";
3
- import {Link} from "react-router-dom";
4
- import {__RouterContext} from "react-router";
5
-
6
- import {LabelLarge, LabelSmall} from "@khanacademy/wonder-blocks-typography";
7
- import {addStyle, View} from "@khanacademy/wonder-blocks-core";
8
- import {CircularSpinner} from "@khanacademy/wonder-blocks-progress-spinner";
9
- import {isClientSideUrl} from "@khanacademy/wonder-blocks-clickable";
10
- import {
11
- ThemedStylesFn,
12
- useScopedTheme,
13
- useStyles,
14
- } from "@khanacademy/wonder-blocks-theming";
15
-
16
- import type {
17
- ChildrenProps,
18
- ClickableState,
19
- } from "@khanacademy/wonder-blocks-clickable";
20
- import type {SharedProps} from "./button";
21
- import {ButtonThemeContext, ButtonThemeContract} from "../themes/themed-button";
22
- import {ButtonIcon} from "./button-icon";
23
-
24
- type Props = SharedProps & ChildrenProps & ClickableState;
25
-
26
- const StyledAnchor = addStyle("a");
27
- const StyledButton = addStyle("button");
28
- const StyledLink = addStyle(Link);
29
-
30
- const ButtonCore: React.ForwardRefExoticComponent<
31
- Props &
32
- React.RefAttributes<typeof Link | HTMLButtonElement | HTMLAnchorElement>
33
- > = React.forwardRef<
34
- typeof Link | HTMLButtonElement | HTMLAnchorElement,
35
- Props
36
- >(function ButtonCore(props: Props, ref) {
37
- const {theme, themeName} = useScopedTheme(ButtonThemeContext);
38
- const sharedStyles = useStyles(themedSharedStyles, theme);
39
-
40
- const renderInner = (router: any): React.ReactNode => {
41
- const {
42
- children,
43
- skipClientNav,
44
- color,
45
- disabled: disabledProp,
46
- focused,
47
- hovered,
48
- href = undefined,
49
- kind = "primary",
50
- labelStyle,
51
- light = false,
52
- pressed,
53
- size = "medium",
54
- style,
55
- testId,
56
- type = undefined,
57
- spinner,
58
- startIcon,
59
- endIcon,
60
- id,
61
- waiting: _,
62
- ...restProps
63
- } = props;
64
-
65
- const buttonStyles = _generateStyles(
66
- color,
67
- kind,
68
- light,
69
- size,
70
- theme,
71
- themeName,
72
- );
73
-
74
- const disabled = spinner || disabledProp;
75
-
76
- const defaultStyle = [
77
- sharedStyles.shared,
78
- disabled && sharedStyles.disabled,
79
- startIcon && sharedStyles.withStartIcon,
80
- endIcon && sharedStyles.withEndIcon,
81
- buttonStyles.default,
82
- disabled && buttonStyles.disabled,
83
- // apply focus effect only to default and secondary buttons
84
- kind !== "tertiary" &&
85
- !disabled &&
86
- (pressed
87
- ? buttonStyles.active
88
- : (hovered || focused) && buttonStyles.focus),
89
- kind === "tertiary" &&
90
- !pressed &&
91
- focused && [
92
- buttonStyles.focus,
93
- disabled && buttonStyles.disabledFocus,
94
- ],
95
- size === "small" && sharedStyles.small,
96
- size === "large" && sharedStyles.large,
97
- ];
98
-
99
- const commonProps = {
100
- "data-testid": testId,
101
- id: id,
102
- role: "button",
103
- style: [defaultStyle, style],
104
- ...restProps,
105
- } as const;
106
-
107
- const Label = size === "small" ? LabelSmall : LabelLarge;
108
-
109
- const label = (
110
- <Label
111
- style={[
112
- sharedStyles.text,
113
- size === "large" && sharedStyles.largeText,
114
- labelStyle,
115
- spinner && sharedStyles.hiddenText,
116
- kind === "tertiary" && sharedStyles.textWithFocus,
117
- // apply press/hover effects on the label
118
- kind === "tertiary" &&
119
- !disabled &&
120
- (pressed
121
- ? [buttonStyles.hover, buttonStyles.active]
122
- : hovered && buttonStyles.hover),
123
- ]}
124
- testId={testId ? `${testId}-inner-label` : undefined}
125
- >
126
- {children}
127
- </Label>
128
- );
129
-
130
- const sizeMapping = {
131
- medium: "small",
132
- small: "xsmall",
133
- large: "medium",
134
- } as const;
135
-
136
- // We have to use `medium` for both md and lg buttons so we can fit the
137
- // icons in large buttons.
138
- const iconSize = size === "small" ? "small" : "medium";
139
-
140
- const contents = (
141
- <React.Fragment>
142
- {startIcon && (
143
- <View
144
- // The start icon doesn't have the circle around it
145
- // in the Khanmigo theme, but we wrap it with
146
- // iconWrapper anyway to give it the same spacing
147
- // as the end icon so the button is symmetrical.
148
- style={sharedStyles.iconWrapper}
149
- >
150
- <ButtonIcon
151
- size={iconSize}
152
- icon={startIcon}
153
- style={[
154
- sharedStyles.startIcon,
155
- kind === "tertiary" &&
156
- sharedStyles.tertiaryStartIcon,
157
- ]}
158
- testId={testId ? `${testId}-start-icon` : undefined}
159
- />
160
- </View>
161
- )}
162
- {label}
163
- {spinner && (
164
- <CircularSpinner
165
- style={sharedStyles.spinner}
166
- size={sizeMapping[size]}
167
- light={kind === "primary"}
168
- testId={`${testId || "button"}-spinner`}
169
- />
170
- )}
171
- {endIcon && (
172
- <View
173
- testId={
174
- testId ? `${testId}-end-icon-wrapper` : undefined
175
- }
176
- style={[
177
- styles.endIcon,
178
- sharedStyles.iconWrapper,
179
- sharedStyles.endIconWrapper,
180
- kind === "tertiary" &&
181
- sharedStyles.endIconWrapperTertiary,
182
- (focused || hovered) &&
183
- kind !== "primary" &&
184
- sharedStyles.iconWrapperSecondaryHovered,
185
- ]}
186
- >
187
- <ButtonIcon
188
- size={iconSize}
189
- icon={endIcon}
190
- testId={testId ? `${testId}-end-icon` : undefined}
191
- />
192
- </View>
193
- )}
194
- </React.Fragment>
195
- );
196
-
197
- if (href && !disabled) {
198
- return router && !skipClientNav && isClientSideUrl(href) ? (
199
- <StyledLink
200
- {...commonProps}
201
- to={href}
202
- ref={ref as React.Ref<typeof Link>}
203
- >
204
- {contents}
205
- </StyledLink>
206
- ) : (
207
- <StyledAnchor
208
- {...commonProps}
209
- href={href}
210
- ref={ref as React.Ref<HTMLAnchorElement>}
211
- >
212
- {contents}
213
- </StyledAnchor>
214
- );
215
- } else {
216
- return (
217
- <StyledButton
218
- type={type || "button"}
219
- {...commonProps}
220
- aria-disabled={disabled}
221
- ref={ref as React.Ref<HTMLButtonElement>}
222
- >
223
- {contents}
224
- </StyledButton>
225
- );
226
- }
227
- };
228
-
229
- return (
230
- <__RouterContext.Consumer>
231
- {(router) => renderInner(router)}
232
- </__RouterContext.Consumer>
233
- );
234
- });
235
-
236
- export default ButtonCore;
237
-
238
- const themedSharedStyles: ThemedStylesFn<ButtonThemeContract> = (theme) => ({
239
- shared: {
240
- position: "relative",
241
- display: "inline-flex",
242
- alignItems: "center",
243
- justifyContent: "center",
244
- height: theme.size.height.medium,
245
- paddingTop: 0,
246
- paddingBottom: 0,
247
- paddingLeft: theme.padding.large,
248
- paddingRight: theme.padding.large,
249
- border: "none",
250
- borderRadius: theme.border.radius.default,
251
- cursor: "pointer",
252
- outline: "none",
253
- textDecoration: "none",
254
- boxSizing: "border-box",
255
- // This removes the 300ms click delay on mobile browsers by indicating that
256
- // "double-tap to zoom" shouldn't be used on this element.
257
- touchAction: "manipulation",
258
- userSelect: "none",
259
- ":focus": {
260
- // Mobile: Removes a blue highlight style shown when the user clicks a button
261
- WebkitTapHighlightColor: "rgba(0,0,0,0)",
262
- },
263
- },
264
- disabled: {
265
- cursor: "auto",
266
- },
267
- small: {
268
- borderRadius: theme.border.radius.small,
269
- height: theme.size.height.small,
270
- },
271
- large: {
272
- borderRadius: theme.border.radius.large,
273
- height: theme.size.height.large,
274
- },
275
- text: {
276
- alignItems: "center",
277
- fontWeight: theme.font.weight.default,
278
- whiteSpace: "nowrap",
279
- overflow: "hidden",
280
- textOverflow: "ellipsis",
281
- display: "inline-block", // allows the button text to truncate
282
- pointerEvents: "none", // fix Safari bug where the browser was eating mouse events
283
- },
284
- largeText: {
285
- fontSize: theme.font.size.large,
286
- lineHeight: `${theme.font.lineHeight.large}px`,
287
- },
288
- textWithFocus: {
289
- position: "relative", // allows the tertiary button border to use the label width
290
- },
291
- hiddenText: {
292
- visibility: "hidden",
293
- },
294
- spinner: {
295
- position: "absolute",
296
- },
297
- startIcon: {
298
- marginRight: theme.padding.small,
299
- marginLeft: theme.margin.icon.offset,
300
- },
301
- tertiaryStartIcon: {
302
- // Undo the negative padding from startIcon since tertiary
303
- // buttons don't have extra padding.
304
- marginLeft: 0,
305
- },
306
- endIcon: {
307
- marginLeft: theme.padding.small,
308
- },
309
- iconWrapper: {
310
- borderRadius: theme.border.radius.icon,
311
- padding: theme.padding.xsmall,
312
- // View has a default minWidth of 0, which causes the label text
313
- // to encroach on the icon when it needs to truncate. We can fix
314
- // this by setting the minWidth to auto.
315
- minWidth: "auto",
316
- },
317
- iconWrapperSecondaryHovered: {
318
- backgroundColor: theme.color.bg.icon.secondaryHover,
319
- color: theme.color.text.icon.secondaryHover,
320
- },
321
- endIconWrapper: {
322
- marginLeft: theme.padding.small,
323
- marginRight: theme.margin.icon.offset,
324
- },
325
- endIconWrapperTertiary: {
326
- marginRight: 0,
327
- },
328
- });
329
-
330
- const styles: Record<string, any> = {};
331
-
332
- // export for testing only
333
- export const _generateStyles = (
334
- buttonColor = "default",
335
- kind: "primary" | "secondary" | "tertiary",
336
- light: boolean,
337
- size: "large" | "medium" | "small",
338
- theme: ButtonThemeContract,
339
- themeName: string,
340
- ) => {
341
- const color: string =
342
- buttonColor === "destructive"
343
- ? theme.color.bg.critical.default
344
- : theme.color.bg.action.default;
345
-
346
- const buttonType = `${color}-${kind}-${light}-${size}-${themeName}`;
347
-
348
- if (styles[buttonType]) {
349
- return styles[buttonType];
350
- }
351
-
352
- const fadedColor =
353
- buttonColor === "destructive"
354
- ? theme.color.bg.critical.inverse
355
- : theme.color.bg.action.inverse;
356
- const activeColor =
357
- buttonColor === "destructive"
358
- ? theme.color.bg.critical.active
359
- : theme.color.bg.action.active;
360
- const padding =
361
- size === "large" ? theme.padding.xLarge : theme.padding.large;
362
-
363
- let newStyles: Record<string, CSSProperties> = {};
364
- if (kind === "primary") {
365
- const boxShadowInnerColor: string = light
366
- ? theme.color.bg.primary.inverse
367
- : theme.color.bg.primary.default;
368
-
369
- newStyles = {
370
- default: {
371
- background: light ? theme.color.bg.primary.default : color,
372
- color: light ? color : theme.color.text.inverse,
373
- paddingLeft: padding,
374
- paddingRight: padding,
375
- },
376
- focus: {
377
- // This assumes a background of white for the regular button and
378
- // a background of darkBlue for the light version. The inner
379
- // box shadow/ring is also small enough for a slight variation
380
- // in the background color not to matter too much.
381
- boxShadow: `0 0 0 1px ${boxShadowInnerColor}, 0 0 0 3px ${
382
- light ? theme.color.bg.primary.default : color
383
- }`,
384
- },
385
- active: {
386
- boxShadow: `0 0 0 1px ${boxShadowInnerColor}, 0 0 0 3px ${
387
- light ? fadedColor : activeColor
388
- }`,
389
- background: light ? fadedColor : activeColor,
390
- color: light ? activeColor : fadedColor,
391
- },
392
- disabled: {
393
- background: light
394
- ? fadedColor
395
- : theme.color.bg.primary.disabled,
396
- color: light ? color : theme.color.text.primary.disabled,
397
- cursor: "default",
398
- ":focus": {
399
- boxShadow: `0 0 0 1px ${
400
- light
401
- ? theme.color.bg.primary.disabled
402
- : theme.color.bg.primary.default
403
- }, 0 0 0 3px ${
404
- light ? fadedColor : theme.color.bg.primary.disabled
405
- }`,
406
- },
407
- },
408
- };
409
- } else if (kind === "secondary") {
410
- const secondaryBorderColor =
411
- buttonColor === "destructive"
412
- ? theme.color.border.secondary.critical
413
- : theme.color.border.secondary.action;
414
- const secondaryActiveColor =
415
- buttonColor === "destructive"
416
- ? theme.color.bg.secondary.active.critical
417
- : theme.color.bg.secondary.active.action;
418
-
419
- newStyles = {
420
- default: {
421
- background: light
422
- ? theme.color.bg.secondary.inverse
423
- : theme.color.bg.secondary.default,
424
- color: light ? theme.color.text.inverse : color,
425
- borderColor: light
426
- ? theme.color.border.secondary.inverse
427
- : secondaryBorderColor,
428
- borderStyle: "solid",
429
- borderWidth: theme.border.width.secondary,
430
- paddingLeft: padding,
431
- paddingRight: padding,
432
- },
433
- focus: {
434
- background: light
435
- ? theme.color.bg.secondary.inverse
436
- : theme.color.bg.secondary.focus,
437
- borderColor: "transparent",
438
- outlineColor: light
439
- ? theme.color.border.primary.inverse
440
- : color,
441
- outlineStyle: "solid",
442
- outlineWidth: theme.border.width.focused,
443
- },
444
-
445
- active: {
446
- background: light ? activeColor : secondaryActiveColor,
447
- color: light ? fadedColor : activeColor,
448
- borderColor: "transparent",
449
- outlineColor: light ? fadedColor : activeColor,
450
- outlineStyle: "solid",
451
- outlineWidth: theme.border.width.focused,
452
- },
453
- disabled: {
454
- color: light
455
- ? theme.color.text.secondary.inverse
456
- : theme.color.text.disabled,
457
- outlineColor: light ? fadedColor : theme.color.border.disabled,
458
- cursor: "default",
459
- ":focus": {
460
- outlineColor: light
461
- ? theme.color.border.secondary.inverse
462
- : theme.color.border.disabled,
463
- outlineStyle: "solid",
464
- outlineWidth: theme.border.width.disabled,
465
- },
466
- },
467
- };
468
- } else if (kind === "tertiary") {
469
- newStyles = {
470
- default: {
471
- background: "none",
472
- color: light ? theme.color.text.inverse : color,
473
- paddingLeft: 0,
474
- paddingRight: 0,
475
- },
476
- hover: {
477
- ":after": {
478
- content: "''",
479
- position: "absolute",
480
- height: theme.size.height.tertiaryHover,
481
- width: "100%",
482
- right: 0,
483
- bottom: 0,
484
- background: light ? theme.color.bg.tertiary.hover : color,
485
- borderRadius: theme.border.radius.tertiary,
486
- },
487
- },
488
- focus: {
489
- outlineStyle: "solid",
490
- outlineColor: light
491
- ? theme.color.border.tertiary.inverse
492
- : color,
493
- outlineWidth: theme.border.width.focused,
494
- borderRadius: theme.border.radius.default,
495
- },
496
- active: {
497
- color: light ? fadedColor : activeColor,
498
- ":after": {
499
- height: theme.size.height.tertiaryHover,
500
- background: light ? fadedColor : activeColor,
501
- },
502
- },
503
- disabled: {
504
- color: light ? fadedColor : theme.color.text.disabled,
505
- cursor: "default",
506
- },
507
- disabledFocus: {
508
- outlineColor: light
509
- ? theme.color.border.tertiary.inverse
510
- : theme.color.border.disabled,
511
- },
512
- };
513
- } else {
514
- throw new Error("Button kind not recognized");
515
- }
516
-
517
- styles[buttonType] = StyleSheet.create(newStyles);
518
- return styles[buttonType];
519
- };
@@ -1,47 +0,0 @@
1
- import * as React from "react";
2
- import {StyleType} from "@khanacademy/wonder-blocks-core";
3
- import {PhosphorIcon, PhosphorIconAsset} from "@khanacademy/wonder-blocks-icon";
4
-
5
- /**
6
- * Returns the phosphor icon component based on the size. This is necessary
7
- * so we can cast the icon to the correct type.
8
- */
9
- export function ButtonIcon({
10
- icon,
11
- size,
12
- style,
13
- testId,
14
- }: {
15
- icon: PhosphorIconAsset;
16
- size: "small" | "medium";
17
- style?: StyleType;
18
- testId?: string;
19
- }) {
20
- const commonProps = {
21
- "aria-hidden": true,
22
- color: "currentColor",
23
- style: style,
24
- testId,
25
- };
26
-
27
- switch (size) {
28
- case "small":
29
- return (
30
- <PhosphorIcon
31
- {...commonProps}
32
- size="small"
33
- icon={icon as PhosphorBold | PhosphorFill}
34
- />
35
- );
36
-
37
- case "medium":
38
- default:
39
- return (
40
- <PhosphorIcon
41
- {...commonProps}
42
- size="medium"
43
- icon={icon as PhosphorRegular | PhosphorFill}
44
- />
45
- );
46
- }
47
- }