@khanacademy/wonder-blocks-modal 5.1.10 → 5.1.12

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +10 -10
  3. package/src/components/__tests__/close-button.test.tsx +0 -37
  4. package/src/components/__tests__/focus-trap.test.tsx +0 -100
  5. package/src/components/__tests__/modal-backdrop.test.tsx +0 -241
  6. package/src/components/__tests__/modal-dialog.test.tsx +0 -87
  7. package/src/components/__tests__/modal-header.test.tsx +0 -97
  8. package/src/components/__tests__/modal-launcher.test.tsx +0 -436
  9. package/src/components/__tests__/modal-panel.test.tsx +0 -42
  10. package/src/components/__tests__/one-pane-dialog.test.tsx +0 -87
  11. package/src/components/close-button.tsx +0 -64
  12. package/src/components/focus-trap.tsx +0 -148
  13. package/src/components/modal-backdrop.tsx +0 -172
  14. package/src/components/modal-content.tsx +0 -81
  15. package/src/components/modal-context.ts +0 -16
  16. package/src/components/modal-dialog.tsx +0 -164
  17. package/src/components/modal-footer.tsx +0 -54
  18. package/src/components/modal-header.tsx +0 -194
  19. package/src/components/modal-launcher.tsx +0 -297
  20. package/src/components/modal-panel.tsx +0 -188
  21. package/src/components/one-pane-dialog.tsx +0 -244
  22. package/src/components/scroll-disabler.ts +0 -95
  23. package/src/index.ts +0 -17
  24. package/src/themes/default.ts +0 -36
  25. package/src/themes/khanmigo.ts +0 -16
  26. package/src/themes/themed-modal-dialog.tsx +0 -44
  27. package/src/util/constants.ts +0 -6
  28. package/src/util/find-focusable-nodes.ts +0 -12
  29. package/src/util/maybe-get-portal-mounted-modal-host-element.test.tsx +0 -133
  30. package/src/util/maybe-get-portal-mounted-modal-host-element.ts +0 -35
  31. package/src/util/types.ts +0 -13
  32. package/tsconfig-build.json +0 -20
  33. package/tsconfig-build.tsbuildinfo +0 -1
@@ -1,436 +0,0 @@
1
- import * as React from "react";
2
- import {render, screen, waitFor} from "@testing-library/react";
3
- import {userEvent} from "@testing-library/user-event";
4
-
5
- import {View} from "@khanacademy/wonder-blocks-core";
6
- import Button from "@khanacademy/wonder-blocks-button";
7
-
8
- import ModalLauncher from "../modal-launcher";
9
- import OnePaneDialog from "../one-pane-dialog";
10
-
11
- const exampleModal = (
12
- <OnePaneDialog
13
- title="Modal launcher test"
14
- content={<div data-modal-child />}
15
- />
16
- );
17
-
18
- describe("ModalLauncher", () => {
19
- window.scrollTo = jest.fn();
20
-
21
- test("Children can launch the modal", async () => {
22
- // Arrange
23
- render(
24
- <ModalLauncher modal={exampleModal} testId="modal-launcher-portal">
25
- {({openModal}: any) => <button onClick={openModal} />}
26
- </ModalLauncher>,
27
- );
28
-
29
- // Act
30
- await userEvent.click(await screen.findByRole("button"));
31
-
32
- const portal = await screen.findByTestId("modal-launcher-portal");
33
-
34
- // Assert
35
- expect(portal).toBeInTheDocument();
36
- });
37
-
38
- test("Modal can be manually opened and closed", async () => {
39
- // Arrange
40
- const UnderTest = ({opened}: {opened: boolean}) => (
41
- <ModalLauncher
42
- modal={exampleModal}
43
- opened={opened}
44
- onClose={() => {}}
45
- testId="modal-launcher-portal"
46
- />
47
- );
48
- const {rerender} = render(<UnderTest opened={false} />);
49
-
50
- // Act
51
- expect(
52
- screen.queryByTestId("modal-launcher-portal"),
53
- ).not.toBeInTheDocument();
54
- rerender(<UnderTest opened={true} />);
55
- expect(
56
- await screen.findByTestId("modal-launcher-portal"),
57
- ).toBeInTheDocument();
58
- rerender(<UnderTest opened={false} />);
59
- expect(
60
- screen.queryByTestId("modal-launcher-portal"),
61
- ).not.toBeInTheDocument();
62
- });
63
-
64
- test("Modal can close itself after launching", async () => {
65
- // Arrange
66
- const modalFn = ({closeModal}: {closeModal: () => void}) => (
67
- <OnePaneDialog
68
- title="Modal launcher test"
69
- content={
70
- <View>
71
- <Button onClick={closeModal}>Close it!</Button>
72
- </View>
73
- }
74
- />
75
- );
76
-
77
- const onCloseMock = jest.fn();
78
-
79
- // Mount the modal launcher. This shouldn't trigger any closing yet,
80
- // because we shouldn't be calling the `modal` function yet.
81
- render(
82
- <ModalLauncher
83
- modal={modalFn}
84
- onClose={onCloseMock}
85
- testId="modal-launcher-portal"
86
- >
87
- {({openModal}: any) => <button onClick={openModal} />}
88
- </ModalLauncher>,
89
- );
90
-
91
- await userEvent.click(await screen.findByRole("button"));
92
-
93
- // wait until the modal is open
94
- await screen.findByRole("dialog");
95
-
96
- // Act
97
- await userEvent.click(
98
- await screen.findByRole("button", {name: "Close it!"}),
99
- );
100
-
101
- // Assert
102
- expect(onCloseMock).toHaveBeenCalled();
103
- });
104
-
105
- // TODO(FEI-5533): Key press events aren't working correctly with
106
- // user-event v14. We need to investigate and fix this.
107
- test.skip("Pressing Escape closes the modal", async () => {
108
- // Arrange
109
- render(
110
- <ModalLauncher modal={exampleModal}>
111
- {({openModal}: any) => <button onClick={openModal} />}
112
- </ModalLauncher>,
113
- );
114
-
115
- // Launch the modal.
116
- await userEvent.click(await screen.findByRole("button"));
117
-
118
- // wait until the modal is open
119
- await screen.findByRole("dialog");
120
-
121
- // Act
122
- // Simulate an Escape keypress.
123
- await userEvent.keyboard("{esc}");
124
-
125
- // Assert
126
- // Confirm that the modal is no longer mounted.
127
- await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
128
- });
129
-
130
- test("Disable scrolling when the modal is open", async () => {
131
- // Arrange
132
- render(
133
- <ModalLauncher modal={exampleModal}>
134
- {({openModal}: any) => <button onClick={openModal} />}
135
- </ModalLauncher>,
136
- );
137
-
138
- // Act
139
- // Launch the modal.
140
- await userEvent.click(await screen.findByRole("button"));
141
-
142
- // wait until the modal is open
143
- await screen.findByRole("dialog");
144
-
145
- // Assert
146
- // Now that the modal is open, there should be a ScrollDisabler.
147
- expect(document.body).toHaveStyle("overflow: hidden");
148
- });
149
-
150
- test("re-enable scrolling after the modal is closed", async () => {
151
- // Arrange
152
- render(
153
- <ModalLauncher modal={exampleModal}>
154
- {({openModal}: any) => <button onClick={openModal} />}
155
- </ModalLauncher>,
156
- );
157
-
158
- // Launch the modal.
159
- await userEvent.click(await screen.findByRole("button"));
160
-
161
- await screen.findByRole("dialog");
162
-
163
- // Close the modal.
164
- await userEvent.click(
165
- await screen.findByRole("button", {name: "Close modal"}),
166
- );
167
-
168
- // Assert
169
- // Now that the modal is closed, there should be no ScrollDisabler.
170
- expect(document.body).not.toHaveStyle("overflow: hidden");
171
- });
172
-
173
- test("using `opened` and `children` should warn", async () => {
174
- // Arrange
175
- jest.spyOn(console, "warn").mockImplementation(() => {});
176
-
177
- // Act
178
- render(
179
- <ModalLauncher
180
- modal={exampleModal}
181
- opened={false}
182
- onClose={() => {}}
183
- >
184
- {({openModal}: any) => <button onClick={openModal} />}
185
- </ModalLauncher>,
186
- );
187
-
188
- // Assert
189
- // eslint-disable-next-line no-console
190
- expect(console.warn).toHaveBeenCalledWith(
191
- "'children' and 'opened' can't be used together",
192
- );
193
- });
194
-
195
- test("using `opened` without `onClose` should throw", async () => {
196
- // Arrange
197
- jest.spyOn(console, "warn").mockImplementation(() => {});
198
-
199
- // Act
200
- render(<ModalLauncher modal={exampleModal} opened={false} />);
201
-
202
- // Assert
203
- // eslint-disable-next-line no-console
204
- expect(console.warn).toHaveBeenCalledWith(
205
- "'onClose' should be used with 'opened'",
206
- );
207
- });
208
-
209
- test("using neither `opened` nor `children` should throw", async () => {
210
- // Arrange
211
- jest.spyOn(console, "warn").mockImplementation(() => {});
212
-
213
- // Act
214
- render(<ModalLauncher modal={exampleModal} />);
215
-
216
- // Assert
217
- // eslint-disable-next-line no-console
218
- expect(console.warn).toHaveBeenCalledWith(
219
- "either 'children' or 'opened' must be set",
220
- );
221
- });
222
-
223
- test("If backdropDismissEnabled set to false, clicking the backdrop does not trigger `onClose`", async () => {
224
- // Arrange
225
- const onClose = jest.fn();
226
-
227
- render(
228
- <ModalLauncher
229
- onClose={onClose}
230
- modal={exampleModal}
231
- opened={true}
232
- backdropDismissEnabled={false}
233
- testId="modal-launcher-backdrop"
234
- />,
235
- );
236
-
237
- // Act
238
- const backdrop = await screen.findByTestId("modal-launcher-backdrop");
239
- await userEvent.click(backdrop);
240
-
241
- // Assert
242
- expect(onClose).not.toHaveBeenCalled();
243
- });
244
-
245
- test("if modal is launched, move focus inside the modal", async () => {
246
- // Arrange
247
- render(
248
- <ModalLauncher
249
- modal={
250
- <OnePaneDialog
251
- title="Modal launcher test"
252
- content={
253
- <View>
254
- <Button>Button in modal</Button>
255
- </View>
256
- }
257
- />
258
- }
259
- >
260
- {({openModal}: any) => (
261
- <button onClick={openModal}>Open modal</button>
262
- )}
263
- </ModalLauncher>,
264
- );
265
-
266
- const modalOpener = await screen.findByRole("button", {
267
- name: "Open modal",
268
- });
269
- // force focus
270
- modalOpener.focus();
271
-
272
- // Act
273
- // Launch the modal.
274
- await userEvent.type(modalOpener, "{enter}");
275
-
276
- // wait until the modal is open
277
- await screen.findByRole("dialog");
278
-
279
- // Assert
280
- await waitFor(async () =>
281
- expect(
282
- await screen.findByRole("button", {name: "Button in modal"}),
283
- ).toHaveFocus(),
284
- );
285
- });
286
-
287
- test("if modal is closed, return focus to the last element focused outside the modal", async () => {
288
- // Arrange
289
- const ModalLauncherWrapper = () => {
290
- const [opened, setOpened] = React.useState(false);
291
-
292
- const handleOpen = () => {
293
- setOpened(true);
294
- };
295
-
296
- const handleClose = () => {
297
- setOpened(false);
298
- };
299
-
300
- return (
301
- <View>
302
- <Button>Top of page (should not receive focus)</Button>
303
- <Button
304
- testId="launcher-button"
305
- onClick={() => handleOpen()}
306
- >
307
- Open modal
308
- </Button>
309
- <ModalLauncher
310
- onClose={() => handleClose()}
311
- opened={opened}
312
- modal={({closeModal}: any) => (
313
- <OnePaneDialog
314
- title="Regular modal"
315
- content={<View>Hello World</View>}
316
- footer={
317
- <Button
318
- testId="modal-close-button"
319
- onClick={closeModal}
320
- >
321
- Close Modal
322
- </Button>
323
- }
324
- />
325
- )}
326
- />
327
- </View>
328
- );
329
- };
330
-
331
- render(<ModalLauncherWrapper />);
332
-
333
- const lastButton = await screen.findByTestId("launcher-button");
334
-
335
- // Launch the modal.
336
- await userEvent.click(lastButton);
337
-
338
- // Act
339
- // Close modal
340
- const modalCloseButton = await screen.findByTestId(
341
- "modal-close-button",
342
- );
343
- await userEvent.click(modalCloseButton);
344
-
345
- // Assert
346
- await waitFor(() => {
347
- expect(lastButton).toHaveFocus();
348
- });
349
- });
350
-
351
- test("if `closedFocusId` is passed, shift focus to specified element after the modal closes", async () => {
352
- // Arrange
353
- const ModalLauncherWrapper = () => {
354
- const [opened, setOpened] = React.useState(false);
355
-
356
- const handleOpen = () => {
357
- setOpened(true);
358
- };
359
-
360
- const handleClose = () => {
361
- setOpened(false);
362
- };
363
-
364
- return (
365
- <View>
366
- <Button>Top of page (should not receive focus)</Button>
367
- <Button id="button-to-focus-on" testId="focused-button">
368
- Focus here after close
369
- </Button>
370
- <Button
371
- testId="launcher-button"
372
- onClick={() => handleOpen()}
373
- >
374
- Open modal
375
- </Button>
376
- <ModalLauncher
377
- onClose={() => handleClose()}
378
- opened={opened}
379
- closedFocusId="button-to-focus-on"
380
- modal={({closeModal}: any) => (
381
- <OnePaneDialog
382
- title="Triggered from action menu"
383
- content={<View>Hello World</View>}
384
- footer={
385
- <Button
386
- testId="modal-close-button"
387
- onClick={closeModal}
388
- >
389
- Close Modal
390
- </Button>
391
- }
392
- />
393
- )}
394
- />
395
- </View>
396
- );
397
- };
398
-
399
- render(<ModalLauncherWrapper />);
400
-
401
- // Launch modal
402
- const launcherButton = await screen.findByTestId("launcher-button");
403
- await userEvent.click(launcherButton);
404
-
405
- // Act
406
- // Close modal
407
- const modalCloseButton = await screen.findByTestId(
408
- "modal-close-button",
409
- );
410
- await userEvent.click(modalCloseButton);
411
-
412
- // Assert
413
- const focusedButton = await screen.findByTestId("focused-button");
414
- await waitFor(() => {
415
- expect(focusedButton).toHaveFocus();
416
- });
417
- });
418
-
419
- test("testId should be added to the Backdrop", async () => {
420
- // Arrange
421
- render(
422
- <ModalLauncher
423
- opened={true}
424
- onClose={jest.fn()}
425
- modal={<div role="dialog">dialog</div>}
426
- testId="test-id-example"
427
- />,
428
- );
429
-
430
- // Act
431
- const backdrop = await screen.findByTestId("test-id-example");
432
-
433
- // Assert
434
- expect(backdrop).toBeInTheDocument();
435
- });
436
- });
@@ -1,42 +0,0 @@
1
- import * as React from "react";
2
- import {render, screen} from "@testing-library/react";
3
-
4
- import expectRenderError from "../../../../../utils/testing/expect-render-error";
5
- import ModalPanel from "../modal-panel";
6
- import ModalContext from "../modal-context";
7
-
8
- describe("ModalPanel", () => {
9
- test("ModalContext.Provider and onClose should warn", () => {
10
- expectRenderError(
11
- <ModalContext.Provider value={{closeModal: () => {}}}>
12
- <ModalPanel
13
- content="Hello, world"
14
- onClose={() => {}}
15
- closeButtonVisible={true}
16
- />
17
- </ModalContext.Provider>,
18
- "You've specified 'onClose' on a modal when using ModalLauncher. Please specify 'onClose' on the ModalLauncher instead",
19
- );
20
- });
21
-
22
- test("testId should be added to the panel wrapper", () => {
23
- // Arrange
24
- render(<ModalPanel content="dummy content" testId="test-id" />);
25
-
26
- // Act
27
-
28
- // Assert
29
- expect(screen.getByTestId("test-id-panel")).toBeInTheDocument();
30
- });
31
-
32
- test("testId should be added to the CloseButton element", () => {
33
- // Arrange
34
- render(<ModalPanel content="dummy content" testId="test-id" />);
35
-
36
- // Act
37
- const closeButton = screen.getByLabelText("Close modal");
38
-
39
- // Assert
40
- expect(closeButton).toHaveAttribute("data-testid", "test-id-close");
41
- });
42
- });
@@ -1,87 +0,0 @@
1
- import * as React from "react";
2
- import {render, screen} from "@testing-library/react";
3
-
4
- import {
5
- Breadcrumbs,
6
- BreadcrumbsItem,
7
- } from "@khanacademy/wonder-blocks-breadcrumbs";
8
- import OnePaneDialog from "../one-pane-dialog";
9
-
10
- describe("OnePaneDialog", () => {
11
- test("testId should be set in the Dialog element", () => {
12
- // Arrange
13
- render(
14
- <OnePaneDialog
15
- title="Dialog with multi-step footer"
16
- content="dummy content"
17
- testId="one-pane-dialog-example"
18
- />,
19
- );
20
-
21
- // Act
22
- const dialog = screen.getByRole("dialog");
23
-
24
- // Assert
25
- expect(dialog).toHaveAttribute(
26
- "data-testid",
27
- "one-pane-dialog-example",
28
- );
29
- });
30
-
31
- test("role can be overriden to alertdialog", () => {
32
- // Arrange
33
- render(
34
- <OnePaneDialog
35
- title="Dialog with multi-step footer"
36
- subtitle="Dialog subtitle"
37
- content="dummy content"
38
- testId="one-pane-dialog-example"
39
- role="alertdialog"
40
- />,
41
- );
42
-
43
- // Act
44
- const dialog = screen.getByRole("alertdialog");
45
-
46
- // Assert
47
- expect(dialog).toBeInTheDocument();
48
- });
49
-
50
- test("should include breadcrumbs", () => {
51
- // Arrange
52
- render(
53
- <OnePaneDialog
54
- title="Dialog with multi-step footer"
55
- breadcrumbs={
56
- <Breadcrumbs>
57
- <BreadcrumbsItem>test</BreadcrumbsItem>
58
- </Breadcrumbs>
59
- }
60
- content="dummy content"
61
- testId="one-pane-dialog-example"
62
- />,
63
- );
64
-
65
- // Act
66
-
67
- // Assert
68
- expect(screen.getByLabelText("Breadcrumbs")).toBeInTheDocument();
69
- });
70
-
71
- it("applies aria-describedby to the modal", () => {
72
- // Arrange
73
- render(
74
- <OnePaneDialog
75
- title="unused"
76
- content={<p id="description">cool dialog</p>}
77
- aria-describedby="description"
78
- />,
79
- );
80
-
81
- // Act
82
- const modal = screen.getByRole("dialog");
83
-
84
- // Assert
85
- expect(modal).toHaveDescription(/cool dialog/i);
86
- });
87
- });
@@ -1,64 +0,0 @@
1
- import * as React from "react";
2
- import xIcon from "@phosphor-icons/core/regular/x.svg";
3
- import IconButton from "@khanacademy/wonder-blocks-icon-button";
4
- import type {StyleType} from "@khanacademy/wonder-blocks-core";
5
-
6
- import ModalContext from "./modal-context";
7
-
8
- type Props = {
9
- /**
10
- * Whether the button is on a dark/colored background.
11
- *
12
- * Sets primary button background color to white, and secondary and
13
- * tertiary button title to color.
14
- */
15
- light?: boolean;
16
- /** Optional click handler */
17
- onClick?: () => unknown;
18
- /** Optional custom styles. */
19
- style?: StyleType;
20
- /**
21
- * Test ID used for e2e testing.
22
- *
23
- * In this case, this component is internal, so `testId` is composed with
24
- * the `testId` passed down from the Dialog variant + a suffix to scope it
25
- * to this component.
26
- *
27
- * @example
28
- * For testId="some-random-id"
29
- * The result will be: `some-random-id-modal-panel`
30
- */
31
- testId?: string;
32
- };
33
-
34
- export default class CloseButton extends React.Component<Props> {
35
- render(): React.ReactNode {
36
- const {light, onClick, style, testId} = this.props;
37
-
38
- return (
39
- <ModalContext.Consumer>
40
- {({closeModal}) => {
41
- if (closeModal && onClick) {
42
- throw new Error(
43
- "You've specified 'onClose' on a modal when using ModalLauncher. Please specify 'onClose' on the ModalLauncher instead",
44
- );
45
- }
46
-
47
- return (
48
- <IconButton
49
- icon={xIcon}
50
- // TODO(mdr): Translate this string for i18n.
51
- // TODO(kevinb): provide a way to set this label
52
- aria-label="Close modal"
53
- onClick={onClick || closeModal}
54
- kind={light ? "primary" : "tertiary"}
55
- light={light}
56
- style={style}
57
- testId={testId}
58
- />
59
- );
60
- }}
61
- </ModalContext.Consumer>
62
- );
63
- }
64
- }