@khanacademy/wonder-blocks-modal 2.1.41 → 2.1.45
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/dist/es/index.js +601 -945
- package/dist/index.js +1089 -1422
- package/package.json +15 -14
- package/src/components/__tests__/modal-backdrop.test.js +35 -51
- package/src/components/__tests__/modal-launcher.test.js +36 -6
- package/src/components/modal-backdrop.js +3 -8
- package/src/components/one-pane-dialog.stories.js +8 -10
- package/src/util/find-focusable-nodes.js +14 -0
- package/src/util/maybe-get-portal-mounted-modal-host-element.test.js +2 -3
- package/src/__tests__/index.test.js +0 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-modal",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.45",
|
|
4
4
|
"design": "v2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -15,23 +15,24 @@
|
|
|
15
15
|
"author": "",
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@
|
|
19
|
-
"@khanacademy/wonder-blocks-
|
|
20
|
-
"@khanacademy/wonder-blocks-
|
|
21
|
-
"@khanacademy/wonder-blocks-
|
|
22
|
-
"@khanacademy/wonder-blocks-icon
|
|
23
|
-
"@khanacademy/wonder-blocks-
|
|
24
|
-
"@khanacademy/wonder-blocks-
|
|
25
|
-
"@khanacademy/wonder-blocks-
|
|
26
|
-
"@khanacademy/wonder-blocks-
|
|
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.1",
|
|
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"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
29
30
|
"aphrodite": "^1.2.5",
|
|
30
|
-
"react": "
|
|
31
|
-
"react-dom": "
|
|
31
|
+
"react": "16.14.0",
|
|
32
|
+
"react-dom": "16.14.0"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
|
-
"wb-dev-build-settings": "^0.0
|
|
35
|
+
"wb-dev-build-settings": "^0.2.0"
|
|
35
36
|
},
|
|
36
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "9ebea88533e702011165072f090a377e02fa3f0f"
|
|
37
38
|
}
|
|
@@ -5,7 +5,10 @@ import {mount} from "enzyme";
|
|
|
5
5
|
import ModalBackdrop from "../modal-backdrop.js";
|
|
6
6
|
import OnePaneDialog from "../one-pane-dialog.js";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import {unmountAll} from "../../../../../utils/testing/enzyme-shim.js";
|
|
9
|
+
import {getElementAttachedToDocument} from "../../../../../utils/testing/get-element-attached-to-document.js";
|
|
10
|
+
|
|
11
|
+
const wait = (duration: number = 0) =>
|
|
9
12
|
new Promise((resolve, reject) => setTimeout(resolve, duration));
|
|
10
13
|
|
|
11
14
|
const exampleModal = (
|
|
@@ -31,6 +34,17 @@ const exampleModalWithButtons = (
|
|
|
31
34
|
);
|
|
32
35
|
|
|
33
36
|
describe("ModalBackdrop", () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
jest.useRealTimers();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
unmountAll();
|
|
43
|
+
if (document.body) {
|
|
44
|
+
document.body.innerHTML = "";
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
34
48
|
test("Clicking the backdrop triggers `onCloseModal`", () => {
|
|
35
49
|
const onCloseModal = jest.fn();
|
|
36
50
|
|
|
@@ -80,6 +94,9 @@ describe("ModalBackdrop", () => {
|
|
|
80
94
|
|
|
81
95
|
test("If initialFocusId is set and element is found, we focus that element inside the modal", async () => {
|
|
82
96
|
// Arrange
|
|
97
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
98
|
+
// to work. Changing to testing-library will likely fix this.
|
|
99
|
+
const attachElement = getElementAttachedToDocument("container");
|
|
83
100
|
const initialFocusId = "initial-focus";
|
|
84
101
|
|
|
85
102
|
const wrapper = mount(
|
|
@@ -98,10 +115,11 @@ describe("ModalBackdrop", () => {
|
|
|
98
115
|
footer={<div data-modal-footer />}
|
|
99
116
|
/>
|
|
100
117
|
</ModalBackdrop>,
|
|
118
|
+
{attachTo: attachElement},
|
|
101
119
|
);
|
|
102
120
|
|
|
103
121
|
// Act
|
|
104
|
-
await
|
|
122
|
+
await wait(); // wait for styles to be applied
|
|
105
123
|
const initialFocusElement = wrapper.find(`#${initialFocusId}`);
|
|
106
124
|
|
|
107
125
|
// Assert
|
|
@@ -113,6 +131,9 @@ describe("ModalBackdrop", () => {
|
|
|
113
131
|
|
|
114
132
|
test("If initialFocusId is set but element is NOT found, we focus on the first focusable element instead", async () => {
|
|
115
133
|
// Arrange
|
|
134
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
135
|
+
// to work. Changing to testing-library will likely fix this.
|
|
136
|
+
const attachElement = getElementAttachedToDocument("container");
|
|
116
137
|
const initialFocusId = "initial-focus";
|
|
117
138
|
const firstFocusableElement = "[data-first-button]";
|
|
118
139
|
|
|
@@ -123,10 +144,11 @@ describe("ModalBackdrop", () => {
|
|
|
123
144
|
>
|
|
124
145
|
{exampleModalWithButtons}
|
|
125
146
|
</ModalBackdrop>,
|
|
147
|
+
{attachTo: attachElement},
|
|
126
148
|
);
|
|
127
149
|
|
|
128
150
|
// Act
|
|
129
|
-
await
|
|
151
|
+
await wait(); // wait for styles to be applied
|
|
130
152
|
const initialFocusElement = wrapper.find(`#${initialFocusId}`);
|
|
131
153
|
|
|
132
154
|
// Assert
|
|
@@ -140,14 +162,18 @@ describe("ModalBackdrop", () => {
|
|
|
140
162
|
|
|
141
163
|
test("If no initialFocusId is set, we focus the first button in the modal", async () => {
|
|
142
164
|
// Arrange
|
|
165
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
166
|
+
// to work. Changing to testing-library will likely fix this.
|
|
167
|
+
const attachElement = getElementAttachedToDocument("container");
|
|
143
168
|
const wrapper = mount(
|
|
144
169
|
<ModalBackdrop onCloseModal={() => {}}>
|
|
145
170
|
{exampleModalWithButtons}
|
|
146
171
|
</ModalBackdrop>,
|
|
172
|
+
{attachTo: attachElement},
|
|
147
173
|
);
|
|
148
174
|
|
|
149
175
|
// Act
|
|
150
|
-
await
|
|
176
|
+
await wait(); // wait for styles to be applied
|
|
151
177
|
const focusableElement = wrapper
|
|
152
178
|
.find("[data-first-button]")
|
|
153
179
|
.getDOMNode();
|
|
@@ -158,14 +184,18 @@ describe("ModalBackdrop", () => {
|
|
|
158
184
|
|
|
159
185
|
test("If there are no focusable elements, we focus the Dialog instead", async () => {
|
|
160
186
|
// Arrange
|
|
187
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
188
|
+
// to work. Changing to testing-library will likely fix this.
|
|
189
|
+
const attachElement = getElementAttachedToDocument("container");
|
|
161
190
|
const wrapper = mount(
|
|
162
191
|
<ModalBackdrop onCloseModal={() => {}}>
|
|
163
192
|
{exampleModal}
|
|
164
193
|
</ModalBackdrop>,
|
|
194
|
+
{attachTo: attachElement},
|
|
165
195
|
);
|
|
166
196
|
|
|
167
197
|
// Act
|
|
168
|
-
await
|
|
198
|
+
await wait(); // wait for styles to be applied
|
|
169
199
|
const focusableElement = wrapper
|
|
170
200
|
.find('div[role="dialog"]')
|
|
171
201
|
.getDOMNode();
|
|
@@ -173,50 +203,4 @@ describe("ModalBackdrop", () => {
|
|
|
173
203
|
// Assert
|
|
174
204
|
expect(document.activeElement).toBe(focusableElement);
|
|
175
205
|
});
|
|
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
206
|
});
|
|
@@ -5,7 +5,10 @@ import {mount, shallow} from "enzyme";
|
|
|
5
5
|
import ModalLauncher from "../modal-launcher.js";
|
|
6
6
|
import OnePaneDialog from "../one-pane-dialog.js";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import {unmountAll} from "../../../../../utils/testing/enzyme-shim.js";
|
|
9
|
+
import {getElementAttachedToDocument} from "../../../../../utils/testing/get-element-attached-to-document.js";
|
|
10
|
+
|
|
11
|
+
const wait = (duration: number = 0) =>
|
|
9
12
|
new Promise((resolve, reject) => setTimeout(resolve, duration));
|
|
10
13
|
|
|
11
14
|
const exampleModal = (
|
|
@@ -16,19 +19,41 @@ const exampleModal = (
|
|
|
16
19
|
);
|
|
17
20
|
|
|
18
21
|
describe("ModalLauncher", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.useRealTimers();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
unmountAll();
|
|
28
|
+
if (document.body) {
|
|
29
|
+
document.body.innerHTML = "";
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
19
33
|
window.scrollTo = jest.fn();
|
|
20
34
|
|
|
21
|
-
test("Children can launch the modal", () => {
|
|
35
|
+
test("Children can launch the modal", async () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
38
|
+
// to work. Changing to testing-library will likely fix this.
|
|
39
|
+
const containerDiv = getElementAttachedToDocument("container");
|
|
22
40
|
const wrapper = mount(
|
|
23
41
|
<ModalLauncher modal={exampleModal}>
|
|
24
42
|
{({openModal}) => <button onClick={openModal} />}
|
|
25
43
|
</ModalLauncher>,
|
|
44
|
+
{attachTo: containerDiv},
|
|
26
45
|
);
|
|
46
|
+
|
|
47
|
+
// Act
|
|
27
48
|
wrapper.find("button").simulate("click");
|
|
49
|
+
await wait();
|
|
50
|
+
|
|
28
51
|
const portal = global.document.querySelector(
|
|
29
52
|
"[data-modal-launcher-portal]",
|
|
30
53
|
);
|
|
31
|
-
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(portal).toBeInstanceOf(HTMLDivElement);
|
|
32
57
|
});
|
|
33
58
|
|
|
34
59
|
test("Modal can be manually opened and closed", () => {
|
|
@@ -62,7 +87,7 @@ describe("ModalLauncher", () => {
|
|
|
62
87
|
// this function receives a `closeModal` argument that works.
|
|
63
88
|
const modalFn = ({closeModal}: {|closeModal: () => void|}) => {
|
|
64
89
|
expect(opened).toBe(true);
|
|
65
|
-
|
|
90
|
+
setTimeout(closeModal, 0);
|
|
66
91
|
return exampleModal;
|
|
67
92
|
};
|
|
68
93
|
|
|
@@ -110,6 +135,7 @@ describe("ModalLauncher", () => {
|
|
|
110
135
|
|
|
111
136
|
// Simulate an Escape keypress.
|
|
112
137
|
const event: KeyboardEvent = (document.createEvent("Event"): any);
|
|
138
|
+
// $FlowIgnore[cannot-write]
|
|
113
139
|
event.key = "Escape";
|
|
114
140
|
event.initEvent("keyup", true, true);
|
|
115
141
|
document.dispatchEvent(event);
|
|
@@ -254,7 +280,7 @@ describe("ModalLauncher", () => {
|
|
|
254
280
|
wrapper.find("button").simulate("click");
|
|
255
281
|
|
|
256
282
|
// wait for styles to be applied
|
|
257
|
-
await
|
|
283
|
+
await wait();
|
|
258
284
|
|
|
259
285
|
// Assert
|
|
260
286
|
expect(document.activeElement).not.toBe(lastButton);
|
|
@@ -262,6 +288,9 @@ describe("ModalLauncher", () => {
|
|
|
262
288
|
|
|
263
289
|
test("if modal is closed, return focus to the last element focused outside the modal", async () => {
|
|
264
290
|
// Arrange
|
|
291
|
+
// We need the elements in the DOM document, it seems, for this test
|
|
292
|
+
// to work. Changing to testing-library will likely fix this.
|
|
293
|
+
const containerDiv = getElementAttachedToDocument("container");
|
|
265
294
|
let savedCloseModal = () => {
|
|
266
295
|
throw new Error(`closeModal wasn't saved`);
|
|
267
296
|
};
|
|
@@ -277,6 +306,7 @@ describe("ModalLauncher", () => {
|
|
|
277
306
|
<button onClick={openModal} data-last-focused-button />
|
|
278
307
|
)}
|
|
279
308
|
</ModalLauncher>,
|
|
309
|
+
{attachTo: containerDiv},
|
|
280
310
|
);
|
|
281
311
|
|
|
282
312
|
const lastButton = wrapper
|
|
@@ -289,7 +319,7 @@ describe("ModalLauncher", () => {
|
|
|
289
319
|
wrapper.find("button").simulate("click");
|
|
290
320
|
|
|
291
321
|
// wait for styles to be applied
|
|
292
|
-
await
|
|
322
|
+
await wait();
|
|
293
323
|
|
|
294
324
|
// Act
|
|
295
325
|
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
|
|
@@ -82,7 +77,7 @@ export default class ModalBackdrop extends React.Component<Props> {
|
|
|
82
77
|
*/
|
|
83
78
|
_getFirstFocusableElement(node: HTMLElement): HTMLElement | null {
|
|
84
79
|
// get a collection of elements that can be focused
|
|
85
|
-
const focusableElements = node
|
|
80
|
+
const focusableElements = findFocusableNodes(node);
|
|
86
81
|
|
|
87
82
|
if (!focusableElements) {
|
|
88
83
|
return null;
|
|
@@ -38,7 +38,7 @@ const customViewports = {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
export default {
|
|
41
|
-
title: "OnePaneDialog",
|
|
41
|
+
title: "Floating/Modal/OnePaneDialog",
|
|
42
42
|
parameters: {
|
|
43
43
|
viewport: {
|
|
44
44
|
viewports: customViewports,
|
|
@@ -191,14 +191,12 @@ export const withOpener: StoryComponentType = () => {
|
|
|
191
191
|
);
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
-
withOpener.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
disable: true,
|
|
202
|
-
},
|
|
194
|
+
withOpener.parameters = {
|
|
195
|
+
viewport: {
|
|
196
|
+
defaultViewport: null,
|
|
197
|
+
},
|
|
198
|
+
chromatic: {
|
|
199
|
+
// Don't take screenshots of this story since it would only show a button.
|
|
200
|
+
disableSnapshot: true,
|
|
203
201
|
},
|
|
204
202
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* List of elements that can be focused
|
|
5
|
+
* @see https://www.w3.org/TR/html5/editing.html#can-be-focused
|
|
6
|
+
*/
|
|
7
|
+
const FOCUSABLE_ELEMENTS =
|
|
8
|
+
'a[href], details, input, textarea, select, button:not([aria-label^="Close"])';
|
|
9
|
+
|
|
10
|
+
export function findFocusableNodes(
|
|
11
|
+
root: HTMLElement | Document,
|
|
12
|
+
): Array<HTMLElement> {
|
|
13
|
+
return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS));
|
|
14
|
+
}
|
|
@@ -108,9 +108,8 @@ describe("maybeGetPortalMountedModalHostElement", () => {
|
|
|
108
108
|
if (node) {
|
|
109
109
|
// Act
|
|
110
110
|
const candidateElement = ReactDOM.findDOMNode(node);
|
|
111
|
-
const result =
|
|
112
|
-
candidateElement
|
|
113
|
-
);
|
|
111
|
+
const result =
|
|
112
|
+
maybeGetPortalMountedModalHostElement(candidateElement);
|
|
114
113
|
|
|
115
114
|
// Assert
|
|
116
115
|
expect(result).toBeTruthy();
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
describe("@khanacademy/wonder-blocks-modal", () => {
|
|
3
|
-
test("package exports default", async () => {
|
|
4
|
-
// Arrange
|
|
5
|
-
const importedModule = import("../index.js");
|
|
6
|
-
|
|
7
|
-
// Act
|
|
8
|
-
const result = await importedModule;
|
|
9
|
-
|
|
10
|
-
// Assert
|
|
11
|
-
expect(Object.keys(result).sort()).toEqual(
|
|
12
|
-
[
|
|
13
|
-
"ModalDialog",
|
|
14
|
-
"ModalFooter",
|
|
15
|
-
"ModalHeader",
|
|
16
|
-
"ModalPanel",
|
|
17
|
-
"ModalLauncher",
|
|
18
|
-
"OnePaneDialog",
|
|
19
|
-
"maybeGetPortalMountedModalHostElement",
|
|
20
|
-
].sort(),
|
|
21
|
-
);
|
|
22
|
-
});
|
|
23
|
-
});
|