@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.
- package/LICENSE +21 -0
- package/dist/es/index.js +402 -0
- package/dist/index.js +668 -0
- package/dist/index.js.flow +2 -0
- package/docs.md +0 -0
- package/package.json +37 -0
- package/src/__tests__/__snapshots__/custom-snapshot.test.js.snap +8710 -0
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +4774 -0
- package/src/__tests__/custom-snapshot.test.js +117 -0
- package/src/__tests__/generated-snapshot.test.js +727 -0
- package/src/components/__tests__/button.flowtest.js +53 -0
- package/src/components/__tests__/button.test.js +826 -0
- package/src/components/button-core.js +375 -0
- package/src/components/button.js +347 -0
- package/src/components/button.md +810 -0
- package/src/components/button.stories.js +276 -0
- package/src/index.js +6 -0
|
@@ -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
|
+
};
|