@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 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.
@@ -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;