@khanacademy/wonder-blocks-modal 2.3.7 → 2.3.9

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @khanacademy/wonder-blocks-modal
2
2
 
3
+ ## 2.3.9
4
+
5
+ ### Patch Changes
6
+
7
+ - @khanacademy/wonder-blocks-icon-button@3.4.13
8
+
9
+ ## 2.3.8
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [3bae2aba]
14
+ - @khanacademy/wonder-blocks-icon@1.2.31
15
+ - @khanacademy/wonder-blocks-icon-button@3.4.12
16
+
3
17
  ## 2.3.7
4
18
 
5
19
  ### Patch Changes
package/docs.md CHANGED
@@ -1,492 +1,5 @@
1
- Looking for docs for StandardModal, OneColumnModal, or TwoColumnModal click
2
- [here](https://deploy-preview-389--wonder-blocks.netlify.com/#modal)
1
+ Documentation for `@khanacademy/wonder-blocks-modal` is now in Storybook.
3
2
 
4
- ## Examples
5
-
6
- ### Example: Default modal
7
-
8
- Once the modal is launched, tab focus wraps inside the modal content. Pressing Tab at the end of the modal will focus the modal's first element, and pressing Shift-Tab at the start of the modal will focus the modal's last element.
9
-
10
- ```js
11
- import {StyleSheet} from "aphrodite";
12
-
13
- import {ModalLauncher, OnePaneDialog} from "@khanacademy/wonder-blocks-modal";
14
- import {View} from "@khanacademy/wonder-blocks-core";
15
- import {Body} from "@khanacademy/wonder-blocks-typography";
16
- import Button from "@khanacademy/wonder-blocks-button";
17
- import Spacing from "@khanacademy/wonder-blocks-spacing";
18
-
19
- const styles = StyleSheet.create({
20
- example: {
21
- padding: Spacing.xLarge_32,
22
- alignItems: "center",
23
- },
24
-
25
- title: {
26
- marginBottom: Spacing.medium_16,
27
- },
28
-
29
- modalContent: {
30
- margin: "0 auto",
31
- maxWidth: 544,
32
- },
33
-
34
- above: {
35
- background: "url(/modal-above.png)",
36
- width: 874,
37
- height: 551,
38
- position: "absolute",
39
- top: 40,
40
- left: -140
41
- },
42
-
43
- below: {
44
- background: "url(/modal-below.png)",
45
- width: 868,
46
- height: 521,
47
- position: "absolute",
48
- top: -100,
49
- left: -300
50
- },
51
- });
52
-
53
- const onePaneDialog = ({closeModal}) => (
54
- <OnePaneDialog
55
- title="Title"
56
- subtitle="You're reading the subtitle!"
57
- above={<View style={styles.above} />}
58
- below={<View style={styles.below} />}
59
- testId="dialog-default-example"
60
- content={
61
- <View style={styles.modalContent}>
62
- <Body tag="p">
63
- {
64
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est."
65
- }
66
- </Body>
67
- </View>
68
- }
69
- footer={
70
- <Button onClick={closeModal}>
71
- Close modal
72
- </Button>
73
- }
74
- />
75
- );
76
-
77
- <View style={styles.example}>
78
- <ModalLauncher
79
- modal={onePaneDialog}
80
- testId="modal-launcher-default-example"
81
- >
82
- {({openModal}) => <Button onClick={openModal}>OnePaneDialog</Button>}
83
- </ModalLauncher>
84
- </View>;
85
- ```
86
-
87
- ### Example: Disabling backdrop dismission
88
-
89
- By default, `ModalLauncher` allows you to close the modal by clicking on the overlay/backdrop window. Somethimes you might need to disable it, and to to this, you can set `backdropDismissEnabled` to `false`.
90
-
91
- ```js
92
- import {StyleSheet} from "aphrodite";
93
-
94
- import {ModalLauncher, OnePaneDialog} from "@khanacademy/wonder-blocks-modal";
95
- import {View} from "@khanacademy/wonder-blocks-core";
96
- import {Body} from "@khanacademy/wonder-blocks-typography";
97
- import Button from "@khanacademy/wonder-blocks-button";
98
- import Spacing from "@khanacademy/wonder-blocks-spacing";
99
-
100
- const styles = StyleSheet.create({
101
- example: {
102
- padding: Spacing.xLarge_32,
103
- alignItems: "center",
104
- },
105
-
106
- modalContent: {
107
- margin: "0 auto",
108
- maxWidth: 544,
109
- },
110
- });
111
-
112
- const exampleModal = ({closeModal}) => (
113
- <OnePaneDialog
114
- title="Backdrop dismission disabled"
115
- content={
116
- <View style={styles.modalContent}>
117
- <Body tag="p">
118
- {
119
- "This window won't be closed if you click/tap outside of the ModalPanel. To do that, you can still press `esc` or use the close button located on the top right."
120
- }
121
- </Body>
122
- </View>
123
- }
124
- />
125
- );
126
-
127
- <View style={styles.example}>
128
- <ModalLauncher modal={exampleModal} backdropDismissEnabled={false}>
129
- {({openModal}) => <Button onClick={openModal}>Open modal</Button>}
130
- </ModalLauncher>
131
- </View>
132
- ```
133
-
134
- ### Example: Triggering programmatically
135
-
136
- Sometimes you'll want to trigger a modal programmatically. This can be done by
137
- rendering `ModalLauncher` without any children and instead setting its `opened`
138
- prop to `true`. In this situation `ModalLauncher` is a controlled component
139
- which means you'll also have to update `opened` to `false` in response to the
140
- `onClose` callback being triggered.
141
-
142
- ```js
143
- import {ModalLauncher, OnePaneDialog} from "@khanacademy/wonder-blocks-modal";
144
- import {Title} from "@khanacademy/wonder-blocks-typography";
145
- import {View} from "@khanacademy/wonder-blocks-core";
146
- import Button from "@khanacademy/wonder-blocks-button";
147
- import {ActionMenu, ActionItem} from "@khanacademy/wonder-blocks-dropdown";
148
-
149
- class Example extends React.Component {
150
- constructor(props) {
151
- super(props);
152
- this.state = {
153
- opened: false,
154
- };
155
- }
156
-
157
- handleOpen() {
158
- console.log('opening modal');
159
- this.setState({opened: true});
160
- }
161
-
162
- handleClose() {
163
- console.log('closing modal');
164
- this.setState({opened: false});
165
- }
166
-
167
- render() {
168
- return <View>
169
- <ActionMenu menuText="actions">
170
- <ActionItem
171
- label="Open modal"
172
- onClick={() => this.handleOpen()}
173
- />
174
- </ActionMenu>
175
- <ModalLauncher
176
- onClose={() => this.handleClose()}
177
- opened={this.state.opened}
178
- modal={({closeModal}) => (
179
- <OnePaneDialog
180
- title="Triggered from action menu"
181
- content={
182
- <View>
183
- <Title>Hello, world</Title>
184
- </View>
185
- }
186
- footer={
187
- <Button onClick={closeModal}>
188
- Close Modal
189
- </Button>
190
- }
191
- />
192
- )}
193
- />
194
- </View>;
195
- }
196
- }
197
-
198
- <Example />
199
- ```
200
-
201
- **Warning:** Do not wrap items in a dropdown in a `ModalLauncher`. Instead, trigger
202
- the modal programmatically by using the `ModalLauncher` as an uncontrolled component
203
- as shown in the above example.
204
-
205
- This is necessary because wrapping an item in `ModalLauncher` will result in the
206
- modal disappearing as soon as the focus changes. The reason is that the change in
207
- focus results in the item that in the dropdown that was clicked to be blur which
208
- closes the dropdown. This results in all of its children to unmount including the
209
- ModalLauncher which was wrapping the menu item.
210
-
211
-
212
- ## Accessibility
213
-
214
- It should follow guidelines from [W3C](https://www.w3.org/TR/wai-aria-practices/#dialog_modal).
215
-
216
- ### Keyboard Interaction
217
-
218
- When a dialog opens, focus moves to an element inside the dialog. See notes below regarding initial focus placement.
219
-
220
- - Tab:
221
- - Moves focus to the next tabbable element inside the dialog.
222
- - If focus is on the last tabbable element inside the dialog, moves focus to the first tabbable element inside the dialog.
223
- - Shift + Tab:
224
- - Moves focus to the previous tabbable element inside the dialog.
225
- - If focus is on the first tabbable element inside the dialog, moves focus to the last tabbable element inside the dialog.
226
- - Escape: Closes the dialog.
227
-
228
- #### Initial focus placement:
229
- The initial focus placement depends on the following scenarios:
230
-
231
- 1. `initialFocusId` (default): `ModalLauncher` exposes this prop as a string. The dialog will try to find this element into the DOM. If it's found, focus is initially set on this element.
232
- 2. focusable elements: This is the second scenario, where the dialog tries to find the first ocurrence of possible focusable elements.
233
- 3. Dialog: If the first two conditions are not met, then focus is initially set to the Dialog element itself.
234
-
235
- ### Example: Set initial focus on a given element inside the modal
236
-
237
- ```js
238
- import {StyleSheet} from "aphrodite";
239
-
240
- import {ModalLauncher, OnePaneDialog} from "@khanacademy/wonder-blocks-modal";
241
- import {Title} from "@khanacademy/wonder-blocks-typography";
242
- import {View} from "@khanacademy/wonder-blocks-core";
243
- import Button from "@khanacademy/wonder-blocks-button";
244
- import {Strut} from "@khanacademy/wonder-blocks-layout";
245
- import Spacing from "@khanacademy/wonder-blocks-spacing";
246
-
247
- const styles = StyleSheet.create({
248
- example: {
249
- padding: Spacing.xLarge_32,
250
- alignItems: "center",
251
- }
252
- });
253
-
254
- const modalInitialFocus = ({closeModal}) => (
255
- <OnePaneDialog
256
- title="Single-line title"
257
- content={
258
- <View>
259
- <View>
260
- <label>Label</label>
261
- <input type="text" />
262
- <Strut size={Spacing.medium_16} />
263
- <Button id="initial-focus">
264
- Button to receive initial focus
265
- </Button>
266
- </View>
267
- </View>
268
- }
269
- footer={
270
- <React.Fragment>
271
- <Button kind="tertiary" onClick={closeModal}>
272
- Cancel
273
- </Button>
274
- <Strut size={Spacing.medium_16} />
275
- <Button>
276
- Submit
277
- </Button>
278
- </React.Fragment>
279
- }
280
- />
281
- );
282
-
283
- <View style={styles.example}>
284
- <ModalLauncher
285
- onClose={() => window.alert("you closed the modal")}
286
- initialFocusId="initial-focus"
287
- modal={modalInitialFocus}
288
- >
289
- {({openModal}) => <Button onClick={openModal}>Open modal with initial focus</Button>}
290
- </ModalLauncher>
291
- </View>
292
- ```
293
-
294
- ### WAI-ARIA Roles
295
- - The element that serves as the **dialog container** has `aria-role` defined as `dialog`.
296
- - The dialog has a value set for the `aria-labelledby` property that refers to a **visible dialog title**.
297
-
298
- ## Customization
299
-
300
- ### Example: Flexible dialogs
301
-
302
- This example illustrates how we can update the Modal's contents by wrapping it into a new component/container. **Modal** is built in a way that provides great flexibility and makes it work with different variations and/or layouts (see Custom Two-Pane Dialog example).
303
-
304
- ```js
305
- import {StyleSheet} from "aphrodite";
306
-
307
- import {ModalLauncher, OnePaneDialog} from "@khanacademy/wonder-blocks-modal";
308
- import Button from "@khanacademy/wonder-blocks-button";
309
- import {View} from "@khanacademy/wonder-blocks-core";
310
- import {Strut} from "@khanacademy/wonder-blocks-layout";
311
- import Spacing from "@khanacademy/wonder-blocks-spacing";
312
- import {Body, LabelLarge} from "@khanacademy/wonder-blocks-typography";
313
-
314
- const styles = StyleSheet.create({
315
- example: {
316
- padding: Spacing.xLarge_32,
317
- alignItems: "center",
318
- },
319
- row: {
320
- flexDirection: "row",
321
- justifyContent: "flex-end"
322
- },
323
- footer: {
324
- alignItems: "center",
325
- flexDirection: "row",
326
- justifyContent: "space-between",
327
- width: "100%"
328
- }
329
- });
330
-
331
- class ExerciseModal extends React.Component {
332
- render() {
333
- const {current, handleNextButton, handlePrevButton, question, total} = this.props;
334
-
335
- return (
336
- <OnePaneDialog
337
- title="Exercises"
338
- content={
339
- <View>
340
- <Body>This is the current question: {question}</Body>
341
- </View>
342
- }
343
- footer={
344
- <View style={styles.footer}>
345
- <LabelLarge>Step {current+1} of {total}</LabelLarge>
346
- <View style={styles.row}>
347
- <Button kind="tertiary" onClick={handlePrevButton}>Previous</Button>
348
- <Strut size={16} />
349
- <Button kind="primary" onClick={handleNextButton}>Next</Button>
350
- </View>
351
- </View>
352
- }
353
- />
354
- );
355
- }
356
- }
357
-
358
- class ExerciseContainer extends React.Component {
359
- constructor(props) {
360
- super(props);
361
-
362
- this.state = {
363
- currentQuestion: 0
364
- };
365
- }
366
-
367
- handleNextButton() {
368
- this.setState({
369
- currentQuestion: Math.min(this.state.currentQuestion + 1, this.props.questions.length - 1),
370
- });
371
- };
372
-
373
- handlePrevButton() {
374
- this.setState({
375
- currentQuestion: Math.max(0, this.state.currentQuestion - 1),
376
- });
377
- }
378
-
379
- render() {
380
- return (
381
- <ModalLauncher
382
- onClose={() => console.log("you closed the modal")}
383
- modal={
384
- <ExerciseModal
385
- question={this.props.questions[this.state.currentQuestion]}
386
- current={this.state.currentQuestion}
387
- total={this.props.questions.length}
388
- handlePrevButton={() => this.handlePrevButton()}
389
- handleNextButton={() => this.handleNextButton()}
390
- />
391
- }
392
- >
393
- {({openModal}) => <Button onClick={openModal}>Open flexible modal</Button>}
394
- </ModalLauncher>
395
- );
396
- }
397
- }
398
-
399
- <View style={styles.example}>
400
- <ExerciseContainer questions={["First question", "Second question", "Last question"]} />
401
- </View>
402
- ```
403
-
404
- ### Example: Dialogs with custom styles
405
-
406
- Sometimes you'll want to customize the styling of the **Dialog** .e.g., custom width or height. You can pass in `style` which will customize the styling of the modal wrapper.
407
- To use styling for different screen sizes, wrap your component with `MediaLayout` component. Please see example code below for details.
408
-
409
- ```js
410
- import {StyleSheet} from "aphrodite";
411
-
412
- import {OnePaneDialog} from "@khanacademy/wonder-blocks-modal";
413
- import {View} from "@khanacademy/wonder-blocks-core";
414
- import {Title, Body} from "@khanacademy/wonder-blocks-typography";
415
- import {MediaLayout} from "@khanacademy/wonder-blocks-layout";
416
-
417
- const styles = StyleSheet.create({
418
- previewSizer: {
419
- height: 512,
420
- },
421
-
422
- modalPositioner: {
423
- // Checkerboard background
424
- backgroundImage: "linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)",
425
- backgroundSize: "20px 20px",
426
- backgroundPosition: "0 0, 0 10px, 10px -10px, -10px 0px",
427
-
428
- display: "flex",
429
- flexDirection: "row",
430
- alignItems: "center",
431
- justifyContent: "center",
432
-
433
- position: "absolute",
434
- left: 0,
435
- right: 0,
436
- top: 0,
437
- bottom: 0,
438
- },
439
- });
440
-
441
- const styleSheets = {
442
- mdOrLarger: StyleSheet.create({
443
- customModal: {
444
- maxWidth: 300,
445
- maxHeight: 200,
446
- },
447
-
448
- below: {
449
- background: "url(/blue-blob.png)",
450
- backgroundSize: "cover",
451
- width: 294,
452
- height: 306,
453
- position: "absolute",
454
- top: 0,
455
- left: -60
456
- },
457
-
458
- above: {
459
- background: "url(/asteroid.png)",
460
- backgroundSize: "cover",
461
- width: 418,
462
- height: 260,
463
- position: "absolute",
464
- top: -10,
465
- left: -5
466
- },
467
- }),
468
- };
469
-
470
- <View style={styles.previewSizer}>
471
- <View style={styles.modalPositioner}>
472
- <MediaLayout styleSheets={styleSheets}>
473
- {({styles}) => (
474
- <OnePaneDialog
475
- style={styles.customModal}
476
- title="Single-line title"
477
- content={
478
- <View>
479
- <Body>
480
- Hello World!
481
- </Body>
482
- </View>
483
- }
484
- onClose={() => alert("This would close the modal.")}
485
- below={<View style={styles.below} />}
486
- above={<View style={styles.above} />}
487
- />
488
- )}
489
- </MediaLayout>
490
- </View>
491
- </View>;
492
- ```
3
+ Visit the [Storybook
4
+ Modal](https://khan.github.io/wonder-blocks/?path=/docs/modal) docs on GitHub
5
+ Pages.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-modal",
3
- "version": "2.3.7",
3
+ "version": "2.3.9",
4
4
  "design": "v2",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -19,8 +19,8 @@
19
19
  "@khanacademy/wonder-blocks-breadcrumbs": "^1.0.33",
20
20
  "@khanacademy/wonder-blocks-color": "^1.2.0",
21
21
  "@khanacademy/wonder-blocks-core": "^4.4.0",
22
- "@khanacademy/wonder-blocks-icon": "^1.2.30",
23
- "@khanacademy/wonder-blocks-icon-button": "^3.4.11",
22
+ "@khanacademy/wonder-blocks-icon": "^1.2.31",
23
+ "@khanacademy/wonder-blocks-icon-button": "^3.4.13",
24
24
  "@khanacademy/wonder-blocks-layout": "^1.4.11",
25
25
  "@khanacademy/wonder-blocks-spacing": "^3.0.5",
26
26
  "@khanacademy/wonder-blocks-toolbar": "^2.1.34",
@@ -156,7 +156,7 @@ describe("ModalLauncher", () => {
156
156
  });
157
157
 
158
158
  test("Disable scrolling when the modal is open", () => {
159
- let savedCloseModal = () => {
159
+ let savedCloseModal: () => void = () => {
160
160
  throw new Error(`closeModal wasn't saved`);
161
161
  };
162
162
 
@@ -1,14 +1,10 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
- import {mount} from "enzyme";
4
- import "jest-enzyme";
5
-
6
- import {View} from "@khanacademy/wonder-blocks-core";
3
+ import {render, screen} from "@testing-library/react";
7
4
 
8
5
  import expectRenderError from "../../../../../utils/testing/expect-render-error.js";
9
6
  import ModalPanel from "../modal-panel.js";
10
7
  import ModalContext from "../modal-context.js";
11
- import CloseButton from "../close-button.js";
12
8
 
13
9
  describe("ModalPanel", () => {
14
10
  test("ModalContext.Provider and onClose should warn", () => {
@@ -26,27 +22,22 @@ describe("ModalPanel", () => {
26
22
 
27
23
  test("testId should be added to the panel wrapper", () => {
28
24
  // Arrange
29
- const wrapper = mount(
30
- <ModalPanel content="dummy content" testId="test-id" />,
31
- );
25
+ render(<ModalPanel content="dummy content" testId="test-id" />);
32
26
 
33
27
  // Act
34
- const mainElement = wrapper.find(View).first();
35
28
 
36
29
  // Assert
37
- expect(mainElement.prop("testId")).toBe("test-id-panel");
30
+ expect(screen.getByTestId("test-id-panel")).toBeInTheDocument();
38
31
  });
39
32
 
40
33
  test("testId should be added to the CloseButton element", () => {
41
34
  // Arrange
42
- const wrapper = mount(
43
- <ModalPanel content="dummy content" testId="test-id" />,
44
- );
35
+ render(<ModalPanel content="dummy content" testId="test-id" />);
45
36
 
46
37
  // Act
47
- const closeButton = wrapper.find(CloseButton);
38
+ const closeButton = screen.getByLabelText("Close modal");
48
39
 
49
40
  // Assert
50
- expect(closeButton.prop("testId")).toBe("test-id-close");
41
+ expect(closeButton).toHaveAttribute("data-test-id", "test-id-close");
51
42
  });
52
43
  });
@@ -1,14 +1,17 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
- import {mount} from "enzyme";
4
- import "jest-enzyme";
3
+ import {render, screen} from "@testing-library/react";
5
4
 
5
+ import {
6
+ Breadcrumbs,
7
+ BreadcrumbsItem,
8
+ } from "@khanacademy/wonder-blocks-breadcrumbs";
6
9
  import OnePaneDialog from "../one-pane-dialog.js";
7
10
 
8
11
  describe("OnePaneDialog", () => {
9
12
  test("testId should be set in the Dialog element", () => {
10
13
  // Arrange
11
- const wrapper = mount(
14
+ render(
12
15
  <OnePaneDialog
13
16
  title="Dialog with multi-step footer"
14
17
  content="dummy content"
@@ -17,17 +20,21 @@ describe("OnePaneDialog", () => {
17
20
  );
18
21
 
19
22
  // Act
20
- const dialog = wrapper.find(`[role="dialog"]`).first();
23
+ const dialog = screen.getByRole("dialog");
21
24
 
22
25
  // Assert
23
- expect(dialog.prop("testId")).toBe("one-pane-dialog-example");
26
+ expect(dialog).toHaveAttribute(
27
+ "data-test-id",
28
+ "one-pane-dialog-example",
29
+ );
24
30
  });
25
31
 
26
32
  test("role can be overriden to alertdialog", () => {
27
33
  // Arrange
28
- const wrapper = mount(
34
+ render(
29
35
  <OnePaneDialog
30
36
  title="Dialog with multi-step footer"
37
+ subtitle="Dialog subtitle"
31
38
  content="dummy content"
32
39
  testId="one-pane-dialog-example"
33
40
  role="alertdialog"
@@ -35,9 +42,30 @@ describe("OnePaneDialog", () => {
35
42
  );
36
43
 
37
44
  // Act
38
- const dialog = wrapper.find(`[role="alertdialog"]`).first();
45
+ const dialog = screen.getByRole("alertdialog");
46
+
47
+ // Assert
48
+ expect(dialog).toBeInTheDocument();
49
+ });
50
+
51
+ test("should include breadcrumbs", () => {
52
+ // Arrange
53
+ render(
54
+ <OnePaneDialog
55
+ title="Dialog with multi-step footer"
56
+ breadcrumbs={
57
+ <Breadcrumbs>
58
+ <BreadcrumbsItem>test</BreadcrumbsItem>
59
+ </Breadcrumbs>
60
+ }
61
+ content="dummy content"
62
+ testId="one-pane-dialog-example"
63
+ />,
64
+ );
65
+
66
+ // Act
39
67
 
40
68
  // Assert
41
- expect(dialog).toHaveLength(1);
69
+ expect(screen.getByLabelText("Breadcrumbs")).toBeInTheDocument();
42
70
  });
43
71
  });