@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Khan Academy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/es/index.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import _extends from '@babel/runtime/helpers/extends';
|
|
2
|
+
import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutPropertiesLoose';
|
|
3
|
+
import { Component, createElement, Fragment } from 'react';
|
|
4
|
+
import { any } from 'prop-types';
|
|
5
|
+
import { isClientSideUrl, getClickableBehavior } from '@khanacademy/wonder-blocks-clickable';
|
|
6
|
+
import { StyleSheet } from 'aphrodite';
|
|
7
|
+
import { Link } from 'react-router-dom';
|
|
8
|
+
import { LabelSmall, LabelLarge } from '@khanacademy/wonder-blocks-typography';
|
|
9
|
+
import Color, { SemanticColor, mix, fade } from '@khanacademy/wonder-blocks-color';
|
|
10
|
+
import { addStyle } from '@khanacademy/wonder-blocks-core';
|
|
11
|
+
import { CircularSpinner } from '@khanacademy/wonder-blocks-progress-spinner';
|
|
12
|
+
import Icon from '@khanacademy/wonder-blocks-icon';
|
|
13
|
+
import Spacing from '@khanacademy/wonder-blocks-spacing';
|
|
14
|
+
|
|
15
|
+
const _excluded = ["children", "skipClientNav", "color", "disabled", "focused", "hovered", "href", "kind", "light", "pressed", "size", "style", "testId", "type", "spinner", "icon", "id", "waiting"];
|
|
16
|
+
const StyledAnchor = addStyle("a");
|
|
17
|
+
const StyledButton = addStyle("button");
|
|
18
|
+
const StyledLink = addStyle(Link);
|
|
19
|
+
class ButtonCore extends Component {
|
|
20
|
+
render() {
|
|
21
|
+
const _this$props = this.props,
|
|
22
|
+
{
|
|
23
|
+
children,
|
|
24
|
+
skipClientNav,
|
|
25
|
+
color,
|
|
26
|
+
disabled: disabledProp,
|
|
27
|
+
focused,
|
|
28
|
+
hovered,
|
|
29
|
+
href = undefined,
|
|
30
|
+
kind,
|
|
31
|
+
light,
|
|
32
|
+
pressed,
|
|
33
|
+
size,
|
|
34
|
+
style,
|
|
35
|
+
testId,
|
|
36
|
+
type = undefined,
|
|
37
|
+
spinner,
|
|
38
|
+
icon,
|
|
39
|
+
id
|
|
40
|
+
} = _this$props,
|
|
41
|
+
restProps = _objectWithoutPropertiesLoose(_this$props, _excluded);
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
router
|
|
45
|
+
} = this.context;
|
|
46
|
+
const buttonColor = color === "destructive" ? SemanticColor.controlDestructive : SemanticColor.controlDefault;
|
|
47
|
+
const iconWidth = icon ? (size === "small" ? 16 : 24) + 8 : 0;
|
|
48
|
+
|
|
49
|
+
const buttonStyles = _generateStyles(buttonColor, kind, light, iconWidth, size);
|
|
50
|
+
|
|
51
|
+
const disabled = spinner || disabledProp;
|
|
52
|
+
const defaultStyle = [sharedStyles.shared, disabled && sharedStyles.disabled, icon && sharedStyles.withIcon, buttonStyles.default, disabled && buttonStyles.disabled, // apply focus effect only to default and secondary buttons
|
|
53
|
+
kind !== "tertiary" && !disabled && (pressed ? buttonStyles.active : (hovered || focused) && buttonStyles.focus), size === "small" && sharedStyles.small, size === "xlarge" && sharedStyles.xlarge];
|
|
54
|
+
|
|
55
|
+
const commonProps = _extends({
|
|
56
|
+
"data-test-id": testId,
|
|
57
|
+
id: id,
|
|
58
|
+
role: "button",
|
|
59
|
+
style: [defaultStyle, style]
|
|
60
|
+
}, restProps);
|
|
61
|
+
|
|
62
|
+
const Label = size === "small" ? LabelSmall : LabelLarge;
|
|
63
|
+
const label = /*#__PURE__*/createElement(Label, {
|
|
64
|
+
style: [sharedStyles.text, size === "xlarge" && sharedStyles.xlargeText, icon && sharedStyles.textWithIcon, spinner && sharedStyles.hiddenText, kind === "tertiary" && sharedStyles.textWithFocus, // apply focus effect on the label instead
|
|
65
|
+
kind === "tertiary" && !disabled && (pressed ? buttonStyles.active : (hovered || focused) && buttonStyles.focus)]
|
|
66
|
+
}, icon && /*#__PURE__*/createElement(Icon, {
|
|
67
|
+
size: size,
|
|
68
|
+
color: "currentColor",
|
|
69
|
+
icon: icon,
|
|
70
|
+
style: sharedStyles.icon
|
|
71
|
+
}), children);
|
|
72
|
+
const contents = /*#__PURE__*/createElement(Fragment, null, label, spinner && /*#__PURE__*/createElement(CircularSpinner, {
|
|
73
|
+
style: sharedStyles.spinner,
|
|
74
|
+
size: {
|
|
75
|
+
medium: "small",
|
|
76
|
+
small: "xsmall",
|
|
77
|
+
xlarge: "medium"
|
|
78
|
+
}[size],
|
|
79
|
+
light: kind === "primary"
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
if (href && !disabled) {
|
|
83
|
+
return router && !skipClientNav && isClientSideUrl(href) ? /*#__PURE__*/createElement(StyledLink, _extends({}, commonProps, {
|
|
84
|
+
to: href
|
|
85
|
+
}), contents) : /*#__PURE__*/createElement(StyledAnchor, _extends({}, commonProps, {
|
|
86
|
+
href: href
|
|
87
|
+
}), contents);
|
|
88
|
+
} else {
|
|
89
|
+
return /*#__PURE__*/createElement(StyledButton, _extends({
|
|
90
|
+
type: type || "button"
|
|
91
|
+
}, commonProps, {
|
|
92
|
+
disabled: disabled
|
|
93
|
+
}), contents);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
ButtonCore.contextTypes = {
|
|
99
|
+
router: any
|
|
100
|
+
};
|
|
101
|
+
const sharedStyles = StyleSheet.create({
|
|
102
|
+
shared: {
|
|
103
|
+
position: "relative",
|
|
104
|
+
display: "inline-flex",
|
|
105
|
+
alignItems: "center",
|
|
106
|
+
justifyContent: "center",
|
|
107
|
+
height: 40,
|
|
108
|
+
paddingTop: 0,
|
|
109
|
+
paddingBottom: 0,
|
|
110
|
+
paddingLeft: 16,
|
|
111
|
+
paddingRight: 16,
|
|
112
|
+
border: "none",
|
|
113
|
+
borderRadius: 4,
|
|
114
|
+
cursor: "pointer",
|
|
115
|
+
outline: "none",
|
|
116
|
+
textDecoration: "none",
|
|
117
|
+
boxSizing: "border-box",
|
|
118
|
+
// This removes the 300ms click delay on mobile browsers by indicating that
|
|
119
|
+
// "double-tap to zoom" shouldn't be used on this element.
|
|
120
|
+
touchAction: "manipulation",
|
|
121
|
+
userSelect: "none",
|
|
122
|
+
":focus": {
|
|
123
|
+
// Mobile: Removes a blue highlight style shown when the user clicks a button
|
|
124
|
+
WebkitTapHighlightColor: "rgba(0,0,0,0)"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
withIcon: {
|
|
128
|
+
// The left padding for the button with icon should have 4px less padding
|
|
129
|
+
paddingLeft: 12
|
|
130
|
+
},
|
|
131
|
+
disabled: {
|
|
132
|
+
cursor: "auto"
|
|
133
|
+
},
|
|
134
|
+
small: {
|
|
135
|
+
height: 32
|
|
136
|
+
},
|
|
137
|
+
xlarge: {
|
|
138
|
+
height: 60
|
|
139
|
+
},
|
|
140
|
+
text: {
|
|
141
|
+
alignItems: "center",
|
|
142
|
+
fontWeight: "bold",
|
|
143
|
+
whiteSpace: "nowrap",
|
|
144
|
+
overflow: "hidden",
|
|
145
|
+
textOverflow: "ellipsis",
|
|
146
|
+
display: "inline-block",
|
|
147
|
+
// allows the button text to truncate
|
|
148
|
+
pointerEvents: "none" // fix Safari bug where the browser was eating mouse events
|
|
149
|
+
|
|
150
|
+
},
|
|
151
|
+
xlargeText: {
|
|
152
|
+
fontSize: 18,
|
|
153
|
+
lineHeight: "20px"
|
|
154
|
+
},
|
|
155
|
+
textWithIcon: {
|
|
156
|
+
display: "flex" // allows the text and icon to sit nicely together
|
|
157
|
+
|
|
158
|
+
},
|
|
159
|
+
textWithFocus: {
|
|
160
|
+
position: "relative" // allows the tertiary button border to use the label width
|
|
161
|
+
|
|
162
|
+
},
|
|
163
|
+
hiddenText: {
|
|
164
|
+
visibility: "hidden"
|
|
165
|
+
},
|
|
166
|
+
spinner: {
|
|
167
|
+
position: "absolute"
|
|
168
|
+
},
|
|
169
|
+
icon: {
|
|
170
|
+
paddingRight: Spacing.xSmall_8
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
const styles = {};
|
|
174
|
+
|
|
175
|
+
const _generateStyles = (color, kind, light, iconWidth, size) => {
|
|
176
|
+
const buttonType = color + kind + light.toString() + iconWidth.toString() + size;
|
|
177
|
+
|
|
178
|
+
if (styles[buttonType]) {
|
|
179
|
+
return styles[buttonType];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const {
|
|
183
|
+
white,
|
|
184
|
+
white50,
|
|
185
|
+
white64,
|
|
186
|
+
offBlack32,
|
|
187
|
+
offBlack50,
|
|
188
|
+
darkBlue
|
|
189
|
+
} = Color;
|
|
190
|
+
const fadedColor = mix(fade(color, 0.32), white);
|
|
191
|
+
const activeColor = mix(offBlack32, color);
|
|
192
|
+
const padding = size === "xlarge" ? Spacing.xLarge_32 : Spacing.medium_16;
|
|
193
|
+
let newStyles = {};
|
|
194
|
+
|
|
195
|
+
if (kind === "primary") {
|
|
196
|
+
newStyles = {
|
|
197
|
+
default: {
|
|
198
|
+
background: light ? white : color,
|
|
199
|
+
color: light ? color : white,
|
|
200
|
+
paddingLeft: padding,
|
|
201
|
+
paddingRight: padding
|
|
202
|
+
},
|
|
203
|
+
focus: {
|
|
204
|
+
// This assumes a background of white for the regular button and
|
|
205
|
+
// a background of darkBlue for the light version. The inner
|
|
206
|
+
// box shadow/ring is also small enough for a slight variation
|
|
207
|
+
// in the background color not to matter too much.
|
|
208
|
+
boxShadow: `0 0 0 1px ${light ? darkBlue : white}, 0 0 0 3px ${light ? white : color}`
|
|
209
|
+
},
|
|
210
|
+
active: {
|
|
211
|
+
boxShadow: `0 0 0 1px ${light ? darkBlue : white}, 0 0 0 3px ${light ? fadedColor : activeColor}`,
|
|
212
|
+
background: light ? fadedColor : activeColor,
|
|
213
|
+
color: light ? activeColor : fadedColor
|
|
214
|
+
},
|
|
215
|
+
disabled: {
|
|
216
|
+
background: light ? fadedColor : offBlack32,
|
|
217
|
+
color: light ? color : white64,
|
|
218
|
+
cursor: "default"
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
} else if (kind === "secondary") {
|
|
222
|
+
newStyles = {
|
|
223
|
+
default: {
|
|
224
|
+
background: "none",
|
|
225
|
+
color: light ? white : color,
|
|
226
|
+
borderColor: light ? white50 : offBlack50,
|
|
227
|
+
borderStyle: "solid",
|
|
228
|
+
borderWidth: 1,
|
|
229
|
+
paddingLeft: iconWidth ? padding - 4 : padding,
|
|
230
|
+
paddingRight: padding
|
|
231
|
+
},
|
|
232
|
+
focus: {
|
|
233
|
+
background: light ? "transparent" : white,
|
|
234
|
+
borderColor: light ? white : color,
|
|
235
|
+
borderWidth: 2,
|
|
236
|
+
// The left padding for the button with icon should have 4px
|
|
237
|
+
// less padding
|
|
238
|
+
paddingLeft: iconWidth ? padding - 5 : padding - 1,
|
|
239
|
+
paddingRight: padding - 1
|
|
240
|
+
},
|
|
241
|
+
active: {
|
|
242
|
+
background: light ? activeColor : fadedColor,
|
|
243
|
+
color: light ? fadedColor : activeColor,
|
|
244
|
+
borderColor: light ? fadedColor : activeColor,
|
|
245
|
+
borderWidth: 2,
|
|
246
|
+
// The left padding for the button with icon should have 4px
|
|
247
|
+
// less padding
|
|
248
|
+
paddingLeft: iconWidth ? padding - 5 : padding - 1,
|
|
249
|
+
paddingRight: padding - 1
|
|
250
|
+
},
|
|
251
|
+
disabled: {
|
|
252
|
+
color: light ? white50 : offBlack32,
|
|
253
|
+
borderColor: light ? fadedColor : offBlack32,
|
|
254
|
+
cursor: "default"
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
} else if (kind === "tertiary") {
|
|
258
|
+
newStyles = {
|
|
259
|
+
default: {
|
|
260
|
+
background: "none",
|
|
261
|
+
color: light ? white : color,
|
|
262
|
+
paddingLeft: 0,
|
|
263
|
+
paddingRight: 0
|
|
264
|
+
},
|
|
265
|
+
focus: {
|
|
266
|
+
":after": {
|
|
267
|
+
content: "''",
|
|
268
|
+
position: "absolute",
|
|
269
|
+
height: 2,
|
|
270
|
+
width: `calc(100% - ${iconWidth}px)`,
|
|
271
|
+
right: 0,
|
|
272
|
+
bottom: 0,
|
|
273
|
+
background: light ? white : color,
|
|
274
|
+
borderRadius: 2
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
active: {
|
|
278
|
+
color: light ? fadedColor : activeColor,
|
|
279
|
+
":after": {
|
|
280
|
+
content: "''",
|
|
281
|
+
position: "absolute",
|
|
282
|
+
height: 2,
|
|
283
|
+
width: `calc(100% - ${iconWidth}px)`,
|
|
284
|
+
right: 0,
|
|
285
|
+
bottom: "calc(50% - 11px)",
|
|
286
|
+
background: light ? fadedColor : activeColor,
|
|
287
|
+
borderRadius: 2
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
disabled: {
|
|
291
|
+
color: light ? fadedColor : offBlack32,
|
|
292
|
+
cursor: "default"
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
} else {
|
|
296
|
+
throw new Error("Button kind not recognized");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
styles[buttonType] = StyleSheet.create(newStyles);
|
|
300
|
+
return styles[buttonType];
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const _excluded$1 = ["href", "type", "children", "skipClientNav", "spinner", "disabled", "onClick", "beforeNav", "safeWithNav", "tabIndex", "target", "rel"],
|
|
304
|
+
_excluded2 = ["tabIndex"];
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Reusable button component.
|
|
308
|
+
*
|
|
309
|
+
* Consisting of a [`ClickableBehavior`](#clickablebehavior) surrounding a
|
|
310
|
+
* `ButtonCore`. `ClickableBehavior` handles interactions and state changes.
|
|
311
|
+
* `ButtonCore` is a stateless component which displays the different states
|
|
312
|
+
* the `Button` can take.
|
|
313
|
+
*
|
|
314
|
+
* Example usage:
|
|
315
|
+
* ```jsx
|
|
316
|
+
* <Button
|
|
317
|
+
* onClick={(e) => console.log("Hello, world!")}
|
|
318
|
+
* >
|
|
319
|
+
* Label
|
|
320
|
+
* </Button>
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
class Button extends Component {
|
|
324
|
+
render() {
|
|
325
|
+
const _this$props = this.props,
|
|
326
|
+
{
|
|
327
|
+
href = undefined,
|
|
328
|
+
type = undefined,
|
|
329
|
+
children,
|
|
330
|
+
skipClientNav,
|
|
331
|
+
spinner,
|
|
332
|
+
disabled,
|
|
333
|
+
onClick,
|
|
334
|
+
beforeNav = undefined,
|
|
335
|
+
safeWithNav = undefined,
|
|
336
|
+
tabIndex,
|
|
337
|
+
target,
|
|
338
|
+
rel
|
|
339
|
+
} = _this$props,
|
|
340
|
+
sharedButtonCoreProps = _objectWithoutPropertiesLoose(_this$props, _excluded$1);
|
|
341
|
+
|
|
342
|
+
const ClickableBehavior = getClickableBehavior(href, skipClientNav, this.context.router);
|
|
343
|
+
|
|
344
|
+
const renderProp = (state, _ref) => {
|
|
345
|
+
let {
|
|
346
|
+
tabIndex: clickableTabIndex
|
|
347
|
+
} = _ref,
|
|
348
|
+
restChildProps = _objectWithoutPropertiesLoose(_ref, _excluded2);
|
|
349
|
+
|
|
350
|
+
return /*#__PURE__*/createElement(ButtonCore, _extends({}, sharedButtonCoreProps, state, restChildProps, {
|
|
351
|
+
disabled: disabled,
|
|
352
|
+
spinner: spinner || state.waiting,
|
|
353
|
+
skipClientNav: skipClientNav,
|
|
354
|
+
href: href,
|
|
355
|
+
target: target,
|
|
356
|
+
type: type // If tabIndex is provide to the component we allow
|
|
357
|
+
// it to override the tabIndex provide to use by
|
|
358
|
+
// ClickableBehavior.
|
|
359
|
+
,
|
|
360
|
+
tabIndex: tabIndex || clickableTabIndex
|
|
361
|
+
}), children);
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
if (beforeNav) {
|
|
365
|
+
return /*#__PURE__*/createElement(ClickableBehavior, {
|
|
366
|
+
disabled: spinner || disabled,
|
|
367
|
+
href: href,
|
|
368
|
+
role: "button",
|
|
369
|
+
type: type,
|
|
370
|
+
onClick: onClick,
|
|
371
|
+
beforeNav: beforeNav,
|
|
372
|
+
safeWithNav: safeWithNav,
|
|
373
|
+
rel: rel
|
|
374
|
+
}, renderProp);
|
|
375
|
+
} else {
|
|
376
|
+
return /*#__PURE__*/createElement(ClickableBehavior, {
|
|
377
|
+
disabled: spinner || disabled,
|
|
378
|
+
href: href,
|
|
379
|
+
role: "button",
|
|
380
|
+
type: type,
|
|
381
|
+
onClick: onClick,
|
|
382
|
+
safeWithNav: safeWithNav,
|
|
383
|
+
target: target,
|
|
384
|
+
rel: rel
|
|
385
|
+
}, renderProp);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
}
|
|
390
|
+
Button.contextTypes = {
|
|
391
|
+
router: any
|
|
392
|
+
};
|
|
393
|
+
Button.defaultProps = {
|
|
394
|
+
color: "default",
|
|
395
|
+
kind: "primary",
|
|
396
|
+
light: false,
|
|
397
|
+
size: "medium",
|
|
398
|
+
disabled: false,
|
|
399
|
+
spinner: false
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
export default Button;
|