@khanacademy/wonder-blocks-button 2.9.13

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.
@@ -0,0 +1,375 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import {StyleSheet} from "aphrodite";
4
+ import {Link} from "react-router-dom";
5
+ import * as PropTypes from "prop-types";
6
+
7
+ import {LabelLarge, LabelSmall} from "@khanacademy/wonder-blocks-typography";
8
+ import Color, {
9
+ SemanticColor,
10
+ mix,
11
+ fade,
12
+ } from "@khanacademy/wonder-blocks-color";
13
+ import {addStyle} from "@khanacademy/wonder-blocks-core";
14
+ import {CircularSpinner} from "@khanacademy/wonder-blocks-progress-spinner";
15
+ import Icon from "@khanacademy/wonder-blocks-icon";
16
+ import Spacing from "@khanacademy/wonder-blocks-spacing";
17
+ import {isClientSideUrl} from "@khanacademy/wonder-blocks-clickable";
18
+
19
+ import type {
20
+ ChildrenProps,
21
+ ClickableState,
22
+ } from "@khanacademy/wonder-blocks-clickable";
23
+ import type {SharedProps} from "./button.js";
24
+
25
+ type Props = {|
26
+ ...SharedProps,
27
+ ...ChildrenProps,
28
+ ...ClickableState,
29
+ href?: string,
30
+ type?: "submit",
31
+ |};
32
+
33
+ type ContextTypes = {|
34
+ router: $FlowFixMe,
35
+ |};
36
+
37
+ const StyledAnchor = addStyle<"a">("a");
38
+ const StyledButton = addStyle<"button">("button");
39
+ const StyledLink = addStyle<typeof Link>(Link);
40
+
41
+ export default class ButtonCore extends React.Component<Props> {
42
+ static contextTypes: ContextTypes = {router: PropTypes.any};
43
+
44
+ render(): React.Node {
45
+ const {
46
+ children,
47
+ skipClientNav,
48
+ color,
49
+ disabled: disabledProp,
50
+ focused,
51
+ hovered,
52
+ href = undefined,
53
+ kind,
54
+ light,
55
+ pressed,
56
+ size,
57
+ style,
58
+ testId,
59
+ type = undefined,
60
+ spinner,
61
+ icon,
62
+ id,
63
+ waiting: _,
64
+ ...restProps
65
+ } = this.props;
66
+ const {router} = this.context;
67
+
68
+ const buttonColor =
69
+ color === "destructive"
70
+ ? SemanticColor.controlDestructive
71
+ : SemanticColor.controlDefault;
72
+
73
+ const iconWidth = icon ? (size === "small" ? 16 : 24) + 8 : 0;
74
+ const buttonStyles = _generateStyles(
75
+ buttonColor,
76
+ kind,
77
+ light,
78
+ iconWidth,
79
+ size,
80
+ );
81
+
82
+ const disabled = spinner || disabledProp;
83
+
84
+ const defaultStyle = [
85
+ sharedStyles.shared,
86
+ disabled && sharedStyles.disabled,
87
+ icon && sharedStyles.withIcon,
88
+ buttonStyles.default,
89
+ disabled && buttonStyles.disabled,
90
+ // apply focus effect only to default and secondary buttons
91
+ kind !== "tertiary" &&
92
+ !disabled &&
93
+ (pressed
94
+ ? buttonStyles.active
95
+ : (hovered || focused) && buttonStyles.focus),
96
+ size === "small" && sharedStyles.small,
97
+ size === "xlarge" && sharedStyles.xlarge,
98
+ ];
99
+
100
+ const commonProps = {
101
+ "data-test-id": testId,
102
+ id: id,
103
+ role: "button",
104
+ style: [defaultStyle, style],
105
+ ...restProps,
106
+ };
107
+
108
+ const Label = size === "small" ? LabelSmall : LabelLarge;
109
+
110
+ const label = (
111
+ <Label
112
+ style={[
113
+ sharedStyles.text,
114
+ size === "xlarge" && sharedStyles.xlargeText,
115
+ icon && sharedStyles.textWithIcon,
116
+ spinner && sharedStyles.hiddenText,
117
+ kind === "tertiary" && sharedStyles.textWithFocus,
118
+ // apply focus effect on the label instead
119
+ kind === "tertiary" &&
120
+ !disabled &&
121
+ (pressed
122
+ ? buttonStyles.active
123
+ : (hovered || focused) && buttonStyles.focus),
124
+ ]}
125
+ >
126
+ {icon && (
127
+ <Icon
128
+ size={size}
129
+ color="currentColor"
130
+ icon={icon}
131
+ style={sharedStyles.icon}
132
+ />
133
+ )}
134
+ {children}
135
+ </Label>
136
+ );
137
+
138
+ const contents = (
139
+ <React.Fragment>
140
+ {label}
141
+ {spinner && (
142
+ <CircularSpinner
143
+ style={sharedStyles.spinner}
144
+ size={
145
+ {
146
+ medium: "small",
147
+ small: "xsmall",
148
+ xlarge: "medium",
149
+ }[size]
150
+ }
151
+ light={kind === "primary"}
152
+ />
153
+ )}
154
+ </React.Fragment>
155
+ );
156
+
157
+ if (href && !disabled) {
158
+ return router && !skipClientNav && isClientSideUrl(href) ? (
159
+ <StyledLink {...commonProps} to={href}>
160
+ {contents}
161
+ </StyledLink>
162
+ ) : (
163
+ <StyledAnchor {...commonProps} href={href}>
164
+ {contents}
165
+ </StyledAnchor>
166
+ );
167
+ } else {
168
+ return (
169
+ <StyledButton
170
+ type={type || "button"}
171
+ {...commonProps}
172
+ disabled={disabled}
173
+ >
174
+ {contents}
175
+ </StyledButton>
176
+ );
177
+ }
178
+ }
179
+ }
180
+
181
+ const sharedStyles = StyleSheet.create({
182
+ shared: {
183
+ position: "relative",
184
+ display: "inline-flex",
185
+ alignItems: "center",
186
+ justifyContent: "center",
187
+ height: 40,
188
+ paddingTop: 0,
189
+ paddingBottom: 0,
190
+ paddingLeft: 16,
191
+ paddingRight: 16,
192
+ border: "none",
193
+ borderRadius: 4,
194
+ cursor: "pointer",
195
+ outline: "none",
196
+ textDecoration: "none",
197
+ boxSizing: "border-box",
198
+ // This removes the 300ms click delay on mobile browsers by indicating that
199
+ // "double-tap to zoom" shouldn't be used on this element.
200
+ touchAction: "manipulation",
201
+ userSelect: "none",
202
+ ":focus": {
203
+ // Mobile: Removes a blue highlight style shown when the user clicks a button
204
+ WebkitTapHighlightColor: "rgba(0,0,0,0)",
205
+ },
206
+ },
207
+ withIcon: {
208
+ // The left padding for the button with icon should have 4px less padding
209
+ paddingLeft: 12,
210
+ },
211
+ disabled: {
212
+ cursor: "auto",
213
+ },
214
+ small: {
215
+ height: 32,
216
+ },
217
+ xlarge: {
218
+ height: 60,
219
+ },
220
+ text: {
221
+ alignItems: "center",
222
+ fontWeight: "bold",
223
+ whiteSpace: "nowrap",
224
+ overflow: "hidden",
225
+ textOverflow: "ellipsis",
226
+ display: "inline-block", // allows the button text to truncate
227
+ pointerEvents: "none", // fix Safari bug where the browser was eating mouse events
228
+ },
229
+ xlargeText: {
230
+ fontSize: 18,
231
+ lineHeight: "20px",
232
+ },
233
+ textWithIcon: {
234
+ display: "flex", // allows the text and icon to sit nicely together
235
+ },
236
+ textWithFocus: {
237
+ position: "relative", // allows the tertiary button border to use the label width
238
+ },
239
+ hiddenText: {
240
+ visibility: "hidden",
241
+ },
242
+ spinner: {
243
+ position: "absolute",
244
+ },
245
+ icon: {
246
+ paddingRight: Spacing.xSmall_8,
247
+ },
248
+ });
249
+
250
+ const styles = {};
251
+
252
+ const _generateStyles = (color, kind, light, iconWidth, size) => {
253
+ const buttonType =
254
+ color + kind + light.toString() + iconWidth.toString() + size;
255
+ if (styles[buttonType]) {
256
+ return styles[buttonType];
257
+ }
258
+
259
+ const {white, white50, white64, offBlack32, offBlack50, darkBlue} = Color;
260
+ const fadedColor = mix(fade(color, 0.32), white);
261
+ const activeColor = mix(offBlack32, color);
262
+ const padding = size === "xlarge" ? Spacing.xLarge_32 : Spacing.medium_16;
263
+
264
+ let newStyles = {};
265
+ if (kind === "primary") {
266
+ newStyles = {
267
+ default: {
268
+ background: light ? white : color,
269
+ color: light ? color : white,
270
+ paddingLeft: padding,
271
+ paddingRight: padding,
272
+ },
273
+ focus: {
274
+ // This assumes a background of white for the regular button and
275
+ // a background of darkBlue for the light version. The inner
276
+ // box shadow/ring is also small enough for a slight variation
277
+ // in the background color not to matter too much.
278
+ boxShadow: `0 0 0 1px ${light ? darkBlue : white}, 0 0 0 3px ${
279
+ light ? white : color
280
+ }`,
281
+ },
282
+ active: {
283
+ boxShadow: `0 0 0 1px ${light ? darkBlue : white}, 0 0 0 3px ${
284
+ light ? fadedColor : activeColor
285
+ }`,
286
+ background: light ? fadedColor : activeColor,
287
+ color: light ? activeColor : fadedColor,
288
+ },
289
+ disabled: {
290
+ background: light ? fadedColor : offBlack32,
291
+ color: light ? color : white64,
292
+ cursor: "default",
293
+ },
294
+ };
295
+ } else if (kind === "secondary") {
296
+ newStyles = {
297
+ default: {
298
+ background: "none",
299
+ color: light ? white : color,
300
+ borderColor: light ? white50 : offBlack50,
301
+ borderStyle: "solid",
302
+ borderWidth: 1,
303
+ paddingLeft: iconWidth ? padding - 4 : padding,
304
+ paddingRight: padding,
305
+ },
306
+ focus: {
307
+ background: light ? "transparent" : white,
308
+ borderColor: light ? white : color,
309
+ borderWidth: 2,
310
+ // The left padding for the button with icon should have 4px
311
+ // less padding
312
+ paddingLeft: iconWidth ? padding - 5 : padding - 1,
313
+ paddingRight: padding - 1,
314
+ },
315
+ active: {
316
+ background: light ? activeColor : fadedColor,
317
+ color: light ? fadedColor : activeColor,
318
+ borderColor: light ? fadedColor : activeColor,
319
+ borderWidth: 2,
320
+ // The left padding for the button with icon should have 4px
321
+ // less padding
322
+ paddingLeft: iconWidth ? padding - 5 : padding - 1,
323
+ paddingRight: padding - 1,
324
+ },
325
+ disabled: {
326
+ color: light ? white50 : offBlack32,
327
+ borderColor: light ? fadedColor : offBlack32,
328
+ cursor: "default",
329
+ },
330
+ };
331
+ } else if (kind === "tertiary") {
332
+ newStyles = {
333
+ default: {
334
+ background: "none",
335
+ color: light ? white : color,
336
+ paddingLeft: 0,
337
+ paddingRight: 0,
338
+ },
339
+ focus: {
340
+ ":after": {
341
+ content: "''",
342
+ position: "absolute",
343
+ height: 2,
344
+ width: `calc(100% - ${iconWidth}px)`,
345
+ right: 0,
346
+ bottom: 0,
347
+ background: light ? white : color,
348
+ borderRadius: 2,
349
+ },
350
+ },
351
+ active: {
352
+ color: light ? fadedColor : activeColor,
353
+ ":after": {
354
+ content: "''",
355
+ position: "absolute",
356
+ height: 2,
357
+ width: `calc(100% - ${iconWidth}px)`,
358
+ right: 0,
359
+ bottom: "calc(50% - 11px)",
360
+ background: light ? fadedColor : activeColor,
361
+ borderRadius: 2,
362
+ },
363
+ },
364
+ disabled: {
365
+ color: light ? fadedColor : offBlack32,
366
+ cursor: "default",
367
+ },
368
+ };
369
+ } else {
370
+ throw new Error("Button kind not recognized");
371
+ }
372
+
373
+ styles[buttonType] = StyleSheet.create(newStyles);
374
+ return styles[buttonType];
375
+ };