@khanacademy/wonder-blocks-modal 2.1.42 → 2.2.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 +10 -0
- package/dist/es/index.js +96 -85
- package/dist/index.js +913 -757
- package/package.json +15 -16
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +11 -11
- package/src/components/__tests__/close-button.test.js +1 -0
- package/src/components/__tests__/modal-backdrop.test.js +115 -57
- package/src/components/__tests__/modal-header.test.js +1 -0
- package/src/components/__tests__/modal-launcher.test.js +37 -6
- package/src/components/__tests__/modal-panel.test.js +1 -0
- package/src/components/__tests__/one-pane-dialog.test.js +1 -0
- package/src/components/modal-backdrop.js +17 -13
- package/src/components/one-pane-dialog.stories.js +11 -13
- package/src/util/find-focusable-nodes.js +14 -0
- package/src/util/maybe-get-portal-mounted-modal-host-element.test.js +3 -3
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-modal",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"design": "v2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -15,24 +15,23 @@
|
|
|
15
15
|
"author": "",
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@babel/runtime": "^7.
|
|
19
|
-
"@khanacademy/wonder-blocks-breadcrumbs": "^1.0.
|
|
20
|
-
"@khanacademy/wonder-blocks-color": "^1.1.
|
|
21
|
-
"@khanacademy/wonder-blocks-core": "^
|
|
22
|
-
"@khanacademy/wonder-blocks-icon": "^1.2.
|
|
23
|
-
"@khanacademy/wonder-blocks-icon-button": "^3.
|
|
24
|
-
"@khanacademy/wonder-blocks-layout": "^1.4.
|
|
25
|
-
"@khanacademy/wonder-blocks-spacing": "^3.0.
|
|
26
|
-
"@khanacademy/wonder-blocks-toolbar": "^2.1.
|
|
27
|
-
"@khanacademy/wonder-blocks-typography": "^1.1.
|
|
18
|
+
"@babel/runtime": "^7.16.3",
|
|
19
|
+
"@khanacademy/wonder-blocks-breadcrumbs": "^1.0.27",
|
|
20
|
+
"@khanacademy/wonder-blocks-color": "^1.1.20",
|
|
21
|
+
"@khanacademy/wonder-blocks-core": "^4.0.0",
|
|
22
|
+
"@khanacademy/wonder-blocks-icon": "^1.2.24",
|
|
23
|
+
"@khanacademy/wonder-blocks-icon-button": "^3.4.2",
|
|
24
|
+
"@khanacademy/wonder-blocks-layout": "^1.4.6",
|
|
25
|
+
"@khanacademy/wonder-blocks-spacing": "^3.0.5",
|
|
26
|
+
"@khanacademy/wonder-blocks-toolbar": "^2.1.28",
|
|
27
|
+
"@khanacademy/wonder-blocks-typography": "^1.1.28"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"aphrodite": "^1.2.5",
|
|
31
|
-
"react": "
|
|
32
|
-
"react-dom": "
|
|
31
|
+
"react": "16.14.0",
|
|
32
|
+
"react-dom": "16.14.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"wb-dev-build-settings": "^0.
|
|
36
|
-
}
|
|
37
|
-
"gitHead": "8022bb419eed74be37f71f71c7621854794a731c"
|
|
35
|
+
"wb-dev-build-settings": "^0.2.0"
|
|
36
|
+
}
|
|
38
37
|
}
|
|
@@ -21,8 +21,8 @@ exports[`wonder-blocks-modal example 1 1`] = `
|
|
|
21
21
|
}
|
|
22
22
|
>
|
|
23
23
|
<button
|
|
24
|
+
aria-disabled={false}
|
|
24
25
|
className=""
|
|
25
|
-
disabled={false}
|
|
26
26
|
onBlur={[Function]}
|
|
27
27
|
onClick={[Function]}
|
|
28
28
|
onDragStart={[Function]}
|
|
@@ -116,8 +116,8 @@ exports[`wonder-blocks-modal example 2 1`] = `
|
|
|
116
116
|
}
|
|
117
117
|
>
|
|
118
118
|
<button
|
|
119
|
+
aria-disabled={false}
|
|
119
120
|
className=""
|
|
120
|
-
disabled={false}
|
|
121
121
|
onBlur={[Function]}
|
|
122
122
|
onClick={[Function]}
|
|
123
123
|
onDragStart={[Function]}
|
|
@@ -392,8 +392,8 @@ exports[`wonder-blocks-modal example 4 1`] = `
|
|
|
392
392
|
}
|
|
393
393
|
>
|
|
394
394
|
<button
|
|
395
|
+
aria-disabled={false}
|
|
395
396
|
className=""
|
|
396
|
-
disabled={false}
|
|
397
397
|
onBlur={[Function]}
|
|
398
398
|
onClick={[Function]}
|
|
399
399
|
onDragStart={[Function]}
|
|
@@ -487,8 +487,8 @@ exports[`wonder-blocks-modal example 5 1`] = `
|
|
|
487
487
|
}
|
|
488
488
|
>
|
|
489
489
|
<button
|
|
490
|
+
aria-disabled={false}
|
|
490
491
|
className=""
|
|
491
|
-
disabled={false}
|
|
492
492
|
onBlur={[Function]}
|
|
493
493
|
onClick={[Function]}
|
|
494
494
|
onDragStart={[Function]}
|
|
@@ -1387,8 +1387,8 @@ exports[`wonder-blocks-modal example 7 1`] = `
|
|
|
1387
1387
|
}
|
|
1388
1388
|
>
|
|
1389
1389
|
<button
|
|
1390
|
+
aria-disabled={false}
|
|
1390
1391
|
className=""
|
|
1391
|
-
disabled={false}
|
|
1392
1392
|
onBlur={[Function]}
|
|
1393
1393
|
onClick={[Function]}
|
|
1394
1394
|
onDragStart={[Function]}
|
|
@@ -1996,8 +1996,8 @@ exports[`wonder-blocks-modal example 8 1`] = `
|
|
|
1996
1996
|
}
|
|
1997
1997
|
>
|
|
1998
1998
|
<button
|
|
1999
|
+
aria-disabled={false}
|
|
1999
2000
|
className=""
|
|
2000
|
-
disabled={false}
|
|
2001
2001
|
onBlur={[Function]}
|
|
2002
2002
|
onClick={[Function]}
|
|
2003
2003
|
onDragStart={[Function]}
|
|
@@ -2070,8 +2070,8 @@ exports[`wonder-blocks-modal example 8 1`] = `
|
|
|
2070
2070
|
</span>
|
|
2071
2071
|
</button>
|
|
2072
2072
|
<button
|
|
2073
|
+
aria-disabled={false}
|
|
2073
2074
|
className=""
|
|
2074
|
-
disabled={false}
|
|
2075
2075
|
onBlur={[Function]}
|
|
2076
2076
|
onClick={[Function]}
|
|
2077
2077
|
onDragStart={[Function]}
|
|
@@ -2144,8 +2144,8 @@ exports[`wonder-blocks-modal example 8 1`] = `
|
|
|
2144
2144
|
</span>
|
|
2145
2145
|
</button>
|
|
2146
2146
|
<button
|
|
2147
|
+
aria-disabled={false}
|
|
2147
2148
|
className=""
|
|
2148
|
-
disabled={false}
|
|
2149
2149
|
onBlur={[Function]}
|
|
2150
2150
|
onClick={[Function]}
|
|
2151
2151
|
onDragStart={[Function]}
|
|
@@ -2627,8 +2627,8 @@ exports[`wonder-blocks-modal example 9 1`] = `
|
|
|
2627
2627
|
}
|
|
2628
2628
|
>
|
|
2629
2629
|
<button
|
|
2630
|
+
aria-disabled={false}
|
|
2630
2631
|
className=""
|
|
2631
|
-
disabled={false}
|
|
2632
2632
|
onBlur={[Function]}
|
|
2633
2633
|
onClick={[Function]}
|
|
2634
2634
|
onDragStart={[Function]}
|
|
@@ -2726,8 +2726,8 @@ exports[`wonder-blocks-modal example 9 1`] = `
|
|
|
2726
2726
|
}
|
|
2727
2727
|
/>
|
|
2728
2728
|
<button
|
|
2729
|
+
aria-disabled={false}
|
|
2729
2730
|
className=""
|
|
2730
|
-
disabled={false}
|
|
2731
2731
|
onBlur={[Function]}
|
|
2732
2732
|
onClick={[Function]}
|
|
2733
2733
|
onDragStart={[Function]}
|
|
@@ -3321,8 +3321,8 @@ exports[`wonder-blocks-modal example 10 1`] = `
|
|
|
3321
3321
|
}
|
|
3322
3322
|
/>
|
|
3323
3323
|
<button
|
|
3324
|
+
aria-disabled={false}
|
|
3324
3325
|
className=""
|
|
3325
|
-
disabled={false}
|
|
3326
3326
|
onBlur={[Function]}
|
|
3327
3327
|
onClick={[Function]}
|
|
3328
3328
|
onDragStart={[Function]}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import {mount} from "enzyme";
|
|
4
|
+
import "jest-enzyme";
|
|
5
|
+
import {render, screen, fireEvent} from "@testing-library/react";
|
|
6
|
+
import userEvent from "@testing-library/user-event";
|
|
4
7
|
|
|
5
8
|
import ModalBackdrop from "../modal-backdrop.js";
|
|
6
9
|
import OnePaneDialog from "../one-pane-dialog.js";
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
import {unmountAll} from "../../../../../utils/testing/enzyme-shim.js";
|
|
12
|
+
import {getElementAttachedToDocument} from "../../../../../utils/testing/get-element-attached-to-document.js";
|
|
13
|
+
|
|
14
|
+
const wait = (duration: number = 0) =>
|
|
9
15
|
new Promise((resolve, reject) => setTimeout(resolve, duration));
|
|
10
16
|
|
|
11
17
|
const exampleModal = (
|
|
@@ -13,6 +19,7 @@ const exampleModal = (
|
|
|
13
19
|
content={<div data-modal-content />}
|
|
14
20
|
title="Title"
|
|
15
21
|
footer={<div data-modal-footer />}
|
|
22
|
+
testId="example-modal-test-id"
|
|
16
23
|
/>
|
|
17
24
|
);
|
|
18
25
|
|
|
@@ -31,20 +38,36 @@ const exampleModalWithButtons = (
|
|
|
31
38
|
);
|
|
32
39
|
|
|
33
40
|
describe("ModalBackdrop", () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.useRealTimers();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
unmountAll();
|
|
47
|
+
if (document.body) {
|
|
48
|
+
document.body.innerHTML = "";
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
34
52
|
test("Clicking the backdrop triggers `onCloseModal`", () => {
|
|
53
|
+
// Arrange
|
|
35
54
|
const onCloseModal = jest.fn();
|
|
36
55
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
render(
|
|
57
|
+
<ModalBackdrop
|
|
58
|
+
onCloseModal={onCloseModal}
|
|
59
|
+
testId="modal-backdrop-test-id"
|
|
60
|
+
>
|
|
41
61
|
{exampleModal}
|
|
42
62
|
</ModalBackdrop>,
|
|
43
63
|
);
|
|
44
64
|
|
|
45
|
-
|
|
65
|
+
const backdrop = screen.getByTestId("modal-backdrop-test-id");
|
|
66
|
+
|
|
67
|
+
//Act
|
|
68
|
+
userEvent.click(backdrop);
|
|
46
69
|
|
|
47
|
-
|
|
70
|
+
// Assert
|
|
48
71
|
expect(onCloseModal).toHaveBeenCalled();
|
|
49
72
|
});
|
|
50
73
|
|
|
@@ -63,6 +86,62 @@ describe("ModalBackdrop", () => {
|
|
|
63
86
|
expect(onCloseModal).not.toHaveBeenCalled();
|
|
64
87
|
});
|
|
65
88
|
|
|
89
|
+
test("Clicking and dragging into the backdrop does not close modal", () => {
|
|
90
|
+
// Arrange
|
|
91
|
+
const onCloseModal = jest.fn();
|
|
92
|
+
|
|
93
|
+
render(
|
|
94
|
+
<ModalBackdrop
|
|
95
|
+
onCloseModal={onCloseModal}
|
|
96
|
+
testId="modal-backdrop-test-id"
|
|
97
|
+
>
|
|
98
|
+
{exampleModal}
|
|
99
|
+
</ModalBackdrop>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const panel = screen.getByTestId("example-modal-test-id");
|
|
103
|
+
const backdrop = screen.getByTestId("modal-backdrop-test-id");
|
|
104
|
+
|
|
105
|
+
// Act
|
|
106
|
+
|
|
107
|
+
// Dragging the mouse
|
|
108
|
+
// eslint-disable-next-line testing-library/prefer-user-event
|
|
109
|
+
fireEvent.mouseDown(panel);
|
|
110
|
+
// eslint-disable-next-line testing-library/prefer-user-event
|
|
111
|
+
fireEvent.mouseUp(backdrop);
|
|
112
|
+
|
|
113
|
+
// Assert
|
|
114
|
+
expect(onCloseModal).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("Clicking and dragging in from the backdrop does not close modal", () => {
|
|
118
|
+
// Arrange
|
|
119
|
+
const onCloseModal = jest.fn();
|
|
120
|
+
|
|
121
|
+
render(
|
|
122
|
+
<ModalBackdrop
|
|
123
|
+
onCloseModal={onCloseModal}
|
|
124
|
+
testId="modal-backdrop-test-id"
|
|
125
|
+
>
|
|
126
|
+
{exampleModal}
|
|
127
|
+
</ModalBackdrop>,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const panel = screen.getByTestId("example-modal-test-id");
|
|
131
|
+
const backdrop = screen.getByTestId("modal-backdrop-test-id");
|
|
132
|
+
|
|
133
|
+
// Act
|
|
134
|
+
|
|
135
|
+
// Dragging the mouse
|
|
136
|
+
// eslint-disable-next-line testing-library/prefer-user-event
|
|
137
|
+
fireEvent.mouseDown(backdrop);
|
|
138
|
+
// eslint-disable-next-line testing-library/prefer-user-event
|
|
139
|
+
fireEvent.mouseUp(panel);
|
|
140
|
+
|
|
141
|
+
// Assert
|
|
142
|
+
expect(onCloseModal).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
66
145
|
test("Clicking the modal footer does not trigger `onCloseModal`", () => {
|
|
67
146
|
const onCloseModal = jest.fn();
|
|
68
147
|
|
|
@@ -80,6 +159,10 @@ describe("ModalBackdrop", () => {
|
|
|
80
159
|
|
|
81
160
|
test("If initialFocusId is set and element is found, we focus that element inside the modal", async () => {
|
|
82
161
|
// Arrange
|
|
162
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
163
|
+
// to work. Changing to testing-library will likely fix this.
|
|
164
|
+
// Then we can remove the lint suppression.
|
|
165
|
+
const attachElement = getElementAttachedToDocument("container");
|
|
83
166
|
const initialFocusId = "initial-focus";
|
|
84
167
|
|
|
85
168
|
const wrapper = mount(
|
|
@@ -98,21 +181,28 @@ describe("ModalBackdrop", () => {
|
|
|
98
181
|
footer={<div data-modal-footer />}
|
|
99
182
|
/>
|
|
100
183
|
</ModalBackdrop>,
|
|
184
|
+
{attachTo: attachElement},
|
|
101
185
|
);
|
|
102
186
|
|
|
103
187
|
// Act
|
|
104
|
-
await
|
|
188
|
+
await wait(); // wait for styles to be applied
|
|
105
189
|
const initialFocusElement = wrapper.find(`#${initialFocusId}`);
|
|
106
190
|
|
|
107
191
|
// Assert
|
|
108
192
|
// first we verify the element exists in the DOM
|
|
109
193
|
expect(initialFocusElement).toHaveLength(1);
|
|
194
|
+
|
|
110
195
|
// verify the focus is set on the correct element
|
|
196
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
111
197
|
expect(document.activeElement).toBe(initialFocusElement.getDOMNode());
|
|
112
198
|
});
|
|
113
199
|
|
|
114
200
|
test("If initialFocusId is set but element is NOT found, we focus on the first focusable element instead", async () => {
|
|
115
201
|
// Arrange
|
|
202
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
203
|
+
// to work. Changing to testing-library will likely fix this.
|
|
204
|
+
// Then we can remove the lint suppression.
|
|
205
|
+
const attachElement = getElementAttachedToDocument("container");
|
|
116
206
|
const initialFocusId = "initial-focus";
|
|
117
207
|
const firstFocusableElement = "[data-first-button]";
|
|
118
208
|
|
|
@@ -123,16 +213,18 @@ describe("ModalBackdrop", () => {
|
|
|
123
213
|
>
|
|
124
214
|
{exampleModalWithButtons}
|
|
125
215
|
</ModalBackdrop>,
|
|
216
|
+
{attachTo: attachElement},
|
|
126
217
|
);
|
|
127
218
|
|
|
128
219
|
// Act
|
|
129
|
-
await
|
|
220
|
+
await wait(); // wait for styles to be applied
|
|
130
221
|
const initialFocusElement = wrapper.find(`#${initialFocusId}`);
|
|
131
222
|
|
|
132
223
|
// Assert
|
|
133
224
|
// first we verify the element doesn't exist in the DOM
|
|
134
225
|
expect(initialFocusElement).toHaveLength(0);
|
|
135
226
|
// verify the focus is set on the first focusable element instead
|
|
227
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
136
228
|
expect(document.activeElement).toBe(
|
|
137
229
|
wrapper.find(firstFocusableElement).getDOMNode(),
|
|
138
230
|
);
|
|
@@ -140,83 +232,49 @@ describe("ModalBackdrop", () => {
|
|
|
140
232
|
|
|
141
233
|
test("If no initialFocusId is set, we focus the first button in the modal", async () => {
|
|
142
234
|
// Arrange
|
|
235
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
236
|
+
// to work. Changing to testing-library will likely fix this.
|
|
237
|
+
// Then we can remove the lint suppression.
|
|
238
|
+
const attachElement = getElementAttachedToDocument("container");
|
|
143
239
|
const wrapper = mount(
|
|
144
240
|
<ModalBackdrop onCloseModal={() => {}}>
|
|
145
241
|
{exampleModalWithButtons}
|
|
146
242
|
</ModalBackdrop>,
|
|
243
|
+
{attachTo: attachElement},
|
|
147
244
|
);
|
|
148
245
|
|
|
149
246
|
// Act
|
|
150
|
-
await
|
|
247
|
+
await wait(); // wait for styles to be applied
|
|
151
248
|
const focusableElement = wrapper
|
|
152
249
|
.find("[data-first-button]")
|
|
153
250
|
.getDOMNode();
|
|
154
251
|
|
|
155
252
|
// Assert
|
|
253
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
156
254
|
expect(document.activeElement).toBe(focusableElement);
|
|
157
255
|
});
|
|
158
256
|
|
|
159
257
|
test("If there are no focusable elements, we focus the Dialog instead", async () => {
|
|
160
258
|
// Arrange
|
|
259
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
260
|
+
// to work. Changing to testing-library will likely fix this.
|
|
261
|
+
// Then we can remove the lint suppression.
|
|
262
|
+
const attachElement = getElementAttachedToDocument("container");
|
|
161
263
|
const wrapper = mount(
|
|
162
264
|
<ModalBackdrop onCloseModal={() => {}}>
|
|
163
265
|
{exampleModal}
|
|
164
266
|
</ModalBackdrop>,
|
|
267
|
+
{attachTo: attachElement},
|
|
165
268
|
);
|
|
166
269
|
|
|
167
270
|
// Act
|
|
168
|
-
await
|
|
271
|
+
await wait(); // wait for styles to be applied
|
|
169
272
|
const focusableElement = wrapper
|
|
170
273
|
.find('div[role="dialog"]')
|
|
171
274
|
.getDOMNode();
|
|
172
275
|
|
|
173
276
|
// Assert
|
|
277
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
174
278
|
expect(document.activeElement).toBe(focusableElement);
|
|
175
279
|
});
|
|
176
|
-
|
|
177
|
-
// TODO(mdr): I haven't figured out how to actually simulate tab keystrokes
|
|
178
|
-
// or focus events in a way that JSDOM will recognize, so triggering the
|
|
179
|
-
// global focus handler isn't feasible. I had to do manual testing
|
|
180
|
-
// instead :( Here's what I had, though!
|
|
181
|
-
test.skip("Tabbing inside the modal wraps around", () => {
|
|
182
|
-
const wrapper = mount(
|
|
183
|
-
<div>
|
|
184
|
-
<button data-button-id="A" />
|
|
185
|
-
<ModalBackdrop onCloseModal={() => {}}>
|
|
186
|
-
{exampleModalWithButtons}
|
|
187
|
-
</ModalBackdrop>
|
|
188
|
-
<button data-button-id="Z" />
|
|
189
|
-
</div>,
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
const buttonA = wrapper.find('[data-button-id="A"]').getDOMNode();
|
|
193
|
-
const button1 = wrapper.find('[data-button-id="1"]').getDOMNode();
|
|
194
|
-
const button2 = wrapper.find('[data-button-id="2"]').getDOMNode();
|
|
195
|
-
const button3 = wrapper.find('[data-button-id="3"]').getDOMNode();
|
|
196
|
-
const buttonZ = wrapper.find('[data-button-id="Z"]').getDOMNode();
|
|
197
|
-
|
|
198
|
-
// First, go forward. Confirm that, when we get to button Z, we wrap
|
|
199
|
-
// back to button 1. (I wish we could just simulate tab keypresses!
|
|
200
|
-
// Instead, we depend on the implementation detail that _which_ node you
|
|
201
|
-
// exit from determines where you'll end up.)
|
|
202
|
-
button1.focus();
|
|
203
|
-
expect(document.activeElement).toBe(button1);
|
|
204
|
-
button2.focus();
|
|
205
|
-
expect(document.activeElement).toBe(button2);
|
|
206
|
-
button3.focus();
|
|
207
|
-
expect(document.activeElement).toBe(button3);
|
|
208
|
-
buttonZ.focus();
|
|
209
|
-
expect(document.activeElement).toBe(button1);
|
|
210
|
-
|
|
211
|
-
// Then, go backward. Confirm that, when we get to button A, we wrap
|
|
212
|
-
// back to button 3.
|
|
213
|
-
button3.focus();
|
|
214
|
-
expect(document.activeElement).toBe(button3);
|
|
215
|
-
button2.focus();
|
|
216
|
-
expect(document.activeElement).toBe(button2);
|
|
217
|
-
button1.focus();
|
|
218
|
-
expect(document.activeElement).toBe(button1);
|
|
219
|
-
buttonA.focus();
|
|
220
|
-
expect(document.activeElement).toBe(button3);
|
|
221
|
-
});
|
|
222
280
|
});
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import {mount, shallow} from "enzyme";
|
|
4
|
+
import "jest-enzyme";
|
|
4
5
|
|
|
5
6
|
import ModalLauncher from "../modal-launcher.js";
|
|
6
7
|
import OnePaneDialog from "../one-pane-dialog.js";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
import {unmountAll} from "../../../../../utils/testing/enzyme-shim.js";
|
|
10
|
+
import {getElementAttachedToDocument} from "../../../../../utils/testing/get-element-attached-to-document.js";
|
|
11
|
+
|
|
12
|
+
const wait = (duration: number = 0) =>
|
|
9
13
|
new Promise((resolve, reject) => setTimeout(resolve, duration));
|
|
10
14
|
|
|
11
15
|
const exampleModal = (
|
|
@@ -16,19 +20,41 @@ const exampleModal = (
|
|
|
16
20
|
);
|
|
17
21
|
|
|
18
22
|
describe("ModalLauncher", () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.useRealTimers();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
unmountAll();
|
|
29
|
+
if (document.body) {
|
|
30
|
+
document.body.innerHTML = "";
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
19
34
|
window.scrollTo = jest.fn();
|
|
20
35
|
|
|
21
|
-
test("Children can launch the modal", () => {
|
|
36
|
+
test("Children can launch the modal", async () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
39
|
+
// to work. Changing to testing-library will likely fix this.
|
|
40
|
+
const containerDiv = getElementAttachedToDocument("container");
|
|
22
41
|
const wrapper = mount(
|
|
23
42
|
<ModalLauncher modal={exampleModal}>
|
|
24
43
|
{({openModal}) => <button onClick={openModal} />}
|
|
25
44
|
</ModalLauncher>,
|
|
45
|
+
{attachTo: containerDiv},
|
|
26
46
|
);
|
|
47
|
+
|
|
48
|
+
// Act
|
|
27
49
|
wrapper.find("button").simulate("click");
|
|
50
|
+
await wait();
|
|
51
|
+
|
|
28
52
|
const portal = global.document.querySelector(
|
|
29
53
|
"[data-modal-launcher-portal]",
|
|
30
54
|
);
|
|
31
|
-
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(portal).toBeInstanceOf(HTMLDivElement);
|
|
32
58
|
});
|
|
33
59
|
|
|
34
60
|
test("Modal can be manually opened and closed", () => {
|
|
@@ -62,7 +88,7 @@ describe("ModalLauncher", () => {
|
|
|
62
88
|
// this function receives a `closeModal` argument that works.
|
|
63
89
|
const modalFn = ({closeModal}: {|closeModal: () => void|}) => {
|
|
64
90
|
expect(opened).toBe(true);
|
|
65
|
-
|
|
91
|
+
setTimeout(closeModal, 0);
|
|
66
92
|
return exampleModal;
|
|
67
93
|
};
|
|
68
94
|
|
|
@@ -110,6 +136,7 @@ describe("ModalLauncher", () => {
|
|
|
110
136
|
|
|
111
137
|
// Simulate an Escape keypress.
|
|
112
138
|
const event: KeyboardEvent = (document.createEvent("Event"): any);
|
|
139
|
+
// $FlowIgnore[cannot-write]
|
|
113
140
|
event.key = "Escape";
|
|
114
141
|
event.initEvent("keyup", true, true);
|
|
115
142
|
document.dispatchEvent(event);
|
|
@@ -254,7 +281,7 @@ describe("ModalLauncher", () => {
|
|
|
254
281
|
wrapper.find("button").simulate("click");
|
|
255
282
|
|
|
256
283
|
// wait for styles to be applied
|
|
257
|
-
await
|
|
284
|
+
await wait();
|
|
258
285
|
|
|
259
286
|
// Assert
|
|
260
287
|
expect(document.activeElement).not.toBe(lastButton);
|
|
@@ -262,6 +289,9 @@ describe("ModalLauncher", () => {
|
|
|
262
289
|
|
|
263
290
|
test("if modal is closed, return focus to the last element focused outside the modal", async () => {
|
|
264
291
|
// Arrange
|
|
292
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
293
|
+
// to work. Changing to testing-library will likely fix this.
|
|
294
|
+
const containerDiv = getElementAttachedToDocument("container");
|
|
265
295
|
let savedCloseModal = () => {
|
|
266
296
|
throw new Error(`closeModal wasn't saved`);
|
|
267
297
|
};
|
|
@@ -277,6 +307,7 @@ describe("ModalLauncher", () => {
|
|
|
277
307
|
<button onClick={openModal} data-last-focused-button />
|
|
278
308
|
)}
|
|
279
309
|
</ModalLauncher>,
|
|
310
|
+
{attachTo: containerDiv},
|
|
280
311
|
);
|
|
281
312
|
|
|
282
313
|
const lastButton = wrapper
|
|
@@ -289,7 +320,7 @@ describe("ModalLauncher", () => {
|
|
|
289
320
|
wrapper.find("button").simulate("click");
|
|
290
321
|
|
|
291
322
|
// wait for styles to be applied
|
|
292
|
-
await
|
|
323
|
+
await wait();
|
|
293
324
|
|
|
294
325
|
// Act
|
|
295
326
|
savedCloseModal(); // close the modal
|
|
@@ -7,6 +7,8 @@ import Color from "@khanacademy/wonder-blocks-color";
|
|
|
7
7
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
8
8
|
import {ModalLauncherPortalAttributeName} from "../util/constants.js";
|
|
9
9
|
|
|
10
|
+
import {findFocusableNodes} from "../util/find-focusable-nodes.js";
|
|
11
|
+
|
|
10
12
|
import type {ModalElement} from "../util/types.js";
|
|
11
13
|
|
|
12
14
|
type Props = {|
|
|
@@ -24,13 +26,6 @@ type Props = {|
|
|
|
24
26
|
testId?: string,
|
|
25
27
|
|};
|
|
26
28
|
|
|
27
|
-
/**
|
|
28
|
-
* List of elements that can be focused
|
|
29
|
-
* @see https://www.w3.org/TR/html5/editing.html#can-be-focused
|
|
30
|
-
*/
|
|
31
|
-
const FOCUSABLE_ELEMENTS =
|
|
32
|
-
'a[href], details, input, textarea, select, button:not([aria-label^="Close"])';
|
|
33
|
-
|
|
34
29
|
/**
|
|
35
30
|
* A private component used by ModalLauncher. This is the fixed-position
|
|
36
31
|
* container element that gets mounted outside the DOM. It overlays the modal
|
|
@@ -62,6 +57,8 @@ export default class ModalBackdrop extends React.Component<Props> {
|
|
|
62
57
|
}, 0);
|
|
63
58
|
}
|
|
64
59
|
|
|
60
|
+
_mousePressedOutside: boolean = false;
|
|
61
|
+
|
|
65
62
|
/**
|
|
66
63
|
* Returns an element specified by the user
|
|
67
64
|
*/
|
|
@@ -82,7 +79,7 @@ export default class ModalBackdrop extends React.Component<Props> {
|
|
|
82
79
|
*/
|
|
83
80
|
_getFirstFocusableElement(node: HTMLElement): HTMLElement | null {
|
|
84
81
|
// get a collection of elements that can be focused
|
|
85
|
-
const focusableElements = node
|
|
82
|
+
const focusableElements = findFocusableNodes(node);
|
|
86
83
|
|
|
87
84
|
if (!focusableElements) {
|
|
88
85
|
return null;
|
|
@@ -112,12 +109,18 @@ export default class ModalBackdrop extends React.Component<Props> {
|
|
|
112
109
|
* _directly_ from the positioner, not bubbled up from its children), close
|
|
113
110
|
* the modal.
|
|
114
111
|
*/
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
handleMouseDown: (e: SyntheticEvent<>) => void = (e: SyntheticEvent<>) => {
|
|
113
|
+
// Confirm that it is the backdrop that is being clicked, not the child
|
|
114
|
+
this._mousePressedOutside = e.target === e.currentTarget;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
handleMouseUp: (e: SyntheticEvent<>) => void = (e: SyntheticEvent<>) => {
|
|
118
|
+
// Confirm that it is the backdrop that is being clicked, not the child
|
|
119
|
+
// and that the mouse was pressed in the backdrop first.
|
|
120
|
+
if (e.target === e.currentTarget && this._mousePressedOutside) {
|
|
119
121
|
this.props.onCloseModal();
|
|
120
122
|
}
|
|
123
|
+
this._mousePressedOutside = false;
|
|
121
124
|
};
|
|
122
125
|
|
|
123
126
|
render(): React.Node {
|
|
@@ -129,7 +132,8 @@ export default class ModalBackdrop extends React.Component<Props> {
|
|
|
129
132
|
return (
|
|
130
133
|
<View
|
|
131
134
|
style={styles.modalPositioner}
|
|
132
|
-
|
|
135
|
+
onMouseDown={this.handleMouseDown}
|
|
136
|
+
onMouseUp={this.handleMouseUp}
|
|
133
137
|
testId={testId}
|
|
134
138
|
{...backdropProps}
|
|
135
139
|
>
|