@khanacademy/wonder-blocks-banner 0.1.1 → 1.0.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 +24 -0
- package/dist/es/index.js +216 -2
- package/dist/index.js +5801 -4
- package/package.json +3 -3
- package/src/components/__docs__/banner.argtypes.js +89 -0
- package/src/components/__docs__/banner.stories.js +468 -0
- package/src/components/__tests__/banner.test.js +343 -0
- package/src/components/banner-icons.js +20 -0
- package/src/components/banner.js +341 -0
- package/src/index.js +2 -2
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {render, screen} from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import Banner from "../banner.js";
|
|
6
|
+
|
|
7
|
+
describe("Banner", () => {
|
|
8
|
+
test("having no `onDismiss` prop causes the 'X' button NOT to appear", () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
|
|
11
|
+
// Act
|
|
12
|
+
render(<Banner text="" layout="floating" />);
|
|
13
|
+
|
|
14
|
+
// Assert
|
|
15
|
+
const button = screen.queryByRole("button");
|
|
16
|
+
expect(button).not.toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("passing in `onDismiss` prop causes 'X' button to appear", () => {
|
|
20
|
+
// Arrange
|
|
21
|
+
|
|
22
|
+
// Act
|
|
23
|
+
render(<Banner text="" onDismiss={() => {}} layout="floating" />);
|
|
24
|
+
|
|
25
|
+
// Assert
|
|
26
|
+
const button = screen.queryByRole("button");
|
|
27
|
+
expect(button).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("clicking the dismiss button triggers `onDismiss`", () => {
|
|
31
|
+
// Arrange
|
|
32
|
+
const onDismissSpy = jest.fn();
|
|
33
|
+
render(<Banner text="" onDismiss={onDismissSpy} layout="floating" />);
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
const button = screen.getByRole("button");
|
|
37
|
+
button.click();
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(onDismissSpy).toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("passing an href into an `actions` element causes a Link to appear", () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
|
|
46
|
+
// Act
|
|
47
|
+
render(
|
|
48
|
+
<Banner
|
|
49
|
+
text=""
|
|
50
|
+
layout="floating"
|
|
51
|
+
actions={[{title: "some link", href: "/", onClick: () => {}}]}
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
const link = screen.getByRole("link");
|
|
57
|
+
expect(link).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("passing an onClick without href into an `actions` element causes a Button to appear", () => {
|
|
61
|
+
// Arrange
|
|
62
|
+
|
|
63
|
+
// Act
|
|
64
|
+
render(
|
|
65
|
+
<Banner
|
|
66
|
+
text=""
|
|
67
|
+
layout="floating"
|
|
68
|
+
actions={[{title: "some button", onClick: () => {}}]}
|
|
69
|
+
/>,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
73
|
+
const link = screen.getByRole("button");
|
|
74
|
+
expect(link).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("passing multiple actions causes multiple actions to appear", async () => {
|
|
78
|
+
// Arrange
|
|
79
|
+
|
|
80
|
+
// Act
|
|
81
|
+
render(
|
|
82
|
+
<Banner
|
|
83
|
+
text=""
|
|
84
|
+
layout="floating"
|
|
85
|
+
actions={[
|
|
86
|
+
{title: "button 1", onClick: () => {}},
|
|
87
|
+
{title: "button 2", onClick: () => {}},
|
|
88
|
+
{title: "button 3", onClick: () => {}},
|
|
89
|
+
]}
|
|
90
|
+
/>,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
const buttons = await screen.findAllByRole("button");
|
|
95
|
+
expect(buttons).toHaveLength(3);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("clicking a button triggers the onClick from the action prop", () => {
|
|
99
|
+
// Arrange
|
|
100
|
+
const actionSpy = jest.fn();
|
|
101
|
+
|
|
102
|
+
render(
|
|
103
|
+
<Banner
|
|
104
|
+
text=""
|
|
105
|
+
layout="floating"
|
|
106
|
+
actions={[{title: "a button", onClick: actionSpy}]}
|
|
107
|
+
/>,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Act
|
|
111
|
+
const button = screen.getByRole("button");
|
|
112
|
+
button.click();
|
|
113
|
+
|
|
114
|
+
// Assert
|
|
115
|
+
expect(actionSpy).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("clicking a link triggers the onClick from the action prop", () => {
|
|
119
|
+
// Arrange
|
|
120
|
+
const actionSpy = jest.fn();
|
|
121
|
+
|
|
122
|
+
render(
|
|
123
|
+
<Banner
|
|
124
|
+
text=""
|
|
125
|
+
layout="floating"
|
|
126
|
+
actions={[{title: "a link", onClick: actionSpy, href: "/"}]}
|
|
127
|
+
/>,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Act
|
|
131
|
+
const link = screen.getByRole("link");
|
|
132
|
+
link.click();
|
|
133
|
+
|
|
134
|
+
// Assert
|
|
135
|
+
expect(actionSpy).toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("action href becomes link href", () => {
|
|
139
|
+
// Arrange
|
|
140
|
+
|
|
141
|
+
render(
|
|
142
|
+
<Banner
|
|
143
|
+
text=""
|
|
144
|
+
layout="floating"
|
|
145
|
+
actions={[{title: "a button", onClick: () => {}, href: "/foo"}]}
|
|
146
|
+
/>,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Act
|
|
150
|
+
const link = screen.getByRole("link");
|
|
151
|
+
|
|
152
|
+
// Assert
|
|
153
|
+
expect(link).toHaveAttribute("href", "/foo");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Test kind
|
|
157
|
+
|
|
158
|
+
test("default kind displays info icon", () => {
|
|
159
|
+
// Arrange
|
|
160
|
+
|
|
161
|
+
// Act
|
|
162
|
+
render(<Banner text="" layout="floating" />);
|
|
163
|
+
|
|
164
|
+
// Assert
|
|
165
|
+
const icon = screen.getByTestId("banner-kind-icon");
|
|
166
|
+
expect(icon).toHaveAttribute("aria-label", "info");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it.each(["info", "success", "warning", "critical"])(
|
|
170
|
+
"%s kind displays %s icon",
|
|
171
|
+
(kind) => {
|
|
172
|
+
// Arrange
|
|
173
|
+
|
|
174
|
+
// Act
|
|
175
|
+
render(<Banner text="" kind={kind} layout="floating" />);
|
|
176
|
+
|
|
177
|
+
// Assert
|
|
178
|
+
const icon = screen.getByTestId("banner-kind-icon");
|
|
179
|
+
expect(icon).toHaveAttribute("aria-label", kind);
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Test accessibility
|
|
184
|
+
|
|
185
|
+
test("dismiss button has aria label by default", () => {
|
|
186
|
+
// Arrange
|
|
187
|
+
|
|
188
|
+
// Act
|
|
189
|
+
render(<Banner text="" layout="floating" onDismiss={() => {}} />);
|
|
190
|
+
|
|
191
|
+
// Assert
|
|
192
|
+
const dismissButton = screen.getByRole("button");
|
|
193
|
+
expect(dismissButton).toHaveAttribute("aria-label", "Dismiss banner.");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("dismiss button has the aria label that was passed in", () => {
|
|
197
|
+
// Arrange
|
|
198
|
+
|
|
199
|
+
// Act
|
|
200
|
+
render(
|
|
201
|
+
<Banner
|
|
202
|
+
text=""
|
|
203
|
+
layout="floating"
|
|
204
|
+
onDismiss={() => {}}
|
|
205
|
+
dismissAriaLabel="Test dismiss aria label"
|
|
206
|
+
/>,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Assert
|
|
210
|
+
const dismissButton = screen.getByRole("button");
|
|
211
|
+
expect(dismissButton).toHaveAttribute(
|
|
212
|
+
"aria-label",
|
|
213
|
+
"Test dismiss aria label",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("buttons have their title as the aria label by default", () => {
|
|
218
|
+
// Arrange
|
|
219
|
+
|
|
220
|
+
// Act
|
|
221
|
+
render(
|
|
222
|
+
<Banner
|
|
223
|
+
text=""
|
|
224
|
+
layout="floating"
|
|
225
|
+
actions={[{title: "Test button title", onClick: () => {}}]}
|
|
226
|
+
/>,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Assert
|
|
230
|
+
const actionButton = screen.getByRole("button");
|
|
231
|
+
expect(actionButton).toHaveAttribute("aria-label", "Test button title");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("links have their title as the aria label by default", () => {
|
|
235
|
+
// Arrange
|
|
236
|
+
|
|
237
|
+
// Act
|
|
238
|
+
render(
|
|
239
|
+
<Banner
|
|
240
|
+
text=""
|
|
241
|
+
layout="floating"
|
|
242
|
+
actions={[{title: "Test link title", href: "/"}]}
|
|
243
|
+
/>,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Assert
|
|
247
|
+
const actionLink = screen.getByRole("link");
|
|
248
|
+
expect(actionLink).toHaveAttribute("aria-label", "Test link title");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("buttons use the passed in aria label", () => {
|
|
252
|
+
// Arrange
|
|
253
|
+
|
|
254
|
+
// Act
|
|
255
|
+
render(
|
|
256
|
+
<Banner
|
|
257
|
+
text=""
|
|
258
|
+
layout="floating"
|
|
259
|
+
actions={[
|
|
260
|
+
{
|
|
261
|
+
title: "Test button title",
|
|
262
|
+
onClick: () => {},
|
|
263
|
+
ariaLabel: "Button aria label passed in",
|
|
264
|
+
},
|
|
265
|
+
]}
|
|
266
|
+
/>,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Assert
|
|
270
|
+
const actionButton = screen.getByRole("button");
|
|
271
|
+
expect(actionButton).toHaveAttribute(
|
|
272
|
+
"aria-label",
|
|
273
|
+
"Button aria label passed in",
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("links use the passed in aria label", () => {
|
|
278
|
+
// Arrange
|
|
279
|
+
|
|
280
|
+
// Act
|
|
281
|
+
render(
|
|
282
|
+
<Banner
|
|
283
|
+
text=""
|
|
284
|
+
layout="floating"
|
|
285
|
+
actions={[
|
|
286
|
+
{
|
|
287
|
+
title: "Test link title",
|
|
288
|
+
href: "/",
|
|
289
|
+
ariaLabel: "Link aria label passed in",
|
|
290
|
+
},
|
|
291
|
+
]}
|
|
292
|
+
/>,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Assert
|
|
296
|
+
const actionLink = screen.getByRole("link");
|
|
297
|
+
expect(actionLink).toHaveAttribute(
|
|
298
|
+
"aria-label",
|
|
299
|
+
"Link aria label passed in",
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it.each([
|
|
304
|
+
["info", "status"],
|
|
305
|
+
["success", "status"],
|
|
306
|
+
["warning", "alert"],
|
|
307
|
+
["critical", "alert"],
|
|
308
|
+
])("%s banners have role: %s", (kind, role) => {
|
|
309
|
+
// Arrange
|
|
310
|
+
|
|
311
|
+
// Act
|
|
312
|
+
render(
|
|
313
|
+
<Banner
|
|
314
|
+
text=""
|
|
315
|
+
kind={kind}
|
|
316
|
+
layout="floating"
|
|
317
|
+
testId="wonder-blocks-banner-test-id"
|
|
318
|
+
/>,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// Assert
|
|
322
|
+
const banner = screen.getByTestId("wonder-blocks-banner-test-id");
|
|
323
|
+
expect(banner).toHaveAttribute("role", role);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("warning banners have aria-live polite", () => {
|
|
327
|
+
// Arrange
|
|
328
|
+
|
|
329
|
+
// Act
|
|
330
|
+
render(
|
|
331
|
+
<Banner
|
|
332
|
+
text=""
|
|
333
|
+
kind={"warning"}
|
|
334
|
+
layout="floating"
|
|
335
|
+
testId="wonder-blocks-banner-test-id"
|
|
336
|
+
/>,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Assert
|
|
340
|
+
const banner = screen.getByTestId("wonder-blocks-banner-test-id");
|
|
341
|
+
expect(banner).toHaveAttribute("aria-live", "polite");
|
|
342
|
+
});
|
|
343
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
// TODO(WB-1409): Use Phosphor icons instead of custom svgs. Also, use
|
|
3
|
+
// Wonder Blocks Icon instead of img in the render function.
|
|
4
|
+
import {type IconAsset} from "@khanacademy/wonder-blocks-icon";
|
|
5
|
+
|
|
6
|
+
export const info: IconAsset = {
|
|
7
|
+
medium: "M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM10.25 11.25C10.25 10.6977 10.6977 10.25 11.25 10.25H12C12.5523 10.25 13 10.6977 13 11.25V15.5315C13.4313 15.6425 13.75 16.034 13.75 16.5C13.75 17.0523 13.3023 17.5 12.75 17.5H12C11.4477 17.5 11 17.0523 11 16.5V12.2185C10.5687 12.1075 10.25 11.716 10.25 11.25ZM12.9375 7.875C12.9375 8.49632 12.4338 9 11.8125 9C11.1912 9 10.6875 8.49632 10.6875 7.875C10.6875 7.25368 11.1912 6.75 11.8125 6.75C12.4338 6.75 12.9375 7.25368 12.9375 7.875Z",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const success: IconAsset = {
|
|
11
|
+
medium: "M4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12ZM12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM9.75 10.125C9.75 10.7463 9.24632 11.25 8.625 11.25C8.00368 11.25 7.5 10.7463 7.5 10.125C7.5 9.50368 8.00368 9 8.625 9C9.24632 9 9.75 9.50368 9.75 10.125ZM15.375 11.25C15.9963 11.25 16.5 10.7463 16.5 10.125C16.5 9.50368 15.9963 9 15.375 9C14.7537 9 14.25 9.50368 14.25 10.125C14.25 10.7463 14.7537 11.25 15.375 11.25ZM8.96488 13.7479C8.68763 13.2703 8.07567 13.1078 7.59801 13.3851C7.12036 13.6623 6.9579 14.2743 7.23515 14.7519C7.71955 15.5865 8.41464 16.2791 9.25086 16.7606C10.0871 17.2421 11.0351 17.4956 12 17.4956C12.965 17.4956 13.913 17.2421 14.7492 16.7606C15.5854 16.2791 16.2805 15.5865 16.7649 14.7519C17.0421 14.2743 16.8797 13.6623 16.402 13.3851C15.9244 13.1078 15.3124 13.2703 15.0352 13.7479C14.7266 14.2795 14.2838 14.7207 13.7512 15.0274C13.2185 15.3341 12.6147 15.4956 12 15.4956C11.3854 15.4956 10.7815 15.3341 10.2489 15.0274C9.7162 14.7207 9.27344 14.2795 8.96488 13.7479Z",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const warning: IconAsset = {
|
|
15
|
+
medium: "M10.7505 2.33557C11.1301 2.11544 11.5612 1.99951 12 1.99951C12.4388 1.99951 12.8698 2.11544 13.2494 2.33557C13.6282 2.55524 13.9424 2.87089 14.1603 3.25068L14.1616 3.25305L22.4097 17.4997C22.6288 17.8791 22.7443 18.3094 22.7447 18.7475C22.7451 19.1856 22.6303 19.6161 22.412 19.9959C22.1936 20.3757 21.8792 20.6915 21.5004 20.9115C21.1216 21.1316 20.6916 21.2482 20.2535 21.2497L20.25 21.2497H3.74997L3.74645 21.2497C3.30835 21.2482 2.87835 21.1316 2.49953 20.9115C2.12071 20.6915 1.80636 20.3757 1.58798 19.9959C1.36961 19.6161 1.25486 19.1856 1.25525 18.7475C1.25564 18.3094 1.37114 17.8791 1.5902 17.4997L9.83829 3.25305L9.83965 3.25068C10.0576 2.87089 10.3717 2.55524 10.7505 2.33557ZM10.7062 3.74975L11.5716 4.25079L3.32224 18.4998C3.27857 18.5755 3.25533 18.6618 3.25525 18.7493C3.25517 18.8369 3.27812 18.923 3.3218 18.999C3.36547 19.075 3.42834 19.1381 3.50411 19.1821C3.57952 19.2259 3.66508 19.2492 3.75228 19.2497H20.2477C20.3349 19.2492 20.4204 19.2259 20.4958 19.1821C20.5716 19.1381 20.6345 19.075 20.6781 18.999C20.7218 18.923 20.7448 18.8369 20.7447 18.7493C20.7446 18.6619 20.7216 18.576 20.678 18.5003L12.4258 4.24645C12.3828 4.17143 12.3209 4.10907 12.2461 4.06571C12.1713 4.02235 12.0864 3.99951 12 3.99951C11.9135 3.99951 11.8286 4.02235 11.7538 4.06571C11.6791 4.10907 11.6171 4.17142 11.5741 4.24645L10.7062 3.74975ZM12 8.75C12.5523 8.75 13 9.19772 13 9.75V13.5C13 14.0523 12.5523 14.5 12 14.5C11.4477 14.5 11 14.0523 11 13.5V9.75C11 9.19772 11.4477 8.75 12 8.75ZM13.125 16.875C13.125 17.4963 12.6213 18 12 18C11.3787 18 10.875 17.4963 10.875 16.875C10.875 16.2537 11.3787 15.75 12 15.75C12.6213 15.75 13.125 16.2537 13.125 16.875Z",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const critical: IconAsset = {
|
|
19
|
+
medium: "M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM12 6.5C12.5523 6.5 13 6.94772 13 7.5V12.75C13 13.3023 12.5523 13.75 12 13.75C11.4477 13.75 11 13.3023 11 12.75V7.5C11 6.94772 11.4477 6.5 12 6.5ZM13.125 16.125C13.125 16.7463 12.6213 17.25 12 17.25C11.3787 17.25 10.875 16.7463 10.875 16.125C10.875 15.5037 11.3787 15 12 15C12.6213 15 13.125 15.5037 13.125 16.125Z",
|
|
20
|
+
};
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {StyleSheet} from "aphrodite";
|
|
4
|
+
|
|
5
|
+
import Button from "@khanacademy/wonder-blocks-button";
|
|
6
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
7
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
8
|
+
import Icon, {icons} from "@khanacademy/wonder-blocks-icon";
|
|
9
|
+
import Link from "@khanacademy/wonder-blocks-link";
|
|
10
|
+
import IconButton from "@khanacademy/wonder-blocks-icon-button";
|
|
11
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
12
|
+
import {LabelSmall} from "@khanacademy/wonder-blocks-typography";
|
|
13
|
+
|
|
14
|
+
import * as bannerIcons from "./banner-icons.js";
|
|
15
|
+
|
|
16
|
+
type ActionTriggerBase = {|
|
|
17
|
+
title: string,
|
|
18
|
+
ariaLabel?: string,
|
|
19
|
+
|};
|
|
20
|
+
|
|
21
|
+
type ActionTriggerWithButton = {|
|
|
22
|
+
...ActionTriggerBase,
|
|
23
|
+
onClick: () => void,
|
|
24
|
+
|};
|
|
25
|
+
|
|
26
|
+
type ActionTriggerWithLink = {|
|
|
27
|
+
...ActionTriggerBase,
|
|
28
|
+
href: string,
|
|
29
|
+
onClick?: () => void,
|
|
30
|
+
|};
|
|
31
|
+
|
|
32
|
+
type ActionTrigger = ActionTriggerWithButton | ActionTriggerWithLink;
|
|
33
|
+
|
|
34
|
+
type BannerKind =
|
|
35
|
+
/**
|
|
36
|
+
* Color blue, circle 'i' icon. This is the default.
|
|
37
|
+
*/
|
|
38
|
+
| "info"
|
|
39
|
+
/**
|
|
40
|
+
* Color green, smiley icon
|
|
41
|
+
*/
|
|
42
|
+
| "success"
|
|
43
|
+
/**
|
|
44
|
+
* Color gold, triangle exclamation-point icon
|
|
45
|
+
*/
|
|
46
|
+
| "warning"
|
|
47
|
+
/**
|
|
48
|
+
* Color red, circle exclamation-point icon
|
|
49
|
+
*/
|
|
50
|
+
| "critical";
|
|
51
|
+
|
|
52
|
+
type BannerLayout =
|
|
53
|
+
/**
|
|
54
|
+
* Renders a rounded rectangle, usually for when banner is used as an inset
|
|
55
|
+
* element on a screen (e.g., the SOT card) that appears to be floating
|
|
56
|
+
*/
|
|
57
|
+
| "floating"
|
|
58
|
+
/**
|
|
59
|
+
* Renders a full-width banner, with no rounded corners.
|
|
60
|
+
* This is the default.
|
|
61
|
+
*/
|
|
62
|
+
| "full-width";
|
|
63
|
+
|
|
64
|
+
type BannerValues = {|
|
|
65
|
+
color: string,
|
|
66
|
+
role: "status" | "alert",
|
|
67
|
+
ariaLive?: "assertive" | "polite",
|
|
68
|
+
|};
|
|
69
|
+
|
|
70
|
+
type Props = {|
|
|
71
|
+
/**
|
|
72
|
+
* Determines the color and icon of the banner.
|
|
73
|
+
*/
|
|
74
|
+
kind: BannerKind,
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Determines the edge style of the Banner.
|
|
78
|
+
*/
|
|
79
|
+
layout: BannerLayout,
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Text on the banner (LabelSmall) or a node if you want something different.
|
|
83
|
+
*/
|
|
84
|
+
text: string | React.Node,
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Links or tertiary Buttons that appear to the right of the text.
|
|
88
|
+
*
|
|
89
|
+
* The ActionTrigger must have either an onClick or an href field, or both.
|
|
90
|
+
*/
|
|
91
|
+
actions?: Array<ActionTrigger>,
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* If present, dismiss button is on right side. If not, no button appears.
|
|
95
|
+
*/
|
|
96
|
+
onDismiss?: ?() => void,
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The accessible label for the dismiss button.
|
|
100
|
+
* Please pass in a translated string.
|
|
101
|
+
*/
|
|
102
|
+
dismissAriaLabel: string,
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Test ID used for e2e testing.
|
|
106
|
+
*/
|
|
107
|
+
testId?: string,
|
|
108
|
+
|};
|
|
109
|
+
|
|
110
|
+
const valuesForKind = (kind: BannerKind): BannerValues => {
|
|
111
|
+
switch (kind) {
|
|
112
|
+
case "success":
|
|
113
|
+
return {
|
|
114
|
+
color: Color.green,
|
|
115
|
+
role: "status",
|
|
116
|
+
};
|
|
117
|
+
case "warning":
|
|
118
|
+
return {
|
|
119
|
+
color: Color.gold,
|
|
120
|
+
role: "alert",
|
|
121
|
+
ariaLive: "polite",
|
|
122
|
+
};
|
|
123
|
+
case "critical":
|
|
124
|
+
return {
|
|
125
|
+
color: Color.red,
|
|
126
|
+
role: "alert",
|
|
127
|
+
};
|
|
128
|
+
default:
|
|
129
|
+
return {
|
|
130
|
+
color: Color.blue,
|
|
131
|
+
role: "status",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Banner. A banner displays a prominent message and related optional actions.
|
|
138
|
+
* It can be used as a way of informing the user of important changes.
|
|
139
|
+
* Typically, it is displayed toward the top of the screen.
|
|
140
|
+
*
|
|
141
|
+
* There are two possible layouts for banners - floating and full-width.
|
|
142
|
+
* The `floating` layout is intended to be used when there is whitespace
|
|
143
|
+
* around the banner. The `full-width` layout is intended to be used when
|
|
144
|
+
* the banner needs to be flush with surrounding elements.
|
|
145
|
+
*
|
|
146
|
+
* ### Usage
|
|
147
|
+
* ```jsx
|
|
148
|
+
* import Banner from "@khanacademy/wonder-blocks-banner";
|
|
149
|
+
*
|
|
150
|
+
* <Banner
|
|
151
|
+
* text="Here is some example text."
|
|
152
|
+
* kind="success"
|
|
153
|
+
* layout="floating"
|
|
154
|
+
* actions={[
|
|
155
|
+
* {title: "Button 1", onClick: () => {}},
|
|
156
|
+
* {title: "Button 2", onClick: () => {}},
|
|
157
|
+
* ]}
|
|
158
|
+
* onDismiss={() => {console.log("Has been dismissed.")}}
|
|
159
|
+
* />
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
const Banner = (props: Props): React.Node => {
|
|
163
|
+
const {actions, dismissAriaLabel, onDismiss, kind, layout, text, testId} =
|
|
164
|
+
props;
|
|
165
|
+
const layoutStyle = {
|
|
166
|
+
borderRadius: layout && layout === "full-width" ? 0 : 4,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const renderActions = () => {
|
|
170
|
+
return actions?.filter(Boolean).map((action) => {
|
|
171
|
+
const handleClick = action.onClick;
|
|
172
|
+
if (action.href) {
|
|
173
|
+
return (
|
|
174
|
+
<View style={styles.action} key={action.title}>
|
|
175
|
+
<Link
|
|
176
|
+
kind="primary"
|
|
177
|
+
href={action.href}
|
|
178
|
+
onClick={handleClick}
|
|
179
|
+
aria-label={action.ariaLabel ?? action.title}
|
|
180
|
+
style={styles.link}
|
|
181
|
+
>
|
|
182
|
+
{action.title}
|
|
183
|
+
</Link>
|
|
184
|
+
</View>
|
|
185
|
+
);
|
|
186
|
+
} else {
|
|
187
|
+
return (
|
|
188
|
+
<View style={styles.action} key={action.title}>
|
|
189
|
+
<Button
|
|
190
|
+
kind="tertiary"
|
|
191
|
+
size="small"
|
|
192
|
+
aria-label={action.ariaLabel ?? action.title}
|
|
193
|
+
onClick={handleClick}
|
|
194
|
+
>
|
|
195
|
+
{action.title}
|
|
196
|
+
</Button>
|
|
197
|
+
</View>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<View
|
|
205
|
+
style={[
|
|
206
|
+
styles.containerOuter,
|
|
207
|
+
layoutStyle,
|
|
208
|
+
{borderInlineStartColor: valuesForKind(kind).color},
|
|
209
|
+
]}
|
|
210
|
+
role={valuesForKind(kind).role}
|
|
211
|
+
aria-live={valuesForKind(kind).ariaLive}
|
|
212
|
+
testId={testId}
|
|
213
|
+
>
|
|
214
|
+
<View
|
|
215
|
+
style={[
|
|
216
|
+
styles.backgroundColor,
|
|
217
|
+
layoutStyle,
|
|
218
|
+
{backgroundColor: valuesForKind(kind).color},
|
|
219
|
+
]}
|
|
220
|
+
/>
|
|
221
|
+
<View style={styles.containerInner}>
|
|
222
|
+
<Icon
|
|
223
|
+
icon={bannerIcons[kind]}
|
|
224
|
+
size="medium"
|
|
225
|
+
style={styles.icon}
|
|
226
|
+
aria-label={kind}
|
|
227
|
+
testId="banner-kind-icon"
|
|
228
|
+
/>
|
|
229
|
+
<View style={styles.labelAndButtonsContainer}>
|
|
230
|
+
<View style={styles.labelContainer}>
|
|
231
|
+
<LabelSmall>{text}</LabelSmall>
|
|
232
|
+
</View>
|
|
233
|
+
{actions && (
|
|
234
|
+
<View style={styles.actionsContainer}>
|
|
235
|
+
{renderActions()}
|
|
236
|
+
</View>
|
|
237
|
+
)}
|
|
238
|
+
</View>
|
|
239
|
+
{onDismiss ? (
|
|
240
|
+
<View style={styles.dismissContainer}>
|
|
241
|
+
<IconButton
|
|
242
|
+
icon={icons.dismiss}
|
|
243
|
+
kind={"tertiary"}
|
|
244
|
+
onClick={onDismiss}
|
|
245
|
+
style={styles.dismiss}
|
|
246
|
+
aria-label={dismissAriaLabel}
|
|
247
|
+
/>
|
|
248
|
+
</View>
|
|
249
|
+
) : null}
|
|
250
|
+
</View>
|
|
251
|
+
</View>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
type DefaultProps = {|
|
|
256
|
+
kind: Props["kind"],
|
|
257
|
+
dismissAriaLabel: Props["dismissAriaLabel"],
|
|
258
|
+
|};
|
|
259
|
+
|
|
260
|
+
const defaultProps: DefaultProps = {
|
|
261
|
+
kind: "info",
|
|
262
|
+
dismissAriaLabel: "Dismiss banner.",
|
|
263
|
+
};
|
|
264
|
+
Banner.defaultProps = defaultProps;
|
|
265
|
+
|
|
266
|
+
const styles = StyleSheet.create({
|
|
267
|
+
backgroundColor: {
|
|
268
|
+
position: "absolute",
|
|
269
|
+
top: 0,
|
|
270
|
+
bottom: 0,
|
|
271
|
+
left: 0,
|
|
272
|
+
right: 0,
|
|
273
|
+
opacity: 0.08,
|
|
274
|
+
},
|
|
275
|
+
containerOuter: {
|
|
276
|
+
borderInlineStartWidth: Spacing.xxSmall_6,
|
|
277
|
+
width: "100%",
|
|
278
|
+
// Because of the background color's opacity value,
|
|
279
|
+
// the base color needs to be hard-coded as white for the
|
|
280
|
+
// intended pastel background color to show up correctly
|
|
281
|
+
// on dark backgrounds.
|
|
282
|
+
backgroundColor: Color.white,
|
|
283
|
+
},
|
|
284
|
+
containerInner: {
|
|
285
|
+
flexDirection: "row",
|
|
286
|
+
padding: Spacing.xSmall_8,
|
|
287
|
+
},
|
|
288
|
+
icon: {
|
|
289
|
+
marginTop: Spacing.xSmall_8,
|
|
290
|
+
marginBottom: Spacing.xSmall_8,
|
|
291
|
+
// The total distance from the icon to the edge is 16px. The
|
|
292
|
+
// vertical identifier is already 6px, and the padding on inner
|
|
293
|
+
// conatiner is 8px. So that leaves 2px.
|
|
294
|
+
marginInlineStart: Spacing.xxxxSmall_2,
|
|
295
|
+
marginInlineEnd: Spacing.xSmall_8,
|
|
296
|
+
alignSelf: "flex-start",
|
|
297
|
+
color: Color.offBlack64,
|
|
298
|
+
},
|
|
299
|
+
labelAndButtonsContainer: {
|
|
300
|
+
flex: 1,
|
|
301
|
+
flexDirection: "row",
|
|
302
|
+
alignItems: "center",
|
|
303
|
+
alignContent: "center",
|
|
304
|
+
flexWrap: "wrap",
|
|
305
|
+
justifyContent: "space-between",
|
|
306
|
+
},
|
|
307
|
+
labelContainer: {
|
|
308
|
+
flexShrink: 1,
|
|
309
|
+
margin: Spacing.xSmall_8,
|
|
310
|
+
textAlign: "start",
|
|
311
|
+
},
|
|
312
|
+
actionsContainer: {
|
|
313
|
+
flexDirection: "row",
|
|
314
|
+
justifyContent: "flex-start",
|
|
315
|
+
marginTop: Spacing.xSmall_8,
|
|
316
|
+
marginBottom: Spacing.xSmall_8,
|
|
317
|
+
},
|
|
318
|
+
action: {
|
|
319
|
+
marginLeft: Spacing.xSmall_8,
|
|
320
|
+
marginRight: Spacing.xSmall_8,
|
|
321
|
+
justifyContent: "center",
|
|
322
|
+
// Set the height to remove the padding from buttons
|
|
323
|
+
height: 18,
|
|
324
|
+
},
|
|
325
|
+
link: {
|
|
326
|
+
fontSize: 14,
|
|
327
|
+
},
|
|
328
|
+
dismiss: {
|
|
329
|
+
flexShrink: 1,
|
|
330
|
+
},
|
|
331
|
+
dismissContainer: {
|
|
332
|
+
height: 40,
|
|
333
|
+
width: 40,
|
|
334
|
+
justifyContent: "center",
|
|
335
|
+
alignItems: "center",
|
|
336
|
+
marginLeft: Spacing.xSmall_8,
|
|
337
|
+
marginRight: Spacing.xSmall_8,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
export default Banner;
|