@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 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 /*#__PURE__*/React.createElement(MediaLayoutContext.Provider, {
30
+ return React.createElement(MediaLayoutContext.Provider, {
40
31
  value: contextValue
41
- }, /*#__PURE__*/React.createElement(MediaLayout, {
32
+ }, React.createElement(MediaLayout, {
42
33
  styleSheets: styleSheets$3
43
34
  }, ({
44
35
  styles
45
- }) => /*#__PURE__*/React.createElement(View, {
36
+ }) => React.createElement(View, {
46
37
  style: [styles.wrapper, style]
47
- }, below && /*#__PURE__*/React.createElement(View, {
38
+ }, below && React.createElement(View, {
48
39
  style: styles.below
49
- }, below), /*#__PURE__*/React.createElement(View, {
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 && /*#__PURE__*/React.createElement(View, {
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 /*#__PURE__*/React.createElement(View, {
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 /*#__PURE__*/React.createElement(MediaLayout, {
147
+ return React.createElement(MediaLayout, {
212
148
  styleSheets: styleSheets$2
213
149
  }, ({
214
150
  styles
215
- }) => /*#__PURE__*/React.createElement(View, {
151
+ }) => React.createElement(View, {
216
152
  style: [styles.header, !light && styles.dark],
217
153
  testId: testId
218
- }, breadcrumbs && /*#__PURE__*/React.createElement(View, {
154
+ }, breadcrumbs && React.createElement(View, {
219
155
  style: styles.breadcrumbs
220
- }, breadcrumbs), /*#__PURE__*/React.createElement(HeadingMedium, {
156
+ }, breadcrumbs), React.createElement(HeadingMedium, {
221
157
  style: styles.title,
222
158
  id: titleId,
223
159
  testId: testId && `${testId}-title`
224
- }, title), subtitle && /*#__PURE__*/React.createElement(LabelSmall, {
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
- // If the newly focused node is outside the modal, we try refocusing
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
- } // Focus should now be inside the modal, so record the newly-focused
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) {// ignore error
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 /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
313
+ return React.createElement(React.Fragment, null, React.createElement("div", {
423
314
  tabIndex: "0",
424
315
  style: {
425
316
  position: "fixed"
426
317
  }
427
- }), /*#__PURE__*/React.createElement(View, {
318
+ }), React.createElement(View, {
428
319
  style: style,
429
320
  ref: this.getModalRoot
430
- }, this.props.children), /*#__PURE__*/React.createElement("div", {
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 = // 1. try to get element specified by the user
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
- } // if found, return the first focusable element
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
- // If no focusable elements are found,
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 /*#__PURE__*/React.createElement(View, _extends({
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
- } // Prevent scrolling of the background, the first time a modal is
620
- // opened.
621
-
445
+ }
622
446
 
623
447
  ScrollDisabler.oldOverflow = body.style.overflow;
624
- ScrollDisabler.oldScrollY = window.scrollY; // We need to grab all of the original style properties before we
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"; // On mobile Safari, overflow: hidden is not enough, position:
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
- } // Reset all values on the closing of the final modal.
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 = /*#__PURE__*/React.createContext(defaultContext);
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; // Focus on the specified element after closing the modal.
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
- /*#__PURE__*/
816
- // This flow check is valid, it's the babel plugin which is broken,
817
- // see modal-context.js for details.
818
- // $FlowFixMe
819
- React.createElement(ModalContext.Provider, {
820
- value: {
821
- closeModal: this.handleCloseModal
822
- }
823
- }, renderedChildren, this.state.opened && /*#__PURE__*/ReactDOM.createPortal(
824
- /*#__PURE__*/
825
-
826
- /* We need the container View that FocusTrap creates to be at the
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 /*#__PURE__*/React.createElement(MediaLayout, {
675
+ return React.createElement(MediaLayout, {
911
676
  styleSheets: styleSheets$1
912
677
  }, ({
913
678
  styles
914
- }) => /*#__PURE__*/React.createElement(View, {
679
+ }) => React.createElement(View, {
915
680
  style: [styles.wrapper, scrollOverflow && styles.scrollOverflow]
916
- }, /*#__PURE__*/React.createElement(View, {
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 /*#__PURE__*/React.createElement(ModalContext.Consumer, null, ({
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 /*#__PURE__*/React.createElement(IconButton, {
967
- icon: icons.dismiss // TODO(mdr): Translate this string for i18n.
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 : /*#__PURE__*/React.createElement(ModalContent, null, 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 /*#__PURE__*/React.cloneElement(mainContent, {
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 /*#__PURE__*/React.createElement(View, {
773
+ return React.createElement(View, {
1038
774
  style: [styles.wrapper, !light && styles.dark, style],
1039
775
  testId: testId && `${testId}-panel`
1040
- }, closeButtonVisible && /*#__PURE__*/React.createElement(CloseButton, {
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 : /*#__PURE__*/React.createElement(ModalFooter, null, 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 /*#__PURE__*/React.createElement(ModalHeader, {
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 /*#__PURE__*/React.createElement(ModalHeader, {
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 /*#__PURE__*/React.createElement(ModalHeader, {
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 /*#__PURE__*/React.createElement(MediaLayout, {
862
+ return React.createElement(MediaLayout, {
1135
863
  styleSheets: styleSheets
1136
864
  }, ({
1137
865
  styles
1138
- }) => /*#__PURE__*/React.createElement(IDProvider, {
866
+ }) => React.createElement(IDProvider, {
1139
867
  id: titleId,
1140
868
  scope: "modal"
1141
- }, uniqueId => /*#__PURE__*/React.createElement(ModalDialog, {
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
- }, /*#__PURE__*/React.createElement(ModalPanel, {
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.0",
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.29",
19
+ "@khanacademy/wonder-blocks-breadcrumbs": "^1.0.31",
20
20
  "@khanacademy/wonder-blocks-color": "^1.1.20",
21
- "@khanacademy/wonder-blocks-core": "^4.3.0",
22
- "@khanacademy/wonder-blocks-icon": "^1.2.26",
23
- "@khanacademy/wonder-blocks-icon-button": "^3.4.5",
24
- "@khanacademy/wonder-blocks-layout": "^1.4.8",
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.30",
27
- "@khanacademy/wonder-blocks-typography": "^1.1.30"
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.3.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"
@@ -38,10 +38,6 @@ const exampleModalWithButtons = (
38
38
  );
39
39
 
40
40
  describe("ModalBackdrop", () => {
41
- beforeEach(() => {
42
- jest.useRealTimers();
43
- });
44
-
45
41
  afterEach(() => {
46
42
  unmountAll();
47
43
  if (document.body) {
@@ -25,10 +25,6 @@ const exampleModal = (
25
25
  );
26
26
 
27
27
  describe("ModalLauncher", () => {
28
- beforeEach(() => {
29
- jest.useRealTimers();
30
- });
31
-
32
28
  afterEach(() => {
33
29
  unmountAll();
34
30
  if (document.body) {
@@ -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(/modal-above.png)",
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(/modal-below.png)",
93
+ background: "url(./modal-below.png)",
94
94
  width: 868,
95
95
  height: 521,
96
96
  position: "absolute",