@khanacademy/wonder-blocks-modal 3.0.2 → 3.0.4
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 +25 -0
- package/package.json +8 -8
- package/src/components/__tests__/close-button.test.js +7 -5
- package/src/components/__tests__/modal-backdrop.test.js +45 -83
- package/src/components/__tests__/modal-header.test.js +20 -21
- package/src/components/__tests__/modal-launcher.test.js +121 -137
- package/src/util/maybe-get-portal-mounted-modal-host-element.test.js +13 -14
- package/dist/index.js +0 -3013
- package/docs.md +0 -5
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import {mount} from "enzyme";
|
|
4
|
-
import "jest-enzyme";
|
|
5
3
|
import {render, screen, waitFor} from "@testing-library/react";
|
|
6
4
|
import userEvent from "@testing-library/user-event";
|
|
7
5
|
|
|
@@ -11,12 +9,6 @@ import Button from "@khanacademy/wonder-blocks-button";
|
|
|
11
9
|
import ModalLauncher from "../modal-launcher.js";
|
|
12
10
|
import OnePaneDialog from "../one-pane-dialog.js";
|
|
13
11
|
|
|
14
|
-
import {unmountAll} from "../../../../../utils/testing/enzyme-shim.js";
|
|
15
|
-
import {getElementAttachedToDocument} from "../../../../../utils/testing/get-element-attached-to-document.js";
|
|
16
|
-
|
|
17
|
-
const wait = (duration: number = 0) =>
|
|
18
|
-
new Promise((resolve, reject) => setTimeout(resolve, duration));
|
|
19
|
-
|
|
20
12
|
const exampleModal = (
|
|
21
13
|
<OnePaneDialog
|
|
22
14
|
title="Modal launcher test"
|
|
@@ -25,169 +17,150 @@ const exampleModal = (
|
|
|
25
17
|
);
|
|
26
18
|
|
|
27
19
|
describe("ModalLauncher", () => {
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
unmountAll();
|
|
30
|
-
if (document.body) {
|
|
31
|
-
document.body.innerHTML = "";
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
20
|
window.scrollTo = jest.fn();
|
|
36
21
|
|
|
37
22
|
test("Children can launch the modal", async () => {
|
|
38
23
|
// Arrange
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const containerDiv = getElementAttachedToDocument("container");
|
|
42
|
-
const wrapper = mount(
|
|
43
|
-
<ModalLauncher modal={exampleModal}>
|
|
24
|
+
render(
|
|
25
|
+
<ModalLauncher modal={exampleModal} testId="modal-launcher-portal">
|
|
44
26
|
{({openModal}) => <button onClick={openModal} />}
|
|
45
27
|
</ModalLauncher>,
|
|
46
|
-
{attachTo: containerDiv},
|
|
47
28
|
);
|
|
48
29
|
|
|
49
30
|
// Act
|
|
50
|
-
|
|
51
|
-
await wait();
|
|
31
|
+
userEvent.click(screen.getByRole("button"));
|
|
52
32
|
|
|
53
|
-
|
|
54
|
-
const portal = global.document.querySelector(
|
|
55
|
-
"[data-modal-launcher-portal]",
|
|
56
|
-
);
|
|
33
|
+
const portal = screen.getByTestId("modal-launcher-portal");
|
|
57
34
|
|
|
58
35
|
// Assert
|
|
59
|
-
expect(portal).
|
|
36
|
+
expect(portal).toBeInTheDocument();
|
|
60
37
|
});
|
|
61
38
|
|
|
62
39
|
test("Modal can be manually opened and closed", () => {
|
|
63
|
-
|
|
40
|
+
// Arrange
|
|
41
|
+
const UnderTest = ({opened}: {|opened: boolean|}) => (
|
|
64
42
|
<ModalLauncher
|
|
65
43
|
modal={exampleModal}
|
|
66
|
-
opened={
|
|
44
|
+
opened={opened}
|
|
67
45
|
onClose={() => {}}
|
|
68
|
-
|
|
46
|
+
testId="modal-launcher-portal"
|
|
47
|
+
/>
|
|
69
48
|
);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
49
|
+
const {rerender} = render(<UnderTest opened={false} />);
|
|
50
|
+
|
|
51
|
+
// Act
|
|
52
|
+
expect(
|
|
53
|
+
screen.queryByTestId("modal-launcher-portal"),
|
|
54
|
+
).not.toBeInTheDocument();
|
|
55
|
+
rerender(<UnderTest opened={true} />);
|
|
56
|
+
expect(screen.getByTestId("modal-launcher-portal")).toBeInTheDocument();
|
|
57
|
+
rerender(<UnderTest opened={false} />);
|
|
58
|
+
expect(
|
|
59
|
+
screen.queryByTestId("modal-launcher-portal"),
|
|
60
|
+
).not.toBeInTheDocument();
|
|
75
61
|
});
|
|
76
62
|
|
|
77
|
-
test("Modal can close itself after launching", (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// beyond the scope of this test. Really we just want to check that
|
|
90
|
-
// this function receives a `closeModal` argument that works.
|
|
91
|
-
const modalFn = ({closeModal}: {|closeModal: () => void|}) => {
|
|
92
|
-
expect(opened).toBe(true);
|
|
93
|
-
setTimeout(closeModal, 0);
|
|
94
|
-
return exampleModal;
|
|
95
|
-
};
|
|
63
|
+
test("Modal can close itself after launching", async () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const modalFn = ({closeModal}: {|closeModal: () => void|}) => (
|
|
66
|
+
<OnePaneDialog
|
|
67
|
+
title="Modal launcher test"
|
|
68
|
+
content={
|
|
69
|
+
<View>
|
|
70
|
+
<Button onClick={closeModal}>Close it!</Button>
|
|
71
|
+
</View>
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
96
75
|
|
|
97
|
-
|
|
98
|
-
// finish the test.
|
|
99
|
-
const onClose = () => {
|
|
100
|
-
wrapper.update();
|
|
101
|
-
expect(wrapper.find("ModalBackdrop")).toHaveLength(0);
|
|
102
|
-
done();
|
|
103
|
-
};
|
|
76
|
+
const onCloseMock = jest.fn();
|
|
104
77
|
|
|
105
78
|
// Mount the modal launcher. This shouldn't trigger any closing yet,
|
|
106
79
|
// because we shouldn't be calling the `modal` function yet.
|
|
107
|
-
|
|
108
|
-
<ModalLauncher
|
|
80
|
+
render(
|
|
81
|
+
<ModalLauncher
|
|
82
|
+
modal={modalFn}
|
|
83
|
+
onClose={onCloseMock}
|
|
84
|
+
testId="modal-launcher-portal"
|
|
85
|
+
>
|
|
109
86
|
{({openModal}) => <button onClick={openModal} />}
|
|
110
87
|
</ModalLauncher>,
|
|
111
88
|
);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
"[data-modal-launcher-portal]",
|
|
124
|
-
);
|
|
125
|
-
expect(portal instanceof HTMLDivElement).toBe(true);
|
|
89
|
+
|
|
90
|
+
userEvent.click(screen.getByRole("button"));
|
|
91
|
+
|
|
92
|
+
// wait until the modal is open
|
|
93
|
+
await screen.findByRole("dialog");
|
|
94
|
+
|
|
95
|
+
// Act
|
|
96
|
+
userEvent.click(screen.getByRole("button", {name: "Close it!"}));
|
|
97
|
+
|
|
98
|
+
// Assert
|
|
99
|
+
expect(onCloseMock).toHaveBeenCalled();
|
|
126
100
|
});
|
|
127
101
|
|
|
128
102
|
test("Pressing Escape closes the modal", async () => {
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
const wrapper = mount(
|
|
103
|
+
// Arrange
|
|
104
|
+
render(
|
|
132
105
|
<ModalLauncher modal={exampleModal}>
|
|
133
106
|
{({openModal}) => <button onClick={openModal} />}
|
|
134
107
|
</ModalLauncher>,
|
|
135
108
|
);
|
|
136
109
|
|
|
137
110
|
// Launch the modal.
|
|
138
|
-
|
|
139
|
-
// eslint-disable-next-line testing-library/no-node-access
|
|
140
|
-
expect(document.querySelector("[data-modal-child]")).toBeTruthy();
|
|
111
|
+
userEvent.click(screen.getByRole("button"));
|
|
141
112
|
|
|
113
|
+
// wait until the modal is open
|
|
114
|
+
await screen.findByRole("dialog");
|
|
115
|
+
|
|
116
|
+
// Act
|
|
142
117
|
// Simulate an Escape keypress.
|
|
143
|
-
|
|
144
|
-
// $FlowIgnore[cannot-write]
|
|
145
|
-
event.key = "Escape";
|
|
146
|
-
event.initEvent("keyup", true, true);
|
|
147
|
-
document.dispatchEvent(event);
|
|
118
|
+
userEvent.keyboard("{esc}");
|
|
148
119
|
|
|
120
|
+
// Assert
|
|
149
121
|
// Confirm that the modal is no longer mounted.
|
|
150
|
-
|
|
151
|
-
// NOTE(mdr): This might be fragile once React's async rendering lands.
|
|
152
|
-
// I wonder if we'll be able to force synchronous rendering in unit
|
|
153
|
-
// tests?
|
|
154
|
-
// eslint-disable-next-line testing-library/no-node-access
|
|
155
|
-
expect(document.querySelector("[data-modal-child]")).toBeFalsy();
|
|
122
|
+
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
|
156
123
|
});
|
|
157
124
|
|
|
158
|
-
test("Disable scrolling when the modal is open", () => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// Rather than test this rigorously, we'll just check that a
|
|
164
|
-
// ScrollDisabler is present, and trust ScrollDisabler to do its job.
|
|
165
|
-
const wrapper = mount(
|
|
166
|
-
<ModalLauncher
|
|
167
|
-
modal={({closeModal}) => {
|
|
168
|
-
savedCloseModal = closeModal;
|
|
169
|
-
return exampleModal;
|
|
170
|
-
}}
|
|
171
|
-
>
|
|
125
|
+
test("Disable scrolling when the modal is open", async () => {
|
|
126
|
+
// Arrange
|
|
127
|
+
render(
|
|
128
|
+
<ModalLauncher modal={exampleModal}>
|
|
172
129
|
{({openModal}) => <button onClick={openModal} />}
|
|
173
130
|
</ModalLauncher>,
|
|
174
131
|
);
|
|
175
132
|
|
|
176
|
-
//
|
|
177
|
-
expect(wrapper.find("ScrollDisabler")).toHaveLength(0);
|
|
178
|
-
|
|
133
|
+
// Act
|
|
179
134
|
// Launch the modal.
|
|
180
|
-
|
|
135
|
+
userEvent.click(screen.getByRole("button"));
|
|
181
136
|
|
|
137
|
+
// wait until the modal is open
|
|
138
|
+
await screen.findByRole("dialog");
|
|
139
|
+
|
|
140
|
+
// Assert
|
|
182
141
|
// Now that the modal is open, there should be a ScrollDisabler.
|
|
183
|
-
expect(
|
|
142
|
+
expect(document.body).toHaveStyle("overflow: hidden");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("re-enable scrolling after the modal is closed", async () => {
|
|
146
|
+
// Arrange
|
|
147
|
+
render(
|
|
148
|
+
<ModalLauncher modal={exampleModal}>
|
|
149
|
+
{({openModal}) => <button onClick={openModal} />}
|
|
150
|
+
</ModalLauncher>,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Launch the modal.
|
|
154
|
+
userEvent.click(screen.getByRole("button"));
|
|
155
|
+
|
|
156
|
+
await screen.findByRole("dialog");
|
|
184
157
|
|
|
185
158
|
// Close the modal.
|
|
186
|
-
|
|
187
|
-
wrapper.update();
|
|
159
|
+
userEvent.click(screen.getByRole("button", {name: "Close modal"}));
|
|
188
160
|
|
|
161
|
+
// Assert
|
|
189
162
|
// Now that the modal is closed, there should be no ScrollDisabler.
|
|
190
|
-
expect(
|
|
163
|
+
expect(document.body).not.toHaveStyle("overflow: hidden");
|
|
191
164
|
});
|
|
192
165
|
|
|
193
166
|
test("using `opened` and `children` should warn", () => {
|
|
@@ -247,20 +220,19 @@ describe("ModalLauncher", () => {
|
|
|
247
220
|
// Arrange
|
|
248
221
|
const onClose = jest.fn();
|
|
249
222
|
|
|
250
|
-
|
|
251
|
-
// click handler expects actual DOM events.
|
|
252
|
-
const wrapper = mount(
|
|
223
|
+
render(
|
|
253
224
|
<ModalLauncher
|
|
254
225
|
onClose={onClose}
|
|
255
226
|
modal={exampleModal}
|
|
256
227
|
opened={true}
|
|
257
228
|
backdropDismissEnabled={false}
|
|
229
|
+
testId="modal-launcher-backdrop"
|
|
258
230
|
/>,
|
|
259
231
|
);
|
|
260
232
|
|
|
261
233
|
// Act
|
|
262
|
-
const backdrop =
|
|
263
|
-
|
|
234
|
+
const backdrop = screen.getByTestId("modal-launcher-backdrop");
|
|
235
|
+
userEvent.click(backdrop);
|
|
264
236
|
|
|
265
237
|
// Assert
|
|
266
238
|
expect(onClose).not.toHaveBeenCalled();
|
|
@@ -268,30 +240,42 @@ describe("ModalLauncher", () => {
|
|
|
268
240
|
|
|
269
241
|
test("if modal is launched, move focus inside the modal", async () => {
|
|
270
242
|
// Arrange
|
|
271
|
-
|
|
272
|
-
<ModalLauncher
|
|
243
|
+
render(
|
|
244
|
+
<ModalLauncher
|
|
245
|
+
modal={
|
|
246
|
+
<OnePaneDialog
|
|
247
|
+
title="Modal launcher test"
|
|
248
|
+
content={
|
|
249
|
+
<View>
|
|
250
|
+
<Button>Button in modal</Button>
|
|
251
|
+
</View>
|
|
252
|
+
}
|
|
253
|
+
/>
|
|
254
|
+
}
|
|
255
|
+
>
|
|
273
256
|
{({openModal}) => (
|
|
274
|
-
<button onClick={openModal}
|
|
257
|
+
<button onClick={openModal}>Open modal</button>
|
|
275
258
|
)}
|
|
276
259
|
</ModalLauncher>,
|
|
277
260
|
);
|
|
278
261
|
|
|
279
|
-
const
|
|
280
|
-
.find("[data-last-focused-button]")
|
|
281
|
-
.getDOMNode();
|
|
262
|
+
const modalOpener = screen.getByRole("button", {name: "Open modal"});
|
|
282
263
|
// force focus
|
|
283
|
-
|
|
264
|
+
modalOpener.focus();
|
|
284
265
|
|
|
285
266
|
// Act
|
|
286
267
|
// Launch the modal.
|
|
287
|
-
|
|
268
|
+
userEvent.type(modalOpener, "{enter}");
|
|
288
269
|
|
|
289
|
-
// wait
|
|
290
|
-
await
|
|
270
|
+
// wait until the modal is open
|
|
271
|
+
await screen.findByRole("dialog");
|
|
291
272
|
|
|
292
273
|
// Assert
|
|
293
|
-
|
|
294
|
-
|
|
274
|
+
await waitFor(() =>
|
|
275
|
+
expect(
|
|
276
|
+
screen.getByRole("button", {name: "Button in modal"}),
|
|
277
|
+
).toHaveFocus(),
|
|
278
|
+
);
|
|
295
279
|
});
|
|
296
280
|
|
|
297
281
|
test("if modal is closed, return focus to the last element focused outside the modal", async () => {
|
|
@@ -428,7 +412,7 @@ describe("ModalLauncher", () => {
|
|
|
428
412
|
|
|
429
413
|
test("testId should be added to the Backdrop", () => {
|
|
430
414
|
// Arrange
|
|
431
|
-
|
|
415
|
+
render(
|
|
432
416
|
<ModalLauncher
|
|
433
417
|
opened={true}
|
|
434
418
|
onClose={jest.fn()}
|
|
@@ -438,9 +422,9 @@ describe("ModalLauncher", () => {
|
|
|
438
422
|
);
|
|
439
423
|
|
|
440
424
|
// Act
|
|
441
|
-
const backdrop =
|
|
425
|
+
const backdrop = screen.getByTestId("test-id-example");
|
|
442
426
|
|
|
443
427
|
// Assert
|
|
444
|
-
expect(backdrop
|
|
428
|
+
expect(backdrop).toBeInTheDocument();
|
|
445
429
|
});
|
|
446
430
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import * as ReactDOM from "react-dom";
|
|
4
|
-
import {
|
|
5
|
-
import "
|
|
4
|
+
import {render, screen} from "@testing-library/react";
|
|
5
|
+
import userEvent from "@testing-library/user-event";
|
|
6
6
|
|
|
7
7
|
import {ModalLauncherPortalAttributeName} from "./constants.js";
|
|
8
8
|
import maybeGetPortalMountedModalHostElement from "./maybe-get-portal-mounted-modal-host-element.js";
|
|
@@ -30,13 +30,11 @@ describe("maybeGetPortalMountedModalHostElement", () => {
|
|
|
30
30
|
</div>
|
|
31
31
|
</div>
|
|
32
32
|
);
|
|
33
|
-
|
|
34
|
-
const candidateElement =
|
|
33
|
+
render(nodes);
|
|
34
|
+
const candidateElement = screen.getByRole("button");
|
|
35
35
|
|
|
36
36
|
// Act
|
|
37
|
-
const result = maybeGetPortalMountedModalHostElement(
|
|
38
|
-
candidateElement.getDOMNode(),
|
|
39
|
-
);
|
|
37
|
+
const result = maybeGetPortalMountedModalHostElement(candidateElement);
|
|
40
38
|
|
|
41
39
|
// Assert
|
|
42
40
|
expect(result).toBeFalsy();
|
|
@@ -63,13 +61,14 @@ describe("maybeGetPortalMountedModalHostElement", () => {
|
|
|
63
61
|
/>
|
|
64
62
|
);
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
const candidateElement =
|
|
64
|
+
render(modal);
|
|
65
|
+
const candidateElement = screen.getByRole("button", {
|
|
66
|
+
name: "Candidate",
|
|
67
|
+
});
|
|
68
68
|
|
|
69
69
|
// Act
|
|
70
|
-
const result =
|
|
71
|
-
candidateElement
|
|
72
|
-
);
|
|
70
|
+
const result =
|
|
71
|
+
maybeGetPortalMountedModalHostElement(candidateElement);
|
|
73
72
|
|
|
74
73
|
// Assert
|
|
75
74
|
expect(result).toBeFalsy();
|
|
@@ -101,8 +100,8 @@ describe("maybeGetPortalMountedModalHostElement", () => {
|
|
|
101
100
|
)}
|
|
102
101
|
</ModalLauncher>
|
|
103
102
|
);
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
render(launcher);
|
|
104
|
+
userEvent.click(screen.getByRole("button"));
|
|
106
105
|
};
|
|
107
106
|
|
|
108
107
|
const actAndAssert = (node) => {
|