@khanacademy/wonder-blocks-button 4.0.13 → 4.1.0
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/CHANGELOG.md +30 -0
- package/dist/components/button-core.d.ts +3 -5
- package/dist/components/button-core.js.flow +6 -4
- package/dist/components/button.d.ts +9 -20
- package/dist/components/button.js.flow +12 -19
- package/dist/es/index.js +81 -77
- package/dist/index.js +81 -77
- package/package.json +6 -6
- package/src/__tests__/__snapshots__/custom-snapshot.test.tsx.snap +144 -216
- package/src/components/__tests__/button.test.tsx +19 -0
- package/src/components/button-core.tsx +75 -58
- package/src/components/button.tsx +48 -50
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -2,6 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import {MemoryRouter, Route, Switch} from "react-router-dom";
|
|
3
3
|
import {render, screen, waitFor} from "@testing-library/react";
|
|
4
4
|
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import {icons} from "@khanacademy/wonder-blocks-icon";
|
|
5
6
|
|
|
6
7
|
import Button from "../button";
|
|
7
8
|
|
|
@@ -827,4 +828,22 @@ describe("Button", () => {
|
|
|
827
828
|
}).not.toThrow();
|
|
828
829
|
});
|
|
829
830
|
});
|
|
831
|
+
|
|
832
|
+
describe("button with icon", () => {
|
|
833
|
+
test("icon is displayed when button contains icon", () => {
|
|
834
|
+
// Arrange
|
|
835
|
+
render(
|
|
836
|
+
<Button testId={"button-focus-test"} icon={icons.add}>
|
|
837
|
+
Label
|
|
838
|
+
</Button>,
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
// Act
|
|
842
|
+
const icon = screen.getByTestId("button-focus-test-icon");
|
|
843
|
+
|
|
844
|
+
// Assert
|
|
845
|
+
expect(icon).toBeDefined();
|
|
846
|
+
expect(icon).toHaveAttribute("aria-hidden", "true");
|
|
847
|
+
});
|
|
848
|
+
});
|
|
830
849
|
});
|
|
@@ -27,8 +27,14 @@ const StyledAnchor = addStyle("a");
|
|
|
27
27
|
const StyledButton = addStyle("button");
|
|
28
28
|
const StyledLink = addStyle(Link);
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
>((props: Props, ref) => {
|
|
37
|
+
const renderInner = (router: any): React.ReactNode => {
|
|
32
38
|
const {
|
|
33
39
|
children,
|
|
34
40
|
skipClientNav,
|
|
@@ -37,10 +43,10 @@ export default class ButtonCore extends React.Component<Props> {
|
|
|
37
43
|
focused,
|
|
38
44
|
hovered,
|
|
39
45
|
href = undefined,
|
|
40
|
-
kind,
|
|
41
|
-
light,
|
|
46
|
+
kind = "primary",
|
|
47
|
+
light = false,
|
|
42
48
|
pressed,
|
|
43
|
-
size,
|
|
49
|
+
size = "medium",
|
|
44
50
|
style,
|
|
45
51
|
testId,
|
|
46
52
|
type = undefined,
|
|
@@ -49,7 +55,7 @@ export default class ButtonCore extends React.Component<Props> {
|
|
|
49
55
|
id,
|
|
50
56
|
waiting: _,
|
|
51
57
|
...restProps
|
|
52
|
-
} =
|
|
58
|
+
} = props;
|
|
53
59
|
|
|
54
60
|
const buttonColor =
|
|
55
61
|
color === "destructive"
|
|
@@ -79,6 +85,12 @@ export default class ButtonCore extends React.Component<Props> {
|
|
|
79
85
|
(pressed
|
|
80
86
|
? buttonStyles.active
|
|
81
87
|
: (hovered || focused) && buttonStyles.focus),
|
|
88
|
+
kind === "tertiary" &&
|
|
89
|
+
!pressed &&
|
|
90
|
+
focused && [
|
|
91
|
+
buttonStyles.focus,
|
|
92
|
+
disabled && buttonStyles.disabledFocus,
|
|
93
|
+
],
|
|
82
94
|
size === "small" && sharedStyles.small,
|
|
83
95
|
size === "large" && sharedStyles.large,
|
|
84
96
|
];
|
|
@@ -102,31 +114,17 @@ export default class ButtonCore extends React.Component<Props> {
|
|
|
102
114
|
style={[
|
|
103
115
|
sharedStyles.text,
|
|
104
116
|
size === "large" && sharedStyles.largeText,
|
|
105
|
-
icon && sharedStyles.textWithIcon,
|
|
106
117
|
spinner && sharedStyles.hiddenText,
|
|
107
118
|
kind === "tertiary" && sharedStyles.textWithFocus,
|
|
108
|
-
// apply
|
|
119
|
+
// apply press/hover effects on the label
|
|
109
120
|
kind === "tertiary" &&
|
|
110
121
|
!disabled &&
|
|
111
122
|
(pressed
|
|
112
|
-
? buttonStyles.active
|
|
113
|
-
:
|
|
114
|
-
kind === "tertiary" &&
|
|
115
|
-
disabled &&
|
|
116
|
-
focused && [
|
|
117
|
-
buttonStyles.focus,
|
|
118
|
-
buttonStyles.disabledFocus,
|
|
119
|
-
],
|
|
123
|
+
? [buttonStyles.hover, buttonStyles.active]
|
|
124
|
+
: hovered && buttonStyles.hover),
|
|
120
125
|
]}
|
|
126
|
+
testId={testId ? `${testId}-inner-label` : undefined}
|
|
121
127
|
>
|
|
122
|
-
{icon && (
|
|
123
|
-
<Icon
|
|
124
|
-
size={iconSize}
|
|
125
|
-
color="currentColor"
|
|
126
|
-
icon={icon}
|
|
127
|
-
style={sharedStyles.icon}
|
|
128
|
-
/>
|
|
129
|
-
)}
|
|
130
128
|
{children}
|
|
131
129
|
</Label>
|
|
132
130
|
);
|
|
@@ -139,6 +137,16 @@ export default class ButtonCore extends React.Component<Props> {
|
|
|
139
137
|
|
|
140
138
|
const contents = (
|
|
141
139
|
<React.Fragment>
|
|
140
|
+
{icon && (
|
|
141
|
+
<Icon
|
|
142
|
+
size={iconSize}
|
|
143
|
+
color="currentColor"
|
|
144
|
+
icon={icon}
|
|
145
|
+
style={sharedStyles.icon}
|
|
146
|
+
aria-hidden="true"
|
|
147
|
+
testId={testId ? `${testId}-icon` : undefined}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
142
150
|
{label}
|
|
143
151
|
{spinner && (
|
|
144
152
|
<CircularSpinner
|
|
@@ -153,11 +161,19 @@ export default class ButtonCore extends React.Component<Props> {
|
|
|
153
161
|
|
|
154
162
|
if (href && !disabled) {
|
|
155
163
|
return router && !skipClientNav && isClientSideUrl(href) ? (
|
|
156
|
-
<StyledLink
|
|
164
|
+
<StyledLink
|
|
165
|
+
{...commonProps}
|
|
166
|
+
to={href}
|
|
167
|
+
ref={ref as React.Ref<typeof Link>}
|
|
168
|
+
>
|
|
157
169
|
{contents}
|
|
158
170
|
</StyledLink>
|
|
159
171
|
) : (
|
|
160
|
-
<StyledAnchor
|
|
172
|
+
<StyledAnchor
|
|
173
|
+
{...commonProps}
|
|
174
|
+
href={href}
|
|
175
|
+
ref={ref as React.Ref<HTMLAnchorElement>}
|
|
176
|
+
>
|
|
161
177
|
{contents}
|
|
162
178
|
</StyledAnchor>
|
|
163
179
|
);
|
|
@@ -167,21 +183,22 @@ export default class ButtonCore extends React.Component<Props> {
|
|
|
167
183
|
type={type || "button"}
|
|
168
184
|
{...commonProps}
|
|
169
185
|
aria-disabled={disabled}
|
|
186
|
+
ref={ref as React.Ref<HTMLButtonElement>}
|
|
170
187
|
>
|
|
171
188
|
{contents}
|
|
172
189
|
</StyledButton>
|
|
173
190
|
);
|
|
174
191
|
}
|
|
175
|
-
}
|
|
192
|
+
};
|
|
176
193
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
return (
|
|
195
|
+
<__RouterContext.Consumer>
|
|
196
|
+
{(router) => renderInner(router)}
|
|
197
|
+
</__RouterContext.Consumer>
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
export default ButtonCore;
|
|
185
202
|
|
|
186
203
|
const sharedStyles = StyleSheet.create({
|
|
187
204
|
shared: {
|
|
@@ -236,9 +253,6 @@ const sharedStyles = StyleSheet.create({
|
|
|
236
253
|
fontSize: 18,
|
|
237
254
|
lineHeight: "20px",
|
|
238
255
|
},
|
|
239
|
-
textWithIcon: {
|
|
240
|
-
display: "flex", // allows the text and icon to sit nicely together
|
|
241
|
-
},
|
|
242
256
|
textWithFocus: {
|
|
243
257
|
position: "relative", // allows the tertiary button border to use the label width
|
|
244
258
|
},
|
|
@@ -317,16 +331,14 @@ const _generateStyles = (
|
|
|
317
331
|
borderColor: light ? white50 : offBlack50,
|
|
318
332
|
borderStyle: "solid",
|
|
319
333
|
borderWidth: 1,
|
|
320
|
-
paddingLeft:
|
|
334
|
+
paddingLeft: padding,
|
|
321
335
|
paddingRight: padding,
|
|
322
336
|
},
|
|
323
337
|
focus: {
|
|
324
338
|
background: light ? "transparent" : white,
|
|
325
339
|
borderColor: light ? white : color,
|
|
326
340
|
borderWidth: 2,
|
|
327
|
-
|
|
328
|
-
// less padding
|
|
329
|
-
paddingLeft: iconWidth ? padding - 5 : padding - 1,
|
|
341
|
+
paddingLeft: padding - 1,
|
|
330
342
|
paddingRight: padding - 1,
|
|
331
343
|
},
|
|
332
344
|
active: {
|
|
@@ -336,9 +348,7 @@ const _generateStyles = (
|
|
|
336
348
|
borderWidth: 2,
|
|
337
349
|
// We need to reduce padding to offset the difference
|
|
338
350
|
// caused by the border becoming thicker on focus.
|
|
339
|
-
|
|
340
|
-
// less padding
|
|
341
|
-
paddingLeft: iconWidth ? padding - 5 : padding - 1,
|
|
351
|
+
paddingLeft: padding - 1,
|
|
342
352
|
paddingRight: padding - 1,
|
|
343
353
|
},
|
|
344
354
|
disabled: {
|
|
@@ -350,9 +360,7 @@ const _generateStyles = (
|
|
|
350
360
|
borderWidth: 2,
|
|
351
361
|
// We need to reduce padding to offset the difference
|
|
352
362
|
// caused by the border becoming thicker on focus.
|
|
353
|
-
|
|
354
|
-
// less padding
|
|
355
|
-
paddingLeft: iconWidth ? padding - 5 : padding - 1,
|
|
363
|
+
paddingLeft: padding - 1,
|
|
356
364
|
paddingRight: padding - 1,
|
|
357
365
|
},
|
|
358
366
|
},
|
|
@@ -365,29 +373,38 @@ const _generateStyles = (
|
|
|
365
373
|
paddingLeft: 0,
|
|
366
374
|
paddingRight: 0,
|
|
367
375
|
},
|
|
368
|
-
|
|
376
|
+
hover: {
|
|
369
377
|
":after": {
|
|
370
378
|
content: "''",
|
|
371
379
|
position: "absolute",
|
|
372
380
|
height: 2,
|
|
373
|
-
width:
|
|
381
|
+
width: "100%",
|
|
374
382
|
right: 0,
|
|
375
383
|
bottom: 0,
|
|
376
384
|
background: light ? white : color,
|
|
377
385
|
borderRadius: 2,
|
|
378
386
|
},
|
|
379
387
|
},
|
|
380
|
-
|
|
381
|
-
color: light ? fadedColor : activeColor,
|
|
388
|
+
focus: {
|
|
382
389
|
":after": {
|
|
383
390
|
content: "''",
|
|
391
|
+
// Since we are using a pseudo element, we need to manually
|
|
392
|
+
// calculate the width/height and use absolute position to
|
|
393
|
+
// prevent other elements from being shifted around.
|
|
384
394
|
position: "absolute",
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
395
|
+
width: `calc(100% + ${Spacing.xxxSmall_4}px)`,
|
|
396
|
+
height: `calc(100% - ${Spacing.xxxSmall_4}px)`,
|
|
397
|
+
borderStyle: "solid",
|
|
398
|
+
borderColor: light ? white : color,
|
|
399
|
+
borderWidth: Spacing.xxxxSmall_2,
|
|
400
|
+
borderRadius: Spacing.xxxSmall_4,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
active: {
|
|
404
|
+
color: light ? fadedColor : activeColor,
|
|
405
|
+
":after": {
|
|
406
|
+
height: 1,
|
|
389
407
|
background: light ? fadedColor : activeColor,
|
|
390
|
-
borderRadius: 2,
|
|
391
408
|
},
|
|
392
409
|
},
|
|
393
410
|
disabled: {
|
|
@@ -396,7 +413,7 @@ const _generateStyles = (
|
|
|
396
413
|
},
|
|
397
414
|
disabledFocus: {
|
|
398
415
|
":after": {
|
|
399
|
-
|
|
416
|
+
borderColor: light ? white50 : offBlack32,
|
|
400
417
|
},
|
|
401
418
|
},
|
|
402
419
|
};
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
} from "@khanacademy/wonder-blocks-clickable";
|
|
9
9
|
import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
|
|
10
10
|
import type {IconAsset} from "@khanacademy/wonder-blocks-icon";
|
|
11
|
+
import {Link} from "react-router-dom";
|
|
11
12
|
import ButtonCore from "./button-core";
|
|
12
13
|
|
|
13
14
|
export type SharedProps =
|
|
@@ -32,11 +33,11 @@ export type SharedProps =
|
|
|
32
33
|
*
|
|
33
34
|
* TODO(kevinb): support spinner + light once we have designs
|
|
34
35
|
*/
|
|
35
|
-
spinner
|
|
36
|
+
spinner?: boolean;
|
|
36
37
|
/**
|
|
37
38
|
* The color of the button, either blue or red.
|
|
38
39
|
*/
|
|
39
|
-
color
|
|
40
|
+
color?: "default" | "destructive";
|
|
40
41
|
/**
|
|
41
42
|
* The kind of the button, either primary, secondary, or tertiary.
|
|
42
43
|
*
|
|
@@ -46,23 +47,23 @@ export type SharedProps =
|
|
|
46
47
|
* - Secondary buttons have a border and no background color
|
|
47
48
|
* - Tertiary buttons have no background or border
|
|
48
49
|
*/
|
|
49
|
-
kind
|
|
50
|
+
kind?: "primary" | "secondary" | "tertiary";
|
|
50
51
|
/**
|
|
51
52
|
* Whether the button is on a dark/colored background.
|
|
52
53
|
*
|
|
53
54
|
* Sets primary button background color to white, and secondary and
|
|
54
55
|
* tertiary button title to color.
|
|
55
56
|
*/
|
|
56
|
-
light
|
|
57
|
+
light?: boolean;
|
|
57
58
|
/**
|
|
58
59
|
* The size of the button. "medium" = height: 40; "small" = height: 32;
|
|
59
60
|
* "large" = height: 56;
|
|
60
61
|
*/
|
|
61
|
-
size
|
|
62
|
+
size?: "medium" | "small" | "large";
|
|
62
63
|
/**
|
|
63
64
|
* Whether the button is disabled.
|
|
64
65
|
*/
|
|
65
|
-
disabled
|
|
66
|
+
disabled?: boolean;
|
|
66
67
|
/**
|
|
67
68
|
* An optional id attribute.
|
|
68
69
|
*/
|
|
@@ -171,15 +172,6 @@ export type SharedProps =
|
|
|
171
172
|
|
|
172
173
|
type Props = SharedProps;
|
|
173
174
|
|
|
174
|
-
type DefaultProps = {
|
|
175
|
-
color: Props["color"];
|
|
176
|
-
kind: Props["kind"];
|
|
177
|
-
light: Props["light"];
|
|
178
|
-
size: Props["size"];
|
|
179
|
-
disabled: Props["disabled"];
|
|
180
|
-
spinner: Props["spinner"];
|
|
181
|
-
};
|
|
182
|
-
|
|
183
175
|
/**
|
|
184
176
|
* Reusable button component.
|
|
185
177
|
*
|
|
@@ -200,33 +192,34 @@ type DefaultProps = {
|
|
|
200
192
|
* </Button>
|
|
201
193
|
* ```
|
|
202
194
|
*/
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
195
|
+
const Button: React.ForwardRefExoticComponent<
|
|
196
|
+
Props &
|
|
197
|
+
React.RefAttributes<typeof Link | HTMLButtonElement | HTMLAnchorElement>
|
|
198
|
+
> = React.forwardRef<
|
|
199
|
+
typeof Link | HTMLButtonElement | HTMLAnchorElement,
|
|
200
|
+
Props
|
|
201
|
+
>((props: Props, ref) => {
|
|
202
|
+
const {
|
|
203
|
+
href = undefined,
|
|
204
|
+
type = undefined,
|
|
205
|
+
children,
|
|
206
|
+
skipClientNav,
|
|
207
|
+
onClick,
|
|
208
|
+
beforeNav = undefined,
|
|
209
|
+
safeWithNav = undefined,
|
|
210
|
+
tabIndex,
|
|
211
|
+
target,
|
|
212
|
+
rel,
|
|
213
|
+
color = "default",
|
|
214
|
+
kind = "primary",
|
|
215
|
+
light = false,
|
|
216
|
+
size = "medium",
|
|
217
|
+
disabled = false,
|
|
218
|
+
spinner = false,
|
|
219
|
+
...sharedButtonCoreProps
|
|
220
|
+
} = props;
|
|
229
221
|
|
|
222
|
+
const renderClickableBehavior = (router: any): React.ReactNode => {
|
|
230
223
|
const ClickableBehavior = getClickableBehavior(
|
|
231
224
|
href,
|
|
232
225
|
skipClientNav,
|
|
@@ -244,11 +237,16 @@ export default class Button extends React.Component<Props> {
|
|
|
244
237
|
{...restChildProps}
|
|
245
238
|
disabled={disabled}
|
|
246
239
|
spinner={spinner || state.waiting}
|
|
240
|
+
color={color}
|
|
241
|
+
kind={kind}
|
|
242
|
+
light={light}
|
|
243
|
+
size={size}
|
|
247
244
|
skipClientNav={skipClientNav}
|
|
248
245
|
href={href}
|
|
249
246
|
target={target}
|
|
250
247
|
type={type}
|
|
251
248
|
tabIndex={tabIndex}
|
|
249
|
+
ref={ref}
|
|
252
250
|
>
|
|
253
251
|
{children}
|
|
254
252
|
</ButtonCore>
|
|
@@ -286,13 +284,13 @@ export default class Button extends React.Component<Props> {
|
|
|
286
284
|
</ClickableBehavior>
|
|
287
285
|
);
|
|
288
286
|
}
|
|
289
|
-
}
|
|
287
|
+
};
|
|
290
288
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
289
|
+
return (
|
|
290
|
+
<__RouterContext.Consumer>
|
|
291
|
+
{(router) => renderClickableBehavior(router)}
|
|
292
|
+
</__RouterContext.Consumer>
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
export default Button;
|