@khanacademy/wonder-blocks-modal 2.2.3 → 2.3.2

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