@khanacademy/wonder-blocks-modal 2.3.0 → 2.3.3
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 +34 -0
- package/dist/es/index.js +63 -352
- package/dist/index.js +3 -1
- package/package.json +9 -9
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +19 -0
- package/src/components/__tests__/modal-backdrop.test.js +0 -4
- package/src/components/__tests__/modal-launcher.test.js +0 -4
- package/src/components/modal-dialog.js +2 -0
- package/src/components/one-pane-dialog.stories.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-modal
|
|
2
2
|
|
|
3
|
+
## 2.3.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [83486dba]
|
|
8
|
+
- @khanacademy/wonder-blocks-icon@1.2.29
|
|
9
|
+
- @khanacademy/wonder-blocks-icon-button@3.4.8
|
|
10
|
+
|
|
11
|
+
## 2.3.2
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [5f4a4297]
|
|
16
|
+
- Updated dependencies [2b96fd59]
|
|
17
|
+
- @khanacademy/wonder-blocks-core@4.3.2
|
|
18
|
+
- @khanacademy/wonder-blocks-breadcrumbs@1.0.31
|
|
19
|
+
- @khanacademy/wonder-blocks-icon@1.2.28
|
|
20
|
+
- @khanacademy/wonder-blocks-icon-button@3.4.7
|
|
21
|
+
- @khanacademy/wonder-blocks-layout@1.4.10
|
|
22
|
+
- @khanacademy/wonder-blocks-toolbar@2.1.32
|
|
23
|
+
- @khanacademy/wonder-blocks-typography@1.1.32
|
|
24
|
+
|
|
25
|
+
## 2.3.1
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- @khanacademy/wonder-blocks-breadcrumbs@1.0.30
|
|
30
|
+
- @khanacademy/wonder-blocks-core@4.3.1
|
|
31
|
+
- @khanacademy/wonder-blocks-icon@1.2.27
|
|
32
|
+
- @khanacademy/wonder-blocks-icon-button@3.4.6
|
|
33
|
+
- @khanacademy/wonder-blocks-layout@1.4.9
|
|
34
|
+
- @khanacademy/wonder-blocks-toolbar@2.1.31
|
|
35
|
+
- @khanacademy/wonder-blocks-typography@1.1.31
|
|
36
|
+
|
|
3
37
|
## 2.3.0
|
|
4
38
|
|
|
5
39
|
### Minor Changes
|
package/dist/es/index.js
CHANGED
|
@@ -11,16 +11,6 @@ import _extends from '@babel/runtime/helpers/extends';
|
|
|
11
11
|
import { icons } from '@khanacademy/wonder-blocks-icon';
|
|
12
12
|
import IconButton from '@khanacademy/wonder-blocks-icon-button';
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* `ModalDialog` is a component that contains these elements:
|
|
16
|
-
* - The visual dialog element itself (`<div role="dialog"/>`)
|
|
17
|
-
* - The custom contents below and/or above the Dialog itself (e.g. decorative graphics).
|
|
18
|
-
*
|
|
19
|
-
* **Accessibility notes:**
|
|
20
|
-
* - By default (e.g. using `OnePaneDialog`), `aria-labelledby` is populated automatically using the dialog title `id`.
|
|
21
|
-
* - If there is a custom Dialog implementation (e.g. `TwoPaneDialog`), the dialog element doesn’t have to have
|
|
22
|
-
* the `aria-labelledby` attribute however this is recommended. It should match the `id` of the dialog title.
|
|
23
|
-
*/
|
|
24
14
|
class ModalDialog extends React.Component {
|
|
25
15
|
render() {
|
|
26
16
|
const {
|
|
@@ -30,29 +20,31 @@ class ModalDialog extends React.Component {
|
|
|
30
20
|
style,
|
|
31
21
|
children,
|
|
32
22
|
testId,
|
|
33
|
-
"aria-labelledby": ariaLabelledBy
|
|
23
|
+
"aria-labelledby": ariaLabelledBy,
|
|
24
|
+
"aria-label": ariaLabel
|
|
34
25
|
} = this.props;
|
|
35
26
|
const contextValue = {
|
|
36
27
|
ssrSize: "large",
|
|
37
28
|
mediaSpec: MEDIA_MODAL_SPEC
|
|
38
29
|
};
|
|
39
|
-
return
|
|
30
|
+
return React.createElement(MediaLayoutContext.Provider, {
|
|
40
31
|
value: contextValue
|
|
41
|
-
},
|
|
32
|
+
}, React.createElement(MediaLayout, {
|
|
42
33
|
styleSheets: styleSheets$3
|
|
43
34
|
}, ({
|
|
44
35
|
styles
|
|
45
|
-
}) =>
|
|
36
|
+
}) => React.createElement(View, {
|
|
46
37
|
style: [styles.wrapper, style]
|
|
47
|
-
}, below &&
|
|
38
|
+
}, below && React.createElement(View, {
|
|
48
39
|
style: styles.below
|
|
49
|
-
}, below),
|
|
40
|
+
}, below), React.createElement(View, {
|
|
50
41
|
role: role,
|
|
51
42
|
"aria-modal": "true",
|
|
52
43
|
"aria-labelledby": ariaLabelledBy,
|
|
44
|
+
"aria-label": ariaLabel,
|
|
53
45
|
style: styles.dialog,
|
|
54
46
|
testId: testId
|
|
55
|
-
}, children), above &&
|
|
47
|
+
}, children), above && React.createElement(View, {
|
|
56
48
|
style: styles.above
|
|
57
49
|
}, above))));
|
|
58
50
|
}
|
|
@@ -71,10 +63,6 @@ const styleSheets$3 = {
|
|
|
71
63
|
height: "100%",
|
|
72
64
|
position: "relative"
|
|
73
65
|
},
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Ensures the dialog container uses the container size
|
|
77
|
-
*/
|
|
78
66
|
dialog: {
|
|
79
67
|
width: "100%",
|
|
80
68
|
height: "100%",
|
|
@@ -108,15 +96,6 @@ const styleSheets$3 = {
|
|
|
108
96
|
})
|
|
109
97
|
};
|
|
110
98
|
|
|
111
|
-
/**
|
|
112
|
-
* Modal footer included after the content.
|
|
113
|
-
*
|
|
114
|
-
* **Implementation notes**:
|
|
115
|
-
*
|
|
116
|
-
* If you are creating a custom Dialog, make sure to follow these guidelines:
|
|
117
|
-
* - Make sure to include it as part of [ModalPanel](/#modalpanel) by using the `footer` prop.
|
|
118
|
-
* - The footer is completely flexible. Meaning the developer needs to add its own custom layout to match design specs.
|
|
119
|
-
*/
|
|
120
99
|
class ModalFooter extends React.Component {
|
|
121
100
|
static isClassOf(instance) {
|
|
122
101
|
return instance && instance.type && instance.type.__IS_MODAL_FOOTER__;
|
|
@@ -126,7 +105,7 @@ class ModalFooter extends React.Component {
|
|
|
126
105
|
const {
|
|
127
106
|
children
|
|
128
107
|
} = this.props;
|
|
129
|
-
return
|
|
108
|
+
return React.createElement(View, {
|
|
130
109
|
style: styles$3.footer
|
|
131
110
|
}, children);
|
|
132
111
|
}
|
|
@@ -150,49 +129,6 @@ const styles$3 = StyleSheet.create({
|
|
|
150
129
|
}
|
|
151
130
|
});
|
|
152
131
|
|
|
153
|
-
/**
|
|
154
|
-
* This is a helper component that is never rendered by itself. It is always
|
|
155
|
-
* pinned to the top of the dialog, is responsive using the same behavior as its
|
|
156
|
-
* parent dialog, and has the following properties:
|
|
157
|
-
* - title
|
|
158
|
-
* - breadcrumb OR subtitle, but not both.
|
|
159
|
-
*
|
|
160
|
-
* **Accessibility notes:**
|
|
161
|
-
*
|
|
162
|
-
* - By default (e.g. using [OnePaneDialog](/#onepanedialog)), `titleId` is
|
|
163
|
-
* populated automatically by the parent container.
|
|
164
|
-
* - If there is a custom Dialog implementation (e.g. `TwoPaneDialog`), the
|
|
165
|
-
* ModalHeader doesn’t have to have the `titleId` prop however this is
|
|
166
|
-
* recommended. It should match the `aria-labelledby` prop of the
|
|
167
|
-
* [ModalDialog](/#modaldialog) component. If you want to see an example of
|
|
168
|
-
* how to generate this ID, check [IDProvider](/#idprovider).
|
|
169
|
-
*
|
|
170
|
-
* **Implementation notes:**
|
|
171
|
-
*
|
|
172
|
-
* If you are creating a custom Dialog, make sure to follow these guidelines:
|
|
173
|
-
* - Make sure to include it as part of [ModalPanel](/#modalpanel) by using the
|
|
174
|
-
* `header` prop.
|
|
175
|
-
* - Add a title (required).
|
|
176
|
-
* - Optionally add a subtitle or breadcrumbs.
|
|
177
|
-
* - We encourage you to add `titleId` (see Accessibility notes).
|
|
178
|
-
* - If the `ModalPanel` has a dark background, make sure to set `light` to
|
|
179
|
-
* `false`.
|
|
180
|
-
* - If you need to create e2e tests, make sure to pass a `testId` prop and
|
|
181
|
-
* add a sufix to scope the testId to this component: e.g.
|
|
182
|
-
* `some-random-id-ModalHeader`. This scope will also be passed to the title
|
|
183
|
-
* and subtitle elements: e.g. `some-random-id-ModalHeader-title`.
|
|
184
|
-
*
|
|
185
|
-
* Example:
|
|
186
|
-
*
|
|
187
|
-
* ```js
|
|
188
|
-
* <ModalHeader
|
|
189
|
-
* title="Sidebar using ModalHeader"
|
|
190
|
-
* subtitle="subtitle"
|
|
191
|
-
* titleId="uniqueTitleId"
|
|
192
|
-
* light={false}
|
|
193
|
-
* />
|
|
194
|
-
* ```
|
|
195
|
-
*/
|
|
196
132
|
class ModalHeader extends React.Component {
|
|
197
133
|
render() {
|
|
198
134
|
const {
|
|
@@ -208,20 +144,20 @@ class ModalHeader extends React.Component {
|
|
|
208
144
|
throw new Error("'subtitle' and 'breadcrumbs' can't be used together");
|
|
209
145
|
}
|
|
210
146
|
|
|
211
|
-
return
|
|
147
|
+
return React.createElement(MediaLayout, {
|
|
212
148
|
styleSheets: styleSheets$2
|
|
213
149
|
}, ({
|
|
214
150
|
styles
|
|
215
|
-
}) =>
|
|
151
|
+
}) => React.createElement(View, {
|
|
216
152
|
style: [styles.header, !light && styles.dark],
|
|
217
153
|
testId: testId
|
|
218
|
-
}, breadcrumbs &&
|
|
154
|
+
}, breadcrumbs && React.createElement(View, {
|
|
219
155
|
style: styles.breadcrumbs
|
|
220
|
-
}, breadcrumbs),
|
|
156
|
+
}, breadcrumbs), React.createElement(HeadingMedium, {
|
|
221
157
|
style: styles.title,
|
|
222
158
|
id: titleId,
|
|
223
159
|
testId: testId && `${testId}-title`
|
|
224
|
-
}, title), subtitle &&
|
|
160
|
+
}, title), subtitle && React.createElement(LabelSmall, {
|
|
225
161
|
style: light && styles.subtitle,
|
|
226
162
|
testId: testId && `${testId}-subtitle`
|
|
227
163
|
}, subtitle)));
|
|
@@ -251,7 +187,6 @@ const styleSheets$2 = {
|
|
|
251
187
|
marginBottom: Spacing.xSmall_8
|
|
252
188
|
},
|
|
253
189
|
title: {
|
|
254
|
-
// Prevent title from overlapping the close button
|
|
255
190
|
paddingRight: Spacing.medium_16
|
|
256
191
|
},
|
|
257
192
|
subtitle: {
|
|
@@ -271,22 +206,11 @@ const styleSheets$2 = {
|
|
|
271
206
|
};
|
|
272
207
|
|
|
273
208
|
class FocusTrap extends React.Component {
|
|
274
|
-
/** The most recent node _inside this component_ to receive focus. */
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Whether we're currently applying programmatic focus, and should therefore
|
|
278
|
-
* ignore focus change events.
|
|
279
|
-
*/
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Tabbing is restricted to descendents of this element.
|
|
283
|
-
*/
|
|
284
209
|
constructor(props) {
|
|
285
210
|
super(props);
|
|
286
211
|
|
|
287
212
|
this.getModalRoot = node => {
|
|
288
213
|
if (!node) {
|
|
289
|
-
// The component is being umounted
|
|
290
214
|
return;
|
|
291
215
|
}
|
|
292
216
|
|
|
@@ -300,8 +224,6 @@ class FocusTrap extends React.Component {
|
|
|
300
224
|
};
|
|
301
225
|
|
|
302
226
|
this.handleGlobalFocus = e => {
|
|
303
|
-
// If we're busy applying our own programmatic focus, we ignore focus
|
|
304
|
-
// changes, to avoid an infinite loop.
|
|
305
227
|
if (this.ignoreFocusChanges) {
|
|
306
228
|
return;
|
|
307
229
|
}
|
|
@@ -309,7 +231,6 @@ class FocusTrap extends React.Component {
|
|
|
309
231
|
const target = e.target;
|
|
310
232
|
|
|
311
233
|
if (!(target instanceof Node)) {
|
|
312
|
-
// Sometimes focus events trigger on the document itself. Ignore!
|
|
313
234
|
return;
|
|
314
235
|
}
|
|
315
236
|
|
|
@@ -320,25 +241,13 @@ class FocusTrap extends React.Component {
|
|
|
320
241
|
}
|
|
321
242
|
|
|
322
243
|
if (modalRoot.contains(target)) {
|
|
323
|
-
// If the newly focused node is inside the modal, we just keep track
|
|
324
|
-
// of that.
|
|
325
244
|
this.lastNodeFocusedInModal = target;
|
|
326
245
|
} else {
|
|
327
|
-
|
|
328
|
-
// the first focusable node of the modal. (This could be the user
|
|
329
|
-
// pressing Tab on the last node of the modal, or focus escaping in
|
|
330
|
-
// some other way.)
|
|
331
|
-
this.focusFirstElementIn(modalRoot); // But, if it turns out that the first focusable node of the modal
|
|
332
|
-
// was what we were previously focusing, then this is probably the
|
|
333
|
-
// user pressing Shift-Tab on the first node, wanting to go to the
|
|
334
|
-
// end. So, we instead try focusing the last focusable node of the
|
|
335
|
-
// modal.
|
|
246
|
+
this.focusFirstElementIn(modalRoot);
|
|
336
247
|
|
|
337
248
|
if (document.activeElement === this.lastNodeFocusedInModal) {
|
|
338
249
|
this.focusLastElementIn(modalRoot);
|
|
339
|
-
}
|
|
340
|
-
// node as the last node focused in the modal.
|
|
341
|
-
|
|
250
|
+
}
|
|
342
251
|
|
|
343
252
|
this.lastNodeFocusedInModal = document.activeElement;
|
|
344
253
|
}
|
|
@@ -356,27 +265,18 @@ class FocusTrap extends React.Component {
|
|
|
356
265
|
window.removeEventListener("focus", this.handleGlobalFocus, true);
|
|
357
266
|
}
|
|
358
267
|
|
|
359
|
-
/** Try to focus the given node. Return true iff successful. */
|
|
360
268
|
tryToFocus(node) {
|
|
361
269
|
if (node instanceof HTMLElement) {
|
|
362
270
|
this.ignoreFocusChanges = true;
|
|
363
271
|
|
|
364
272
|
try {
|
|
365
273
|
node.focus();
|
|
366
|
-
} catch (e) {
|
|
367
|
-
}
|
|
274
|
+
} catch (e) {}
|
|
368
275
|
|
|
369
276
|
this.ignoreFocusChanges = false;
|
|
370
277
|
return document.activeElement === node;
|
|
371
278
|
}
|
|
372
279
|
}
|
|
373
|
-
/**
|
|
374
|
-
* Focus the first focusable descendant of the given node.
|
|
375
|
-
*
|
|
376
|
-
* Return true if we succeed. Or, if the given node has no focusable
|
|
377
|
-
* descendants, return false.
|
|
378
|
-
*/
|
|
379
|
-
|
|
380
280
|
|
|
381
281
|
focusFirstElementIn(currentParent) {
|
|
382
282
|
const children = currentParent.childNodes;
|
|
@@ -391,13 +291,6 @@ class FocusTrap extends React.Component {
|
|
|
391
291
|
|
|
392
292
|
return false;
|
|
393
293
|
}
|
|
394
|
-
/**
|
|
395
|
-
* Focus the last focusable descendant of the given node.
|
|
396
|
-
*
|
|
397
|
-
* Return true if we succeed. Or, if the given node has no focusable
|
|
398
|
-
* descendants, return false.
|
|
399
|
-
*/
|
|
400
|
-
|
|
401
294
|
|
|
402
295
|
focusLastElementIn(currentParent) {
|
|
403
296
|
const children = currentParent.childNodes;
|
|
@@ -412,22 +305,20 @@ class FocusTrap extends React.Component {
|
|
|
412
305
|
|
|
413
306
|
return false;
|
|
414
307
|
}
|
|
415
|
-
/** This method is called when any node on the page is focused. */
|
|
416
|
-
|
|
417
308
|
|
|
418
309
|
render() {
|
|
419
310
|
const {
|
|
420
311
|
style
|
|
421
312
|
} = this.props;
|
|
422
|
-
return
|
|
313
|
+
return React.createElement(React.Fragment, null, React.createElement("div", {
|
|
423
314
|
tabIndex: "0",
|
|
424
315
|
style: {
|
|
425
316
|
position: "fixed"
|
|
426
317
|
}
|
|
427
|
-
}),
|
|
318
|
+
}), React.createElement(View, {
|
|
428
319
|
style: style,
|
|
429
320
|
ref: this.getModalRoot
|
|
430
|
-
}, this.props.children),
|
|
321
|
+
}, this.props.children), React.createElement("div", {
|
|
431
322
|
tabIndex: "0",
|
|
432
323
|
style: {
|
|
433
324
|
position: "fixed"
|
|
@@ -437,43 +328,23 @@ class FocusTrap extends React.Component {
|
|
|
437
328
|
|
|
438
329
|
}
|
|
439
330
|
|
|
440
|
-
/**
|
|
441
|
-
* The attribute used to identify a modal launcher portal.
|
|
442
|
-
*/
|
|
443
331
|
const ModalLauncherPortalAttributeName = "data-modal-launcher-portal";
|
|
444
332
|
|
|
445
|
-
/**
|
|
446
|
-
* List of elements that can be focused
|
|
447
|
-
* @see https://www.w3.org/TR/html5/editing.html#can-be-focused
|
|
448
|
-
*/
|
|
449
333
|
const FOCUSABLE_ELEMENTS = 'a[href], details, input, textarea, select, button:not([aria-label^="Close"])';
|
|
450
334
|
function findFocusableNodes(root) {
|
|
451
335
|
return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS));
|
|
452
336
|
}
|
|
453
337
|
|
|
454
|
-
/**
|
|
455
|
-
* A private component used by ModalLauncher. This is the fixed-position
|
|
456
|
-
* container element that gets mounted outside the DOM. It overlays the modal
|
|
457
|
-
* content (provided as `children`) over the content, with a gray backdrop
|
|
458
|
-
* behind it.
|
|
459
|
-
*
|
|
460
|
-
* This component is also responsible for cloning the provided modal `children`,
|
|
461
|
-
* and adding an `onClose` prop that will call `onCloseModal`. If an
|
|
462
|
-
* `onClose` prop is already provided, the two are merged.
|
|
463
|
-
*/
|
|
464
338
|
class ModalBackdrop extends React.Component {
|
|
465
339
|
constructor(...args) {
|
|
466
340
|
super(...args);
|
|
467
341
|
this._mousePressedOutside = false;
|
|
468
342
|
|
|
469
343
|
this.handleMouseDown = e => {
|
|
470
|
-
// Confirm that it is the backdrop that is being clicked, not the child
|
|
471
344
|
this._mousePressedOutside = e.target === e.currentTarget;
|
|
472
345
|
};
|
|
473
346
|
|
|
474
347
|
this.handleMouseUp = e => {
|
|
475
|
-
// Confirm that it is the backdrop that is being clicked, not the child
|
|
476
|
-
// and that the mouse was pressed in the backdrop first.
|
|
477
348
|
if (e.target === e.currentTarget && this._mousePressedOutside) {
|
|
478
349
|
this.props.onCloseModal();
|
|
479
350
|
}
|
|
@@ -489,20 +360,13 @@ class ModalBackdrop extends React.Component {
|
|
|
489
360
|
return;
|
|
490
361
|
}
|
|
491
362
|
|
|
492
|
-
const firstFocusableElement =
|
|
493
|
-
this._getInitialFocusElement(node) || // 2. get first occurence from list of focusable elements
|
|
494
|
-
this._getFirstFocusableElement(node) || // 3. get the dialog itself
|
|
495
|
-
this._getDialogElement(node); // wait for styles to applied
|
|
496
|
-
|
|
363
|
+
const firstFocusableElement = this._getInitialFocusElement(node) || this._getFirstFocusableElement(node) || this._getDialogElement(node);
|
|
497
364
|
|
|
498
365
|
setTimeout(() => {
|
|
499
366
|
firstFocusableElement.focus();
|
|
500
367
|
}, 0);
|
|
501
368
|
}
|
|
502
369
|
|
|
503
|
-
/**
|
|
504
|
-
* Returns an element specified by the user
|
|
505
|
-
*/
|
|
506
370
|
_getInitialFocusElement(node) {
|
|
507
371
|
const {
|
|
508
372
|
initialFocusId
|
|
@@ -514,41 +378,22 @@ class ModalBackdrop extends React.Component {
|
|
|
514
378
|
|
|
515
379
|
return ReactDOM.findDOMNode(node.querySelector(`#${initialFocusId}`));
|
|
516
380
|
}
|
|
517
|
-
/**
|
|
518
|
-
* Returns the first focusable element found inside the Dialog
|
|
519
|
-
*/
|
|
520
|
-
|
|
521
381
|
|
|
522
382
|
_getFirstFocusableElement(node) {
|
|
523
|
-
// get a collection of elements that can be focused
|
|
524
383
|
const focusableElements = findFocusableNodes(node);
|
|
525
384
|
|
|
526
385
|
if (!focusableElements) {
|
|
527
386
|
return null;
|
|
528
|
-
}
|
|
529
|
-
|
|
387
|
+
}
|
|
530
388
|
|
|
531
389
|
return focusableElements[0];
|
|
532
390
|
}
|
|
533
|
-
/**
|
|
534
|
-
* Returns the dialog element
|
|
535
|
-
*/
|
|
536
|
-
|
|
537
391
|
|
|
538
392
|
_getDialogElement(node) {
|
|
539
|
-
|
|
540
|
-
// the dialog content element itself will receive focus.
|
|
541
|
-
const dialogElement = ReactDOM.findDOMNode(node.querySelector('[role="dialog"]')); // add tabIndex to make the Dialog focusable
|
|
542
|
-
|
|
393
|
+
const dialogElement = ReactDOM.findDOMNode(node.querySelector('[role="dialog"]'));
|
|
543
394
|
dialogElement.tabIndex = -1;
|
|
544
395
|
return dialogElement;
|
|
545
396
|
}
|
|
546
|
-
/**
|
|
547
|
-
* When the user clicks on the gray backdrop area (i.e., the click came
|
|
548
|
-
* _directly_ from the positioner, not bubbled up from its children), close
|
|
549
|
-
* the modal.
|
|
550
|
-
*/
|
|
551
|
-
|
|
552
397
|
|
|
553
398
|
render() {
|
|
554
399
|
const {
|
|
@@ -558,7 +403,7 @@ class ModalBackdrop extends React.Component {
|
|
|
558
403
|
const backdropProps = {
|
|
559
404
|
[ModalLauncherPortalAttributeName]: true
|
|
560
405
|
};
|
|
561
|
-
return
|
|
406
|
+
return React.createElement(View, _extends({
|
|
562
407
|
style: styles$2.modalPositioner,
|
|
563
408
|
onMouseDown: this.handleMouseDown,
|
|
564
409
|
onMouseUp: this.handleMouseUp,
|
|
@@ -576,30 +421,11 @@ const styles$2 = StyleSheet.create({
|
|
|
576
421
|
height: "100%",
|
|
577
422
|
alignItems: "center",
|
|
578
423
|
justifyContent: "center",
|
|
579
|
-
// If the modal ends up being too big for the viewport (e.g., the min
|
|
580
|
-
// height is triggered), add another scrollbar specifically for
|
|
581
|
-
// scrolling modal content.
|
|
582
|
-
//
|
|
583
|
-
// TODO(mdr): The specified behavior is that the modal should scroll
|
|
584
|
-
// with the rest of the page, rather than separately, if overflow
|
|
585
|
-
// turns out to be necessary. That sounds hard to do; punting for
|
|
586
|
-
// now!
|
|
587
424
|
overflow: "auto",
|
|
588
425
|
background: Color.offBlack64
|
|
589
426
|
}
|
|
590
427
|
});
|
|
591
428
|
|
|
592
|
-
/**
|
|
593
|
-
* A UI-less component that lets `ModalLauncher` disable page scroll.
|
|
594
|
-
*
|
|
595
|
-
* The positioning of the modal requires some global page state changed
|
|
596
|
-
* unfortunately, and this handles that in an encapsulated way.
|
|
597
|
-
*
|
|
598
|
-
* NOTE(mdr): This component was copied from webapp. Be wary of sync issues. It
|
|
599
|
-
* also doesn't have unit tests, and we haven't added any, since it's a
|
|
600
|
-
* relatively stable component that has now been stress-tested lots in prod.
|
|
601
|
-
*/
|
|
602
|
-
|
|
603
429
|
const needsHackyMobileSafariScrollDisabler = (() => {
|
|
604
430
|
if (typeof window === "undefined") {
|
|
605
431
|
return false;
|
|
@@ -616,13 +442,10 @@ class ScrollDisabler extends React.Component {
|
|
|
616
442
|
|
|
617
443
|
if (!body) {
|
|
618
444
|
throw new Error("couldn't find document.body");
|
|
619
|
-
}
|
|
620
|
-
// opened.
|
|
621
|
-
|
|
445
|
+
}
|
|
622
446
|
|
|
623
447
|
ScrollDisabler.oldOverflow = body.style.overflow;
|
|
624
|
-
ScrollDisabler.oldScrollY = window.scrollY;
|
|
625
|
-
// modified any of them.
|
|
448
|
+
ScrollDisabler.oldScrollY = window.scrollY;
|
|
626
449
|
|
|
627
450
|
if (needsHackyMobileSafariScrollDisabler) {
|
|
628
451
|
ScrollDisabler.oldPosition = body.style.position;
|
|
@@ -630,9 +453,7 @@ class ScrollDisabler extends React.Component {
|
|
|
630
453
|
ScrollDisabler.oldTop = body.style.top;
|
|
631
454
|
}
|
|
632
455
|
|
|
633
|
-
body.style.overflow = "hidden";
|
|
634
|
-
// fixed is also required. Setting style.top = -scollTop maintains
|
|
635
|
-
// the scroll position (without which we'd scroll to the top).
|
|
456
|
+
body.style.overflow = "hidden";
|
|
636
457
|
|
|
637
458
|
if (needsHackyMobileSafariScrollDisabler) {
|
|
638
459
|
body.style.position = "fixed";
|
|
@@ -652,8 +473,7 @@ class ScrollDisabler extends React.Component {
|
|
|
652
473
|
|
|
653
474
|
if (!body) {
|
|
654
475
|
throw new Error("couldn't find document.body");
|
|
655
|
-
}
|
|
656
|
-
|
|
476
|
+
}
|
|
657
477
|
|
|
658
478
|
body.style.overflow = ScrollDisabler.oldOverflow;
|
|
659
479
|
|
|
@@ -680,24 +500,8 @@ ScrollDisabler.numModalsOpened = 0;
|
|
|
680
500
|
const defaultContext = {
|
|
681
501
|
closeModal: undefined
|
|
682
502
|
};
|
|
683
|
-
var ModalContext =
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* This component enables you to launch a modal, covering the screen.
|
|
687
|
-
*
|
|
688
|
-
* Children have access to `openModal` function via the function-as-children
|
|
689
|
-
* pattern, so one common use case is for this component to wrap a button:
|
|
690
|
-
*
|
|
691
|
-
* ```js
|
|
692
|
-
* <ModalLauncher modal={<TwoColumnModal ... />}>
|
|
693
|
-
* {({openModal}) => <button onClick={openModal}>Learn more</button>}
|
|
694
|
-
* </ModalLauncher>
|
|
695
|
-
* ```
|
|
696
|
-
*
|
|
697
|
-
* The actual modal itself is constructed separately, using a layout component
|
|
698
|
-
* like OnePaneDialog and is provided via
|
|
699
|
-
* the `modal` prop.
|
|
700
|
-
*/
|
|
503
|
+
var ModalContext = React.createContext(defaultContext);
|
|
504
|
+
|
|
701
505
|
class ModalLauncher extends React.Component {
|
|
702
506
|
constructor(...args) {
|
|
703
507
|
super(...args);
|
|
@@ -706,7 +510,6 @@ class ModalLauncher extends React.Component {
|
|
|
706
510
|
};
|
|
707
511
|
|
|
708
512
|
this._saveLastElementFocused = () => {
|
|
709
|
-
// keep a reference of the element that triggers the modal
|
|
710
513
|
this.lastElementFocusedOutsideModal = document.activeElement;
|
|
711
514
|
};
|
|
712
515
|
|
|
@@ -723,14 +526,12 @@ class ModalLauncher extends React.Component {
|
|
|
723
526
|
closedFocusId,
|
|
724
527
|
schedule
|
|
725
528
|
} = this.props;
|
|
726
|
-
const lastElement = this.lastElementFocusedOutsideModal;
|
|
529
|
+
const lastElement = this.lastElementFocusedOutsideModal;
|
|
727
530
|
|
|
728
531
|
if (closedFocusId) {
|
|
729
532
|
const focusElement = ReactDOM.findDOMNode(document.getElementById(closedFocusId));
|
|
730
533
|
|
|
731
534
|
if (focusElement) {
|
|
732
|
-
// Wait for the modal to leave the DOM before trying
|
|
733
|
-
// to focus on the specified element.
|
|
734
535
|
schedule.animationFrame(() => {
|
|
735
536
|
focusElement.focus();
|
|
736
537
|
});
|
|
@@ -739,8 +540,6 @@ class ModalLauncher extends React.Component {
|
|
|
739
540
|
}
|
|
740
541
|
|
|
741
542
|
if (lastElement != null) {
|
|
742
|
-
// Wait for the modal to leave the DOM before trying to
|
|
743
|
-
// return focus to the element that triggered the modal.
|
|
744
543
|
schedule.animationFrame(() => {
|
|
745
544
|
lastElement.focus();
|
|
746
545
|
});
|
|
@@ -763,17 +562,14 @@ class ModalLauncher extends React.Component {
|
|
|
763
562
|
|
|
764
563
|
static getDerivedStateFromProps(props, state) {
|
|
765
564
|
if (typeof props.opened === "boolean" && props.children) {
|
|
766
|
-
// eslint-disable-next-line no-console
|
|
767
565
|
console.warn("'children' and 'opened' can't be used together");
|
|
768
566
|
}
|
|
769
567
|
|
|
770
568
|
if (typeof props.opened === "boolean" && !props.onClose) {
|
|
771
|
-
// eslint-disable-next-line no-console
|
|
772
569
|
console.warn("'onClose' should be used with 'opened'");
|
|
773
570
|
}
|
|
774
571
|
|
|
775
572
|
if (typeof props.opened !== "boolean" && !props.children) {
|
|
776
|
-
// eslint-disable-next-line no-console
|
|
777
573
|
console.warn("either 'children' or 'opened' must be set");
|
|
778
574
|
}
|
|
779
575
|
|
|
@@ -783,7 +579,6 @@ class ModalLauncher extends React.Component {
|
|
|
783
579
|
}
|
|
784
580
|
|
|
785
581
|
componentDidUpdate(prevProps) {
|
|
786
|
-
// ensures the element is stored only when the modal is opened
|
|
787
582
|
if (!prevProps.opened && this.props.opened) {
|
|
788
583
|
this._saveLastElementFocused();
|
|
789
584
|
}
|
|
@@ -811,35 +606,22 @@ class ModalLauncher extends React.Component {
|
|
|
811
606
|
return null;
|
|
812
607
|
}
|
|
813
608
|
|
|
814
|
-
return (
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
correct z-index so that it'll be above the global nav in webapp. */
|
|
828
|
-
React.createElement(FocusTrap, {
|
|
829
|
-
style: styles$1.container
|
|
830
|
-
}, /*#__PURE__*/React.createElement(ModalBackdrop, {
|
|
831
|
-
initialFocusId: this.props.initialFocusId,
|
|
832
|
-
testId: this.props.testId,
|
|
833
|
-
onCloseModal: this.props.backdropDismissEnabled ? this.handleCloseModal : () => {}
|
|
834
|
-
}, this._renderModal())), body), this.state.opened && /*#__PURE__*/React.createElement(ModalLauncherKeypressListener, {
|
|
835
|
-
onClose: this.handleCloseModal
|
|
836
|
-
}), this.state.opened && /*#__PURE__*/React.createElement(ScrollDisabler, null))
|
|
837
|
-
);
|
|
609
|
+
return React.createElement(ModalContext.Provider, {
|
|
610
|
+
value: {
|
|
611
|
+
closeModal: this.handleCloseModal
|
|
612
|
+
}
|
|
613
|
+
}, renderedChildren, this.state.opened && ReactDOM.createPortal(React.createElement(FocusTrap, {
|
|
614
|
+
style: styles$1.container
|
|
615
|
+
}, React.createElement(ModalBackdrop, {
|
|
616
|
+
initialFocusId: this.props.initialFocusId,
|
|
617
|
+
testId: this.props.testId,
|
|
618
|
+
onCloseModal: this.props.backdropDismissEnabled ? this.handleCloseModal : () => {}
|
|
619
|
+
}, this._renderModal())), body), this.state.opened && React.createElement(ModalLauncherKeypressListener, {
|
|
620
|
+
onClose: this.handleCloseModal
|
|
621
|
+
}), this.state.opened && React.createElement(ScrollDisabler, null));
|
|
838
622
|
}
|
|
839
623
|
|
|
840
624
|
}
|
|
841
|
-
/** A component that, when mounted, calls `onClose` when Escape is pressed. */
|
|
842
|
-
|
|
843
625
|
|
|
844
626
|
ModalLauncher.defaultProps = {
|
|
845
627
|
backdropDismissEnabled: true
|
|
@@ -850,16 +632,7 @@ class ModalLauncherKeypressListener extends React.Component {
|
|
|
850
632
|
super(...args);
|
|
851
633
|
|
|
852
634
|
this._handleKeyup = e => {
|
|
853
|
-
// We check the key as that's keyboard layout agnostic and also avoids
|
|
854
|
-
// the minefield of deprecated number type properties like keyCode and
|
|
855
|
-
// which, with the replacement code, which uses a string instead.
|
|
856
635
|
if (e.key === "Escape") {
|
|
857
|
-
// Stop the event going any further.
|
|
858
|
-
// For cancellation events, like the Escape key, we generally should
|
|
859
|
-
// air on the side of caution and only allow it to cancel one thing.
|
|
860
|
-
// So, it's polite for us to stop propagation of the event.
|
|
861
|
-
// Otherwise, we end up with UX where one Escape key press
|
|
862
|
-
// unexpectedly cancels multiple things.
|
|
863
636
|
e.preventDefault();
|
|
864
637
|
e.stopPropagation();
|
|
865
638
|
this.props.onClose();
|
|
@@ -883,19 +656,11 @@ class ModalLauncherKeypressListener extends React.Component {
|
|
|
883
656
|
|
|
884
657
|
const styles$1 = StyleSheet.create({
|
|
885
658
|
container: {
|
|
886
|
-
// This z-index is copied from the Khan Academy webapp.
|
|
887
|
-
//
|
|
888
|
-
// TODO(mdr): Should we keep this in a constants file somewhere? Or
|
|
889
|
-
// not hardcode it at all, and provide it to Wonder Blocks via
|
|
890
|
-
// configuration?
|
|
891
659
|
zIndex: 1080
|
|
892
660
|
}
|
|
893
661
|
});
|
|
894
662
|
var modalLauncher = withActionScheduler(ModalLauncher);
|
|
895
663
|
|
|
896
|
-
/**
|
|
897
|
-
* The Modal content included after the header
|
|
898
|
-
*/
|
|
899
664
|
class ModalContent extends React.Component {
|
|
900
665
|
static isClassOf(instance) {
|
|
901
666
|
return instance && instance.type && instance.type.__IS_MODAL_CONTENT__;
|
|
@@ -907,13 +672,13 @@ class ModalContent extends React.Component {
|
|
|
907
672
|
style,
|
|
908
673
|
children
|
|
909
674
|
} = this.props;
|
|
910
|
-
return
|
|
675
|
+
return React.createElement(MediaLayout, {
|
|
911
676
|
styleSheets: styleSheets$1
|
|
912
677
|
}, ({
|
|
913
678
|
styles
|
|
914
|
-
}) =>
|
|
679
|
+
}) => React.createElement(View, {
|
|
915
680
|
style: [styles.wrapper, scrollOverflow && styles.scrollOverflow]
|
|
916
|
-
},
|
|
681
|
+
}, React.createElement(View, {
|
|
917
682
|
style: [styles.content, style]
|
|
918
683
|
}, children)));
|
|
919
684
|
}
|
|
@@ -927,8 +692,6 @@ const styleSheets$1 = {
|
|
|
927
692
|
all: StyleSheet.create({
|
|
928
693
|
wrapper: {
|
|
929
694
|
flex: 1,
|
|
930
|
-
// This helps to ensure that the paddingBottom is preserved when
|
|
931
|
-
// the contents start to overflow, this goes away on display: flex
|
|
932
695
|
display: "block"
|
|
933
696
|
},
|
|
934
697
|
scrollOverflow: {
|
|
@@ -956,17 +719,15 @@ class CloseButton extends React.Component {
|
|
|
956
719
|
style,
|
|
957
720
|
testId
|
|
958
721
|
} = this.props;
|
|
959
|
-
return
|
|
722
|
+
return React.createElement(ModalContext.Consumer, null, ({
|
|
960
723
|
closeModal
|
|
961
724
|
}) => {
|
|
962
725
|
if (closeModal && onClick) {
|
|
963
726
|
throw new Error("You've specified 'onClose' on a modal when using ModalLauncher. Please specify 'onClose' on the ModalLauncher instead");
|
|
964
727
|
}
|
|
965
728
|
|
|
966
|
-
return
|
|
967
|
-
icon: icons.dismiss
|
|
968
|
-
// TODO(kevinb): provide a way to set this label
|
|
969
|
-
,
|
|
729
|
+
return React.createElement(IconButton, {
|
|
730
|
+
icon: icons.dismiss,
|
|
970
731
|
"aria-label": "Close modal",
|
|
971
732
|
onClick: onClick || closeModal,
|
|
972
733
|
kind: light ? "primary" : "tertiary",
|
|
@@ -979,26 +740,6 @@ class CloseButton extends React.Component {
|
|
|
979
740
|
|
|
980
741
|
}
|
|
981
742
|
|
|
982
|
-
/**
|
|
983
|
-
* ModalPanel is the content container.
|
|
984
|
-
*
|
|
985
|
-
* **Implementation notes:**
|
|
986
|
-
*
|
|
987
|
-
* If you are creating a custom Dialog, make sure to follow these guidelines:
|
|
988
|
-
* - Make sure to add this component inside the [ModalDialog](/#modaldialog).
|
|
989
|
-
* - If needed, you can also add a [ModalHeader](/#modalheader) using the
|
|
990
|
-
* `header` prop. Same goes for [ModalFooter](/#modalfooter).
|
|
991
|
-
* - If you need to create e2e tests, make sure to pass a `testId` prop. This
|
|
992
|
-
* will be passed down to this component using a sufix: e.g.
|
|
993
|
-
* `some-random-id-ModalPanel`. This scope will be propagated to the
|
|
994
|
-
* CloseButton element as well: e.g. `some-random-id-CloseButton`.
|
|
995
|
-
*
|
|
996
|
-
* ```js
|
|
997
|
-
* <ModalDialog>
|
|
998
|
-
* <ModalPanel content={"custom content goes here"} />
|
|
999
|
-
* </ModalDialog>
|
|
1000
|
-
* ```
|
|
1001
|
-
*/
|
|
1002
743
|
class ModalPanel extends React.Component {
|
|
1003
744
|
renderMainContent() {
|
|
1004
745
|
const {
|
|
@@ -1006,19 +747,14 @@ class ModalPanel extends React.Component {
|
|
|
1006
747
|
footer,
|
|
1007
748
|
scrollOverflow
|
|
1008
749
|
} = this.props;
|
|
1009
|
-
const mainContent = ModalContent.isClassOf(content) ? content :
|
|
750
|
+
const mainContent = ModalContent.isClassOf(content) ? content : React.createElement(ModalContent, null, content);
|
|
1010
751
|
|
|
1011
752
|
if (!mainContent) {
|
|
1012
753
|
return mainContent;
|
|
1013
754
|
}
|
|
1014
755
|
|
|
1015
|
-
return
|
|
1016
|
-
// Pass the scrollOverflow and header in to the main content
|
|
756
|
+
return React.cloneElement(mainContent, {
|
|
1017
757
|
scrollOverflow,
|
|
1018
|
-
// We override the styling of the main content to help position
|
|
1019
|
-
// it if there is a footer or close button being
|
|
1020
|
-
// shown. We have to do this here as the ModalContent doesn't
|
|
1021
|
-
// know about things being positioned around it.
|
|
1022
758
|
style: [!!footer && styles.hasFooter, mainContent.props.style]
|
|
1023
759
|
});
|
|
1024
760
|
}
|
|
@@ -1034,15 +770,15 @@ class ModalPanel extends React.Component {
|
|
|
1034
770
|
testId
|
|
1035
771
|
} = this.props;
|
|
1036
772
|
const mainContent = this.renderMainContent();
|
|
1037
|
-
return
|
|
773
|
+
return React.createElement(View, {
|
|
1038
774
|
style: [styles.wrapper, !light && styles.dark, style],
|
|
1039
775
|
testId: testId && `${testId}-panel`
|
|
1040
|
-
}, closeButtonVisible &&
|
|
776
|
+
}, closeButtonVisible && React.createElement(CloseButton, {
|
|
1041
777
|
light: !light,
|
|
1042
778
|
onClick: onClose,
|
|
1043
779
|
style: styles.closeButton,
|
|
1044
780
|
testId: testId && `${testId}-close`
|
|
1045
|
-
}), header, mainContent, !footer || ModalFooter.isClassOf(footer) ? footer :
|
|
781
|
+
}), header, mainContent, !footer || ModalFooter.isClassOf(footer) ? footer : React.createElement(ModalFooter, null, footer));
|
|
1046
782
|
}
|
|
1047
783
|
|
|
1048
784
|
}
|
|
@@ -1067,8 +803,6 @@ const styles = StyleSheet.create({
|
|
|
1067
803
|
position: "absolute",
|
|
1068
804
|
right: Spacing.medium_16,
|
|
1069
805
|
top: Spacing.medium_16,
|
|
1070
|
-
// This is to allow the button to be tab-ordered before the modal
|
|
1071
|
-
// content but still be above the header and content.
|
|
1072
806
|
zIndex: 1
|
|
1073
807
|
},
|
|
1074
808
|
dark: {
|
|
@@ -1080,12 +814,6 @@ const styles = StyleSheet.create({
|
|
|
1080
814
|
}
|
|
1081
815
|
});
|
|
1082
816
|
|
|
1083
|
-
/**
|
|
1084
|
-
* This is the standard layout for most straightforward modal experiences.
|
|
1085
|
-
*
|
|
1086
|
-
* The ModalHeader is required, but the ModalFooter is optional.
|
|
1087
|
-
* The content of the dialog itself is fully customizable, but the left/right/top/bottom padding is fixed.
|
|
1088
|
-
*/
|
|
1089
817
|
class OnePaneDialog extends React.Component {
|
|
1090
818
|
renderHeader(uniqueId) {
|
|
1091
819
|
const {
|
|
@@ -1096,21 +824,21 @@ class OnePaneDialog extends React.Component {
|
|
|
1096
824
|
} = this.props;
|
|
1097
825
|
|
|
1098
826
|
if (breadcrumbs) {
|
|
1099
|
-
return
|
|
827
|
+
return React.createElement(ModalHeader, {
|
|
1100
828
|
title: title,
|
|
1101
829
|
breadcrumbs: breadcrumbs,
|
|
1102
830
|
titleId: uniqueId,
|
|
1103
831
|
testId: testId && `${testId}-header`
|
|
1104
832
|
});
|
|
1105
833
|
} else if (subtitle) {
|
|
1106
|
-
return
|
|
834
|
+
return React.createElement(ModalHeader, {
|
|
1107
835
|
title: title,
|
|
1108
836
|
subtitle: subtitle,
|
|
1109
837
|
titleId: uniqueId,
|
|
1110
838
|
testId: testId && `${testId}-header`
|
|
1111
839
|
});
|
|
1112
840
|
} else {
|
|
1113
|
-
return
|
|
841
|
+
return React.createElement(ModalHeader, {
|
|
1114
842
|
title: title,
|
|
1115
843
|
titleId: uniqueId,
|
|
1116
844
|
testId: testId && `${testId}-header`
|
|
@@ -1131,21 +859,21 @@ class OnePaneDialog extends React.Component {
|
|
|
1131
859
|
titleId,
|
|
1132
860
|
role
|
|
1133
861
|
} = this.props;
|
|
1134
|
-
return
|
|
862
|
+
return React.createElement(MediaLayout, {
|
|
1135
863
|
styleSheets: styleSheets
|
|
1136
864
|
}, ({
|
|
1137
865
|
styles
|
|
1138
|
-
}) =>
|
|
866
|
+
}) => React.createElement(IDProvider, {
|
|
1139
867
|
id: titleId,
|
|
1140
868
|
scope: "modal"
|
|
1141
|
-
}, uniqueId =>
|
|
869
|
+
}, uniqueId => React.createElement(ModalDialog, {
|
|
1142
870
|
style: [styles.dialog, style],
|
|
1143
871
|
above: above,
|
|
1144
872
|
below: below,
|
|
1145
873
|
testId: testId,
|
|
1146
874
|
"aria-labelledby": uniqueId,
|
|
1147
875
|
role: role
|
|
1148
|
-
},
|
|
876
|
+
}, React.createElement(ModalPanel, {
|
|
1149
877
|
onClose: onClose,
|
|
1150
878
|
header: this.renderHeader(uniqueId),
|
|
1151
879
|
content: content,
|
|
@@ -1177,14 +905,6 @@ const styleSheets = {
|
|
|
1177
905
|
})
|
|
1178
906
|
};
|
|
1179
907
|
|
|
1180
|
-
/**
|
|
1181
|
-
* From a given element, finds its next ancestor that is a modal launcher portal
|
|
1182
|
-
* element.
|
|
1183
|
-
* @param {?(Element | Text)} element The element whose ancestors are to be
|
|
1184
|
-
* walked.
|
|
1185
|
-
* @returns {?Element} The nearest parent modal launcher portal.
|
|
1186
|
-
*/
|
|
1187
|
-
|
|
1188
908
|
function maybeGetNextAncestorModalLauncherPortal(element) {
|
|
1189
909
|
let candidateElement = element && element.parentElement;
|
|
1190
910
|
|
|
@@ -1194,15 +914,6 @@ function maybeGetNextAncestorModalLauncherPortal(element) {
|
|
|
1194
914
|
|
|
1195
915
|
return candidateElement;
|
|
1196
916
|
}
|
|
1197
|
-
/**
|
|
1198
|
-
* From a given element, finds the next modal host that has been mounted in
|
|
1199
|
-
* a modal portal.
|
|
1200
|
-
* @param {?(Element | Text)} element The element whose ancestors are to be
|
|
1201
|
-
* walked.
|
|
1202
|
-
* @returns {?Element} The next portal-mounted modal host element.
|
|
1203
|
-
* TODO(kevinb): look into getting rid of this
|
|
1204
|
-
*/
|
|
1205
|
-
|
|
1206
917
|
|
|
1207
918
|
function maybeGetPortalMountedModalHostElement(element) {
|
|
1208
919
|
return maybeGetNextAncestorModalLauncherPortal(element);
|
package/dist/index.js
CHANGED
|
@@ -380,7 +380,8 @@ class ModalDialog extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
380
380
|
style,
|
|
381
381
|
children,
|
|
382
382
|
testId,
|
|
383
|
-
"aria-labelledby": ariaLabelledBy
|
|
383
|
+
"aria-labelledby": ariaLabelledBy,
|
|
384
|
+
"aria-label": ariaLabel
|
|
384
385
|
} = this.props;
|
|
385
386
|
const contextValue = {
|
|
386
387
|
ssrSize: "large",
|
|
@@ -400,6 +401,7 @@ class ModalDialog extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
400
401
|
role: role,
|
|
401
402
|
"aria-modal": "true",
|
|
402
403
|
"aria-labelledby": ariaLabelledBy,
|
|
404
|
+
"aria-label": ariaLabel,
|
|
403
405
|
style: styles.dialog,
|
|
404
406
|
testId: testId
|
|
405
407
|
}, children), above && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_3__["View"], {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-modal",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.3",
|
|
4
4
|
"design": "v2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@babel/runtime": "^7.16.3",
|
|
19
|
-
"@khanacademy/wonder-blocks-breadcrumbs": "^1.0.
|
|
19
|
+
"@khanacademy/wonder-blocks-breadcrumbs": "^1.0.31",
|
|
20
20
|
"@khanacademy/wonder-blocks-color": "^1.1.20",
|
|
21
|
-
"@khanacademy/wonder-blocks-core": "^4.3.
|
|
22
|
-
"@khanacademy/wonder-blocks-icon": "^1.2.
|
|
23
|
-
"@khanacademy/wonder-blocks-icon-button": "^3.4.
|
|
24
|
-
"@khanacademy/wonder-blocks-layout": "^1.4.
|
|
21
|
+
"@khanacademy/wonder-blocks-core": "^4.3.2",
|
|
22
|
+
"@khanacademy/wonder-blocks-icon": "^1.2.29",
|
|
23
|
+
"@khanacademy/wonder-blocks-icon-button": "^3.4.8",
|
|
24
|
+
"@khanacademy/wonder-blocks-layout": "^1.4.10",
|
|
25
25
|
"@khanacademy/wonder-blocks-spacing": "^3.0.5",
|
|
26
|
-
"@khanacademy/wonder-blocks-toolbar": "^2.1.
|
|
27
|
-
"@khanacademy/wonder-blocks-typography": "^1.1.
|
|
26
|
+
"@khanacademy/wonder-blocks-toolbar": "^2.1.32",
|
|
27
|
+
"@khanacademy/wonder-blocks-typography": "^1.1.32"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"aphrodite": "^1.2.5",
|
|
@@ -32,6 +32,6 @@
|
|
|
32
32
|
"react-dom": "16.14.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"wb-dev-build-settings": "^0.
|
|
35
|
+
"wb-dev-build-settings": "^0.4.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -232,6 +232,25 @@ exports[`wonder-blocks-modal example 3 1`] = `
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
>
|
|
235
|
+
<span
|
|
236
|
+
aria-atomic="true"
|
|
237
|
+
aria-live="polite"
|
|
238
|
+
aria-relevant="additions text"
|
|
239
|
+
className=""
|
|
240
|
+
data-test-id="dropdown-live-region"
|
|
241
|
+
style={
|
|
242
|
+
Object {
|
|
243
|
+
"border": 0,
|
|
244
|
+
"clip": "rect(0,0,0,0)",
|
|
245
|
+
"height": 1,
|
|
246
|
+
"margin": -1,
|
|
247
|
+
"overflow": "hidden",
|
|
248
|
+
"padding": 0,
|
|
249
|
+
"position": "absolute",
|
|
250
|
+
"width": 1,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/>
|
|
235
254
|
<button
|
|
236
255
|
aria-expanded="false"
|
|
237
256
|
aria-haspopup="menu"
|
|
@@ -78,6 +78,7 @@ export default class ModalDialog extends React.Component<Props> {
|
|
|
78
78
|
children,
|
|
79
79
|
testId,
|
|
80
80
|
"aria-labelledby": ariaLabelledBy,
|
|
81
|
+
"aria-label": ariaLabel,
|
|
81
82
|
} = this.props;
|
|
82
83
|
|
|
83
84
|
const contextValue: MediaLayoutContextValue = {
|
|
@@ -95,6 +96,7 @@ export default class ModalDialog extends React.Component<Props> {
|
|
|
95
96
|
role={role}
|
|
96
97
|
aria-modal="true"
|
|
97
98
|
aria-labelledby={ariaLabelledBy}
|
|
99
|
+
aria-label={ariaLabel}
|
|
98
100
|
style={styles.dialog}
|
|
99
101
|
testId={testId}
|
|
100
102
|
>
|
|
@@ -81,7 +81,7 @@ export const Simple: StoryComponentType = () => {
|
|
|
81
81
|
|
|
82
82
|
const styles = StyleSheet.create({
|
|
83
83
|
above: {
|
|
84
|
-
background: "url(
|
|
84
|
+
background: "url(./modal-above.png)",
|
|
85
85
|
width: 874,
|
|
86
86
|
height: 551,
|
|
87
87
|
position: "absolute",
|
|
@@ -90,7 +90,7 @@ const styles = StyleSheet.create({
|
|
|
90
90
|
},
|
|
91
91
|
|
|
92
92
|
below: {
|
|
93
|
-
background: "url(
|
|
93
|
+
background: "url(./modal-below.png)",
|
|
94
94
|
width: 868,
|
|
95
95
|
height: 521,
|
|
96
96
|
position: "absolute",
|