@khanacademy/wonder-blocks-modal 2.1.45 → 2.2.0

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 ADDED
@@ -0,0 +1,10 @@
1
+ # @khanacademy/wonder-blocks-modal
2
+
3
+ ## 2.2.0
4
+ ### Minor Changes
5
+
6
+ - e7bbf149: Modals will no longer close when a user presses in the panel, drags, and releases the mouse in backdrop and vice versa.
7
+
8
+ ### Patch Changes
9
+
10
+ - @khanacademy/wonder-blocks-icon-button@3.4.2
package/dist/es/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { Component, createElement, Fragment, createContext, cloneElement } from 'react';
1
+ import * as React from 'react';
2
2
  import { StyleSheet } from 'aphrodite';
3
3
  import { MediaLayoutContext, MediaLayout, MEDIA_MODAL_SPEC } from '@khanacademy/wonder-blocks-layout';
4
4
  import { View, IDProvider } from '@khanacademy/wonder-blocks-core';
5
5
  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
- import { findDOMNode, createPortal } from 'react-dom';
8
+ import * as ReactDOM from 'react-dom';
9
9
  import _extends from '@babel/runtime/helpers/extends';
10
10
  import { icons } from '@khanacademy/wonder-blocks-icon';
11
11
  import IconButton from '@khanacademy/wonder-blocks-icon-button';
@@ -20,7 +20,7 @@ import IconButton from '@khanacademy/wonder-blocks-icon-button';
20
20
  * - If there is a custom Dialog implementation (e.g. `TwoPaneDialog`), the dialog element doesn’t have to have
21
21
  * the `aria-labelledby` attribute however this is recommended. It should match the `id` of the dialog title.
22
22
  */
23
- class ModalDialog extends Component {
23
+ class ModalDialog extends React.Component {
24
24
  render() {
25
25
  const {
26
26
  above,
@@ -35,23 +35,23 @@ class ModalDialog extends Component {
35
35
  ssrSize: "large",
36
36
  mediaSpec: MEDIA_MODAL_SPEC
37
37
  };
38
- return /*#__PURE__*/createElement(MediaLayoutContext.Provider, {
38
+ return /*#__PURE__*/React.createElement(MediaLayoutContext.Provider, {
39
39
  value: contextValue
40
- }, /*#__PURE__*/createElement(MediaLayout, {
41
- styleSheets: styleSheets
40
+ }, /*#__PURE__*/React.createElement(MediaLayout, {
41
+ styleSheets: styleSheets$3
42
42
  }, ({
43
43
  styles
44
- }) => /*#__PURE__*/createElement(View, {
44
+ }) => /*#__PURE__*/React.createElement(View, {
45
45
  style: [styles.wrapper, style]
46
- }, below && /*#__PURE__*/createElement(View, {
46
+ }, below && /*#__PURE__*/React.createElement(View, {
47
47
  style: styles.below
48
- }, below), /*#__PURE__*/createElement(View, {
48
+ }, below), /*#__PURE__*/React.createElement(View, {
49
49
  role: role,
50
50
  "aria-modal": "true",
51
51
  "aria-labelledby": ariaLabelledBy,
52
52
  style: styles.dialog,
53
53
  testId: testId
54
- }, children), above && /*#__PURE__*/createElement(View, {
54
+ }, children), above && /*#__PURE__*/React.createElement(View, {
55
55
  style: styles.above
56
56
  }, above))));
57
57
  }
@@ -60,7 +60,7 @@ class ModalDialog extends Component {
60
60
  ModalDialog.defaultProps = {
61
61
  role: "dialog"
62
62
  };
63
- const styleSheets = {
63
+ const styleSheets$3 = {
64
64
  all: StyleSheet.create({
65
65
  wrapper: {
66
66
  display: "flex",
@@ -116,7 +116,7 @@ const styleSheets = {
116
116
  * - Make sure to include it as part of [ModalPanel](/#modalpanel) by using the `footer` prop.
117
117
  * - The footer is completely flexible. Meaning the developer needs to add its own custom layout to match design specs.
118
118
  */
119
- class ModalFooter extends Component {
119
+ class ModalFooter extends React.Component {
120
120
  static isClassOf(instance) {
121
121
  return instance && instance.type && instance.type.__IS_MODAL_FOOTER__;
122
122
  }
@@ -125,14 +125,14 @@ class ModalFooter extends Component {
125
125
  const {
126
126
  children
127
127
  } = this.props;
128
- return /*#__PURE__*/createElement(View, {
129
- style: styles.footer
128
+ return /*#__PURE__*/React.createElement(View, {
129
+ style: styles$3.footer
130
130
  }, children);
131
131
  }
132
132
 
133
133
  }
134
134
  ModalFooter.__IS_MODAL_FOOTER__ = true;
135
- const styles = StyleSheet.create({
135
+ const styles$3 = StyleSheet.create({
136
136
  footer: {
137
137
  flex: "0 0 auto",
138
138
  boxSizing: "border-box",
@@ -192,7 +192,7 @@ const styles = StyleSheet.create({
192
192
  * />
193
193
  * ```
194
194
  */
195
- class ModalHeader extends Component {
195
+ class ModalHeader extends React.Component {
196
196
  render() {
197
197
  const {
198
198
  breadcrumbs = undefined,
@@ -207,20 +207,20 @@ class ModalHeader extends Component {
207
207
  throw new Error("'subtitle' and 'breadcrumbs' can't be used together");
208
208
  }
209
209
 
210
- return /*#__PURE__*/createElement(MediaLayout, {
211
- styleSheets: styleSheets$1
210
+ return /*#__PURE__*/React.createElement(MediaLayout, {
211
+ styleSheets: styleSheets$2
212
212
  }, ({
213
213
  styles
214
- }) => /*#__PURE__*/createElement(View, {
214
+ }) => /*#__PURE__*/React.createElement(View, {
215
215
  style: [styles.header, !light && styles.dark],
216
216
  testId: testId
217
- }, breadcrumbs && /*#__PURE__*/createElement(View, {
217
+ }, breadcrumbs && /*#__PURE__*/React.createElement(View, {
218
218
  style: styles.breadcrumbs
219
- }, breadcrumbs), /*#__PURE__*/createElement(HeadingMedium, {
219
+ }, breadcrumbs), /*#__PURE__*/React.createElement(HeadingMedium, {
220
220
  style: styles.title,
221
221
  id: titleId,
222
222
  testId: testId && `${testId}-title`
223
- }, title), subtitle && /*#__PURE__*/createElement(LabelSmall, {
223
+ }, title), subtitle && /*#__PURE__*/React.createElement(LabelSmall, {
224
224
  style: light && styles.subtitle,
225
225
  testId: testId && `${testId}-subtitle`
226
226
  }, subtitle)));
@@ -230,7 +230,7 @@ class ModalHeader extends Component {
230
230
  ModalHeader.defaultProps = {
231
231
  light: true
232
232
  };
233
- const styleSheets$1 = {
233
+ const styleSheets$2 = {
234
234
  all: StyleSheet.create({
235
235
  header: {
236
236
  boxShadow: `0px 1px 0px ${Color.offBlack16}`,
@@ -269,7 +269,7 @@ const styleSheets$1 = {
269
269
  })
270
270
  };
271
271
 
272
- class FocusTrap extends Component {
272
+ class FocusTrap extends React.Component {
273
273
  /** The most recent node _inside this component_ to receive focus. */
274
274
 
275
275
  /**
@@ -289,7 +289,7 @@ class FocusTrap extends Component {
289
289
  return;
290
290
  }
291
291
 
292
- const modalRoot = findDOMNode(node);
292
+ const modalRoot = ReactDOM.findDOMNode(node);
293
293
 
294
294
  if (!modalRoot) {
295
295
  throw new Error("Assertion error: modal root should exist after mount");
@@ -418,15 +418,15 @@ class FocusTrap extends Component {
418
418
  const {
419
419
  style
420
420
  } = this.props;
421
- return /*#__PURE__*/createElement(Fragment, null, /*#__PURE__*/createElement("div", {
421
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
422
422
  tabIndex: "0",
423
423
  style: {
424
424
  position: "fixed"
425
425
  }
426
- }), /*#__PURE__*/createElement(View, {
426
+ }), /*#__PURE__*/React.createElement(View, {
427
427
  style: style,
428
428
  ref: this.getModalRoot
429
- }, this.props.children), /*#__PURE__*/createElement("div", {
429
+ }, this.props.children), /*#__PURE__*/React.createElement("div", {
430
430
  tabIndex: "0",
431
431
  style: {
432
432
  position: "fixed"
@@ -460,21 +460,29 @@ function findFocusableNodes(root) {
460
460
  * and adding an `onClose` prop that will call `onCloseModal`. If an
461
461
  * `onClose` prop is already provided, the two are merged.
462
462
  */
463
- class ModalBackdrop extends Component {
463
+ class ModalBackdrop extends React.Component {
464
464
  constructor(...args) {
465
465
  super(...args);
466
+ this._mousePressedOutside = false;
467
+
468
+ this.handleMouseDown = e => {
469
+ // Confirm that it is the backdrop that is being clicked, not the child
470
+ this._mousePressedOutside = e.target === e.currentTarget;
471
+ };
466
472
 
467
- this.handleClick = e => {
468
- // Was the lowest-level click target (`e.target`) the positioner element
469
- // (`e.currentTarget`)?
470
- if (e.target === e.currentTarget) {
473
+ 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
+ if (e.target === e.currentTarget && this._mousePressedOutside) {
471
477
  this.props.onCloseModal();
472
478
  }
479
+
480
+ this._mousePressedOutside = false;
473
481
  };
474
482
  }
475
483
 
476
484
  componentDidMount() {
477
- const node = findDOMNode(this);
485
+ const node = ReactDOM.findDOMNode(this);
478
486
 
479
487
  if (!node) {
480
488
  return;
@@ -490,11 +498,10 @@ class ModalBackdrop extends Component {
490
498
  firstFocusableElement.focus();
491
499
  }, 0);
492
500
  }
501
+
493
502
  /**
494
503
  * Returns an element specified by the user
495
504
  */
496
-
497
-
498
505
  _getInitialFocusElement(node) {
499
506
  const {
500
507
  initialFocusId
@@ -504,7 +511,7 @@ class ModalBackdrop extends Component {
504
511
  return null;
505
512
  }
506
513
 
507
- return findDOMNode(node.querySelector(`#${initialFocusId}`));
514
+ return ReactDOM.findDOMNode(node.querySelector(`#${initialFocusId}`));
508
515
  }
509
516
  /**
510
517
  * Returns the first focusable element found inside the Dialog
@@ -530,7 +537,7 @@ class ModalBackdrop extends Component {
530
537
  _getDialogElement(node) {
531
538
  // If no focusable elements are found,
532
539
  // the dialog content element itself will receive focus.
533
- const dialogElement = findDOMNode(node.querySelector('[role="dialog"]')); // add tabIndex to make the Dialog focusable
540
+ const dialogElement = ReactDOM.findDOMNode(node.querySelector('[role="dialog"]')); // add tabIndex to make the Dialog focusable
534
541
 
535
542
  dialogElement.tabIndex = -1;
536
543
  return dialogElement;
@@ -550,15 +557,16 @@ class ModalBackdrop extends Component {
550
557
  const backdropProps = {
551
558
  [ModalLauncherPortalAttributeName]: true
552
559
  };
553
- return /*#__PURE__*/createElement(View, _extends({
554
- style: styles$1.modalPositioner,
555
- onClick: this.handleClick,
560
+ return /*#__PURE__*/React.createElement(View, _extends({
561
+ style: styles$2.modalPositioner,
562
+ onMouseDown: this.handleMouseDown,
563
+ onMouseUp: this.handleMouseUp,
556
564
  testId: testId
557
565
  }, backdropProps), children);
558
566
  }
559
567
 
560
568
  }
561
- const styles$1 = StyleSheet.create({
569
+ const styles$2 = StyleSheet.create({
562
570
  modalPositioner: {
563
571
  position: "fixed",
564
572
  left: 0,
@@ -600,7 +608,7 @@ const needsHackyMobileSafariScrollDisabler = (() => {
600
608
  return userAgent.indexOf("iPad") > -1 || userAgent.indexOf("iPhone") > -1;
601
609
  })();
602
610
 
603
- class ScrollDisabler extends Component {
611
+ class ScrollDisabler extends React.Component {
604
612
  componentDidMount() {
605
613
  if (ScrollDisabler.numModalsOpened === 0) {
606
614
  const body = document.body;
@@ -671,7 +679,7 @@ ScrollDisabler.numModalsOpened = 0;
671
679
  const defaultContext = {
672
680
  closeModal: undefined
673
681
  };
674
- var ModalContext = /*#__PURE__*/createContext(defaultContext);
682
+ var ModalContext = /*#__PURE__*/React.createContext(defaultContext);
675
683
 
676
684
  /**
677
685
  * This component enables you to launch a modal, covering the screen.
@@ -689,7 +697,7 @@ var ModalContext = /*#__PURE__*/createContext(defaultContext);
689
697
  * like OnePaneDialog and is provided via
690
698
  * the `modal` prop.
691
699
  */
692
- class ModalLauncher extends Component {
700
+ class ModalLauncher extends React.Component {
693
701
  constructor(...args) {
694
702
  super(...args);
695
703
  this.state = {
@@ -778,24 +786,24 @@ class ModalLauncher extends Component {
778
786
  // This flow check is valid, it's the babel plugin which is broken,
779
787
  // see modal-context.js for details.
780
788
  // $FlowFixMe
781
- createElement(ModalContext.Provider, {
789
+ React.createElement(ModalContext.Provider, {
782
790
  value: {
783
791
  closeModal: this.handleCloseModal
784
792
  }
785
- }, renderedChildren, this.state.opened && /*#__PURE__*/createPortal(
793
+ }, renderedChildren, this.state.opened && /*#__PURE__*/ReactDOM.createPortal(
786
794
  /*#__PURE__*/
787
795
 
788
796
  /* We need the container View that FocusTrap creates to be at the
789
797
  correct z-index so that it'll be above the global nav in webapp. */
790
- createElement(FocusTrap, {
791
- style: styles$2.container
792
- }, /*#__PURE__*/createElement(ModalBackdrop, {
798
+ React.createElement(FocusTrap, {
799
+ style: styles$1.container
800
+ }, /*#__PURE__*/React.createElement(ModalBackdrop, {
793
801
  initialFocusId: this.props.initialFocusId,
794
802
  testId: this.props.testId,
795
803
  onCloseModal: this.props.backdropDismissEnabled ? this.handleCloseModal : () => {}
796
- }, this._renderModal())), body), this.state.opened && /*#__PURE__*/createElement(ModalLauncherKeypressListener, {
804
+ }, this._renderModal())), body), this.state.opened && /*#__PURE__*/React.createElement(ModalLauncherKeypressListener, {
797
805
  onClose: this.handleCloseModal
798
- }), this.state.opened && /*#__PURE__*/createElement(ScrollDisabler, null))
806
+ }), this.state.opened && /*#__PURE__*/React.createElement(ScrollDisabler, null))
799
807
  );
800
808
  }
801
809
 
@@ -806,7 +814,7 @@ ModalLauncher.defaultProps = {
806
814
  backdropDismissEnabled: true
807
815
  };
808
816
 
809
- class ModalLauncherKeypressListener extends Component {
817
+ class ModalLauncherKeypressListener extends React.Component {
810
818
  constructor(...args) {
811
819
  super(...args);
812
820
 
@@ -842,7 +850,7 @@ class ModalLauncherKeypressListener extends Component {
842
850
 
843
851
  }
844
852
 
845
- const styles$2 = StyleSheet.create({
853
+ const styles$1 = StyleSheet.create({
846
854
  container: {
847
855
  // This z-index is copied from the Khan Academy webapp.
848
856
  //
@@ -856,7 +864,7 @@ const styles$2 = StyleSheet.create({
856
864
  /**
857
865
  * The Modal content included after the header
858
866
  */
859
- class ModalContent extends Component {
867
+ class ModalContent extends React.Component {
860
868
  static isClassOf(instance) {
861
869
  return instance && instance.type && instance.type.__IS_MODAL_CONTENT__;
862
870
  }
@@ -867,13 +875,13 @@ class ModalContent extends Component {
867
875
  style,
868
876
  children
869
877
  } = this.props;
870
- return /*#__PURE__*/createElement(MediaLayout, {
871
- styleSheets: styleSheets$2
878
+ return /*#__PURE__*/React.createElement(MediaLayout, {
879
+ styleSheets: styleSheets$1
872
880
  }, ({
873
881
  styles
874
- }) => /*#__PURE__*/createElement(View, {
882
+ }) => /*#__PURE__*/React.createElement(View, {
875
883
  style: [styles.wrapper, scrollOverflow && styles.scrollOverflow]
876
- }, /*#__PURE__*/createElement(View, {
884
+ }, /*#__PURE__*/React.createElement(View, {
877
885
  style: [styles.content, style]
878
886
  }, children)));
879
887
  }
@@ -883,7 +891,7 @@ ModalContent.defaultProps = {
883
891
  scrollOverflow: true
884
892
  };
885
893
  ModalContent.__IS_MODAL_CONTENT__ = true;
886
- const styleSheets$2 = {
894
+ const styleSheets$1 = {
887
895
  all: StyleSheet.create({
888
896
  wrapper: {
889
897
  flex: 1,
@@ -908,7 +916,7 @@ const styleSheets$2 = {
908
916
  })
909
917
  };
910
918
 
911
- class CloseButton extends Component {
919
+ class CloseButton extends React.Component {
912
920
  render() {
913
921
  const {
914
922
  light,
@@ -916,14 +924,14 @@ class CloseButton extends Component {
916
924
  style,
917
925
  testId
918
926
  } = this.props;
919
- return /*#__PURE__*/createElement(ModalContext.Consumer, null, ({
927
+ return /*#__PURE__*/React.createElement(ModalContext.Consumer, null, ({
920
928
  closeModal
921
929
  }) => {
922
930
  if (closeModal && onClick) {
923
931
  throw new Error("You've specified 'onClose' on a modal when using ModalLauncher. Please specify 'onClose' on the ModalLauncher instead");
924
932
  }
925
933
 
926
- return /*#__PURE__*/createElement(IconButton, {
934
+ return /*#__PURE__*/React.createElement(IconButton, {
927
935
  icon: icons.dismiss // TODO(mdr): Translate this string for i18n.
928
936
  // TODO(kevinb): provide a way to set this label
929
937
  ,
@@ -959,27 +967,27 @@ class CloseButton extends Component {
959
967
  * </ModalDialog>
960
968
  * ```
961
969
  */
962
- class ModalPanel extends Component {
970
+ class ModalPanel extends React.Component {
963
971
  renderMainContent() {
964
972
  const {
965
973
  content,
966
974
  footer,
967
975
  scrollOverflow
968
976
  } = this.props;
969
- const mainContent = ModalContent.isClassOf(content) ? content : /*#__PURE__*/createElement(ModalContent, null, content);
977
+ const mainContent = ModalContent.isClassOf(content) ? content : /*#__PURE__*/React.createElement(ModalContent, null, content);
970
978
 
971
979
  if (!mainContent) {
972
980
  return mainContent;
973
981
  }
974
982
 
975
- return /*#__PURE__*/cloneElement(mainContent, {
983
+ return /*#__PURE__*/React.cloneElement(mainContent, {
976
984
  // Pass the scrollOverflow and header in to the main content
977
985
  scrollOverflow,
978
986
  // We override the styling of the main content to help position
979
987
  // it if there is a footer or close button being
980
988
  // shown. We have to do this here as the ModalContent doesn't
981
989
  // know about things being positioned around it.
982
- style: [!!footer && styles$3.hasFooter, mainContent.props.style]
990
+ style: [!!footer && styles.hasFooter, mainContent.props.style]
983
991
  });
984
992
  }
985
993
 
@@ -994,15 +1002,15 @@ class ModalPanel extends Component {
994
1002
  testId
995
1003
  } = this.props;
996
1004
  const mainContent = this.renderMainContent();
997
- return /*#__PURE__*/createElement(View, {
998
- style: [styles$3.wrapper, !light && styles$3.dark, style],
1005
+ return /*#__PURE__*/React.createElement(View, {
1006
+ style: [styles.wrapper, !light && styles.dark, style],
999
1007
  testId: testId && `${testId}-panel`
1000
- }, closeButtonVisible && /*#__PURE__*/createElement(CloseButton, {
1008
+ }, closeButtonVisible && /*#__PURE__*/React.createElement(CloseButton, {
1001
1009
  light: !light,
1002
1010
  onClick: onClose,
1003
- style: styles$3.closeButton,
1011
+ style: styles.closeButton,
1004
1012
  testId: testId && `${testId}-close`
1005
- }), header, mainContent, !footer || ModalFooter.isClassOf(footer) ? footer : /*#__PURE__*/createElement(ModalFooter, null, footer));
1013
+ }), header, mainContent, !footer || ModalFooter.isClassOf(footer) ? footer : /*#__PURE__*/React.createElement(ModalFooter, null, footer));
1006
1014
  }
1007
1015
 
1008
1016
  }
@@ -1011,7 +1019,7 @@ ModalPanel.defaultProps = {
1011
1019
  scrollOverflow: true,
1012
1020
  light: true
1013
1021
  };
1014
- const styles$3 = StyleSheet.create({
1022
+ const styles = StyleSheet.create({
1015
1023
  wrapper: {
1016
1024
  flex: "1 1 auto",
1017
1025
  position: "relative",
@@ -1046,7 +1054,7 @@ const styles$3 = StyleSheet.create({
1046
1054
  * The ModalHeader is required, but the ModalFooter is optional.
1047
1055
  * The content of the dialog itself is fully customizable, but the left/right/top/bottom padding is fixed.
1048
1056
  */
1049
- class OnePaneDialog extends Component {
1057
+ class OnePaneDialog extends React.Component {
1050
1058
  renderHeader(uniqueId) {
1051
1059
  const {
1052
1060
  title,
@@ -1056,21 +1064,21 @@ class OnePaneDialog extends Component {
1056
1064
  } = this.props;
1057
1065
 
1058
1066
  if (breadcrumbs) {
1059
- return /*#__PURE__*/createElement(ModalHeader, {
1067
+ return /*#__PURE__*/React.createElement(ModalHeader, {
1060
1068
  title: title,
1061
1069
  breadcrumbs: breadcrumbs,
1062
1070
  titleId: uniqueId,
1063
1071
  testId: testId && `${testId}-header`
1064
1072
  });
1065
1073
  } else if (subtitle) {
1066
- return /*#__PURE__*/createElement(ModalHeader, {
1074
+ return /*#__PURE__*/React.createElement(ModalHeader, {
1067
1075
  title: title,
1068
1076
  subtitle: subtitle,
1069
1077
  titleId: uniqueId,
1070
1078
  testId: testId && `${testId}-header`
1071
1079
  });
1072
1080
  } else {
1073
- return /*#__PURE__*/createElement(ModalHeader, {
1081
+ return /*#__PURE__*/React.createElement(ModalHeader, {
1074
1082
  title: title,
1075
1083
  titleId: uniqueId,
1076
1084
  testId: testId && `${testId}-header`
@@ -1091,21 +1099,21 @@ class OnePaneDialog extends Component {
1091
1099
  titleId,
1092
1100
  role
1093
1101
  } = this.props;
1094
- return /*#__PURE__*/createElement(MediaLayout, {
1095
- styleSheets: styleSheets$3
1102
+ return /*#__PURE__*/React.createElement(MediaLayout, {
1103
+ styleSheets: styleSheets
1096
1104
  }, ({
1097
1105
  styles
1098
- }) => /*#__PURE__*/createElement(IDProvider, {
1106
+ }) => /*#__PURE__*/React.createElement(IDProvider, {
1099
1107
  id: titleId,
1100
1108
  scope: "modal"
1101
- }, uniqueId => /*#__PURE__*/createElement(ModalDialog, {
1109
+ }, uniqueId => /*#__PURE__*/React.createElement(ModalDialog, {
1102
1110
  style: [styles.dialog, style],
1103
1111
  above: above,
1104
1112
  below: below,
1105
1113
  testId: testId,
1106
1114
  "aria-labelledby": uniqueId,
1107
1115
  role: role
1108
- }, /*#__PURE__*/createElement(ModalPanel, {
1116
+ }, /*#__PURE__*/React.createElement(ModalPanel, {
1109
1117
  onClose: onClose,
1110
1118
  header: this.renderHeader(uniqueId),
1111
1119
  content: content,
@@ -1119,7 +1127,7 @@ class OnePaneDialog extends Component {
1119
1127
  OnePaneDialog.defaultProps = {
1120
1128
  closeButtonVisible: true
1121
1129
  };
1122
- const styleSheets$3 = {
1130
+ const styleSheets = {
1123
1131
  small: StyleSheet.create({
1124
1132
  dialog: {
1125
1133
  width: "100%",
package/dist/index.js CHANGED
@@ -1285,13 +1285,21 @@ function _extends() { _extends = Object.assign || function (target) { for (var i
1285
1285
  class ModalBackdrop extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
1286
1286
  constructor(...args) {
1287
1287
  super(...args);
1288
+ this._mousePressedOutside = false;
1288
1289
 
1289
- this.handleClick = e => {
1290
- // Was the lowest-level click target (`e.target`) the positioner element
1291
- // (`e.currentTarget`)?
1292
- if (e.target === e.currentTarget) {
1290
+ this.handleMouseDown = e => {
1291
+ // Confirm that it is the backdrop that is being clicked, not the child
1292
+ this._mousePressedOutside = e.target === e.currentTarget;
1293
+ };
1294
+
1295
+ this.handleMouseUp = e => {
1296
+ // Confirm that it is the backdrop that is being clicked, not the child
1297
+ // and that the mouse was pressed in the backdrop first.
1298
+ if (e.target === e.currentTarget && this._mousePressedOutside) {
1293
1299
  this.props.onCloseModal();
1294
1300
  }
1301
+
1302
+ this._mousePressedOutside = false;
1295
1303
  };
1296
1304
  }
1297
1305
 
@@ -1312,11 +1320,10 @@ class ModalBackdrop extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
1312
1320
  firstFocusableElement.focus();
1313
1321
  }, 0);
1314
1322
  }
1323
+
1315
1324
  /**
1316
1325
  * Returns an element specified by the user
1317
1326
  */
1318
-
1319
-
1320
1327
  _getInitialFocusElement(node) {
1321
1328
  const {
1322
1329
  initialFocusId
@@ -1374,7 +1381,8 @@ class ModalBackdrop extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
1374
1381
  };
1375
1382
  return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_4__["View"], _extends({
1376
1383
  style: styles.modalPositioner,
1377
- onClick: this.handleClick,
1384
+ onMouseDown: this.handleMouseDown,
1385
+ onMouseUp: this.handleMouseUp,
1378
1386
  testId: testId
1379
1387
  }, backdropProps), children);
1380
1388
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-modal",
3
- "version": "2.1.45",
3
+ "version": "2.2.0",
4
4
  "design": "v2",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -20,7 +20,7 @@
20
20
  "@khanacademy/wonder-blocks-color": "^1.1.20",
21
21
  "@khanacademy/wonder-blocks-core": "^4.0.0",
22
22
  "@khanacademy/wonder-blocks-icon": "^1.2.24",
23
- "@khanacademy/wonder-blocks-icon-button": "^3.4.1",
23
+ "@khanacademy/wonder-blocks-icon-button": "^3.4.2",
24
24
  "@khanacademy/wonder-blocks-layout": "^1.4.6",
25
25
  "@khanacademy/wonder-blocks-spacing": "^3.0.5",
26
26
  "@khanacademy/wonder-blocks-toolbar": "^2.1.28",
@@ -33,6 +33,5 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "wb-dev-build-settings": "^0.2.0"
36
- },
37
- "gitHead": "9ebea88533e702011165072f090a377e02fa3f0f"
36
+ }
38
37
  }
@@ -21,8 +21,8 @@ exports[`wonder-blocks-modal example 1 1`] = `
21
21
  }
22
22
  >
23
23
  <button
24
+ aria-disabled={false}
24
25
  className=""
25
- disabled={false}
26
26
  onBlur={[Function]}
27
27
  onClick={[Function]}
28
28
  onDragStart={[Function]}
@@ -116,8 +116,8 @@ exports[`wonder-blocks-modal example 2 1`] = `
116
116
  }
117
117
  >
118
118
  <button
119
+ aria-disabled={false}
119
120
  className=""
120
- disabled={false}
121
121
  onBlur={[Function]}
122
122
  onClick={[Function]}
123
123
  onDragStart={[Function]}
@@ -392,8 +392,8 @@ exports[`wonder-blocks-modal example 4 1`] = `
392
392
  }
393
393
  >
394
394
  <button
395
+ aria-disabled={false}
395
396
  className=""
396
- disabled={false}
397
397
  onBlur={[Function]}
398
398
  onClick={[Function]}
399
399
  onDragStart={[Function]}
@@ -487,8 +487,8 @@ exports[`wonder-blocks-modal example 5 1`] = `
487
487
  }
488
488
  >
489
489
  <button
490
+ aria-disabled={false}
490
491
  className=""
491
- disabled={false}
492
492
  onBlur={[Function]}
493
493
  onClick={[Function]}
494
494
  onDragStart={[Function]}
@@ -1387,8 +1387,8 @@ exports[`wonder-blocks-modal example 7 1`] = `
1387
1387
  }
1388
1388
  >
1389
1389
  <button
1390
+ aria-disabled={false}
1390
1391
  className=""
1391
- disabled={false}
1392
1392
  onBlur={[Function]}
1393
1393
  onClick={[Function]}
1394
1394
  onDragStart={[Function]}
@@ -1996,8 +1996,8 @@ exports[`wonder-blocks-modal example 8 1`] = `
1996
1996
  }
1997
1997
  >
1998
1998
  <button
1999
+ aria-disabled={false}
1999
2000
  className=""
2000
- disabled={false}
2001
2001
  onBlur={[Function]}
2002
2002
  onClick={[Function]}
2003
2003
  onDragStart={[Function]}
@@ -2070,8 +2070,8 @@ exports[`wonder-blocks-modal example 8 1`] = `
2070
2070
  </span>
2071
2071
  </button>
2072
2072
  <button
2073
+ aria-disabled={false}
2073
2074
  className=""
2074
- disabled={false}
2075
2075
  onBlur={[Function]}
2076
2076
  onClick={[Function]}
2077
2077
  onDragStart={[Function]}
@@ -2144,8 +2144,8 @@ exports[`wonder-blocks-modal example 8 1`] = `
2144
2144
  </span>
2145
2145
  </button>
2146
2146
  <button
2147
+ aria-disabled={false}
2147
2148
  className=""
2148
- disabled={false}
2149
2149
  onBlur={[Function]}
2150
2150
  onClick={[Function]}
2151
2151
  onDragStart={[Function]}
@@ -2627,8 +2627,8 @@ exports[`wonder-blocks-modal example 9 1`] = `
2627
2627
  }
2628
2628
  >
2629
2629
  <button
2630
+ aria-disabled={false}
2630
2631
  className=""
2631
- disabled={false}
2632
2632
  onBlur={[Function]}
2633
2633
  onClick={[Function]}
2634
2634
  onDragStart={[Function]}
@@ -2726,8 +2726,8 @@ exports[`wonder-blocks-modal example 9 1`] = `
2726
2726
  }
2727
2727
  />
2728
2728
  <button
2729
+ aria-disabled={false}
2729
2730
  className=""
2730
- disabled={false}
2731
2731
  onBlur={[Function]}
2732
2732
  onClick={[Function]}
2733
2733
  onDragStart={[Function]}
@@ -3321,8 +3321,8 @@ exports[`wonder-blocks-modal example 10 1`] = `
3321
3321
  }
3322
3322
  />
3323
3323
  <button
3324
+ aria-disabled={false}
3324
3325
  className=""
3325
- disabled={false}
3326
3326
  onBlur={[Function]}
3327
3327
  onClick={[Function]}
3328
3328
  onDragStart={[Function]}
@@ -1,6 +1,7 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import {mount} from "enzyme";
4
+ import "jest-enzyme";
4
5
 
5
6
  import expectRenderError from "../../../../../utils/testing/expect-render-error.js";
6
7
  import CloseButton from "../close-button.js";
@@ -1,6 +1,9 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import {mount} from "enzyme";
4
+ import "jest-enzyme";
5
+ import {render, screen, fireEvent} from "@testing-library/react";
6
+ import userEvent from "@testing-library/user-event";
4
7
 
5
8
  import ModalBackdrop from "../modal-backdrop.js";
6
9
  import OnePaneDialog from "../one-pane-dialog.js";
@@ -16,6 +19,7 @@ const exampleModal = (
16
19
  content={<div data-modal-content />}
17
20
  title="Title"
18
21
  footer={<div data-modal-footer />}
22
+ testId="example-modal-test-id"
19
23
  />
20
24
  );
21
25
 
@@ -46,19 +50,24 @@ describe("ModalBackdrop", () => {
46
50
  });
47
51
 
48
52
  test("Clicking the backdrop triggers `onCloseModal`", () => {
53
+ // Arrange
49
54
  const onCloseModal = jest.fn();
50
55
 
51
- // We use `mount` instead of `shallow` here, because the component's
52
- // click handler expects actual DOM events.
53
- const wrapper = mount(
54
- <ModalBackdrop onCloseModal={onCloseModal}>
56
+ render(
57
+ <ModalBackdrop
58
+ onCloseModal={onCloseModal}
59
+ testId="modal-backdrop-test-id"
60
+ >
55
61
  {exampleModal}
56
62
  </ModalBackdrop>,
57
63
  );
58
64
 
59
- expect(onCloseModal).not.toHaveBeenCalled();
65
+ const backdrop = screen.getByTestId("modal-backdrop-test-id");
66
+
67
+ //Act
68
+ userEvent.click(backdrop);
60
69
 
61
- wrapper.simulate("click");
70
+ // Assert
62
71
  expect(onCloseModal).toHaveBeenCalled();
63
72
  });
64
73
 
@@ -77,6 +86,62 @@ describe("ModalBackdrop", () => {
77
86
  expect(onCloseModal).not.toHaveBeenCalled();
78
87
  });
79
88
 
89
+ test("Clicking and dragging into the backdrop does not close modal", () => {
90
+ // Arrange
91
+ const onCloseModal = jest.fn();
92
+
93
+ render(
94
+ <ModalBackdrop
95
+ onCloseModal={onCloseModal}
96
+ testId="modal-backdrop-test-id"
97
+ >
98
+ {exampleModal}
99
+ </ModalBackdrop>,
100
+ );
101
+
102
+ const panel = screen.getByTestId("example-modal-test-id");
103
+ const backdrop = screen.getByTestId("modal-backdrop-test-id");
104
+
105
+ // Act
106
+
107
+ // Dragging the mouse
108
+ // eslint-disable-next-line testing-library/prefer-user-event
109
+ fireEvent.mouseDown(panel);
110
+ // eslint-disable-next-line testing-library/prefer-user-event
111
+ fireEvent.mouseUp(backdrop);
112
+
113
+ // Assert
114
+ expect(onCloseModal).not.toHaveBeenCalled();
115
+ });
116
+
117
+ test("Clicking and dragging in from the backdrop does not close modal", () => {
118
+ // Arrange
119
+ const onCloseModal = jest.fn();
120
+
121
+ render(
122
+ <ModalBackdrop
123
+ onCloseModal={onCloseModal}
124
+ testId="modal-backdrop-test-id"
125
+ >
126
+ {exampleModal}
127
+ </ModalBackdrop>,
128
+ );
129
+
130
+ const panel = screen.getByTestId("example-modal-test-id");
131
+ const backdrop = screen.getByTestId("modal-backdrop-test-id");
132
+
133
+ // Act
134
+
135
+ // Dragging the mouse
136
+ // eslint-disable-next-line testing-library/prefer-user-event
137
+ fireEvent.mouseDown(backdrop);
138
+ // eslint-disable-next-line testing-library/prefer-user-event
139
+ fireEvent.mouseUp(panel);
140
+
141
+ // Assert
142
+ expect(onCloseModal).not.toHaveBeenCalled();
143
+ });
144
+
80
145
  test("Clicking the modal footer does not trigger `onCloseModal`", () => {
81
146
  const onCloseModal = jest.fn();
82
147
 
@@ -96,6 +161,7 @@ describe("ModalBackdrop", () => {
96
161
  // Arrange
97
162
  // We need the elements in the DOM document, it seems, for this test
98
163
  // to work. Changing to testing-library will likely fix this.
164
+ // Then we can remove the lint suppression.
99
165
  const attachElement = getElementAttachedToDocument("container");
100
166
  const initialFocusId = "initial-focus";
101
167
 
@@ -125,7 +191,9 @@ describe("ModalBackdrop", () => {
125
191
  // Assert
126
192
  // first we verify the element exists in the DOM
127
193
  expect(initialFocusElement).toHaveLength(1);
194
+
128
195
  // verify the focus is set on the correct element
196
+ // eslint-disable-next-line testing-library/no-node-access
129
197
  expect(document.activeElement).toBe(initialFocusElement.getDOMNode());
130
198
  });
131
199
 
@@ -133,6 +201,7 @@ describe("ModalBackdrop", () => {
133
201
  // Arrange
134
202
  // We need the elements in the DOM document, it seems, for this test
135
203
  // to work. Changing to testing-library will likely fix this.
204
+ // Then we can remove the lint suppression.
136
205
  const attachElement = getElementAttachedToDocument("container");
137
206
  const initialFocusId = "initial-focus";
138
207
  const firstFocusableElement = "[data-first-button]";
@@ -155,6 +224,7 @@ describe("ModalBackdrop", () => {
155
224
  // first we verify the element doesn't exist in the DOM
156
225
  expect(initialFocusElement).toHaveLength(0);
157
226
  // verify the focus is set on the first focusable element instead
227
+ // eslint-disable-next-line testing-library/no-node-access
158
228
  expect(document.activeElement).toBe(
159
229
  wrapper.find(firstFocusableElement).getDOMNode(),
160
230
  );
@@ -164,6 +234,7 @@ describe("ModalBackdrop", () => {
164
234
  // Arrange
165
235
  // We need the elements in the DOM document, it seems, for this test
166
236
  // to work. Changing to testing-library will likely fix this.
237
+ // Then we can remove the lint suppression.
167
238
  const attachElement = getElementAttachedToDocument("container");
168
239
  const wrapper = mount(
169
240
  <ModalBackdrop onCloseModal={() => {}}>
@@ -179,6 +250,7 @@ describe("ModalBackdrop", () => {
179
250
  .getDOMNode();
180
251
 
181
252
  // Assert
253
+ // eslint-disable-next-line testing-library/no-node-access
182
254
  expect(document.activeElement).toBe(focusableElement);
183
255
  });
184
256
 
@@ -186,6 +258,7 @@ describe("ModalBackdrop", () => {
186
258
  // Arrange
187
259
  // We need the elements in the DOM document, it seems, for this test
188
260
  // to work. Changing to testing-library will likely fix this.
261
+ // Then we can remove the lint suppression.
189
262
  const attachElement = getElementAttachedToDocument("container");
190
263
  const wrapper = mount(
191
264
  <ModalBackdrop onCloseModal={() => {}}>
@@ -201,6 +274,7 @@ describe("ModalBackdrop", () => {
201
274
  .getDOMNode();
202
275
 
203
276
  // Assert
277
+ // eslint-disable-next-line testing-library/no-node-access
204
278
  expect(document.activeElement).toBe(focusableElement);
205
279
  });
206
280
  });
@@ -1,6 +1,7 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import {mount, shallow} from "enzyme";
4
+ import "jest-enzyme";
4
5
 
5
6
  import {
6
7
  Breadcrumbs,
@@ -1,6 +1,7 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import {mount, shallow} from "enzyme";
4
+ import "jest-enzyme";
4
5
 
5
6
  import ModalLauncher from "../modal-launcher.js";
6
7
  import OnePaneDialog from "../one-pane-dialog.js";
@@ -1,6 +1,7 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import {mount} from "enzyme";
4
+ import "jest-enzyme";
4
5
 
5
6
  import {View} from "@khanacademy/wonder-blocks-core";
6
7
 
@@ -1,6 +1,7 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import {mount} from "enzyme";
4
+ import "jest-enzyme";
4
5
 
5
6
  import OnePaneDialog from "../one-pane-dialog.js";
6
7
 
@@ -57,6 +57,8 @@ export default class ModalBackdrop extends React.Component<Props> {
57
57
  }, 0);
58
58
  }
59
59
 
60
+ _mousePressedOutside: boolean = false;
61
+
60
62
  /**
61
63
  * Returns an element specified by the user
62
64
  */
@@ -107,12 +109,18 @@ export default class ModalBackdrop extends React.Component<Props> {
107
109
  * _directly_ from the positioner, not bubbled up from its children), close
108
110
  * the modal.
109
111
  */
110
- handleClick: (e: SyntheticEvent<>) => void = (e: SyntheticEvent<>) => {
111
- // Was the lowest-level click target (`e.target`) the positioner element
112
- // (`e.currentTarget`)?
113
- if (e.target === e.currentTarget) {
112
+ handleMouseDown: (e: SyntheticEvent<>) => void = (e: SyntheticEvent<>) => {
113
+ // Confirm that it is the backdrop that is being clicked, not the child
114
+ this._mousePressedOutside = e.target === e.currentTarget;
115
+ };
116
+
117
+ handleMouseUp: (e: SyntheticEvent<>) => void = (e: SyntheticEvent<>) => {
118
+ // Confirm that it is the backdrop that is being clicked, not the child
119
+ // and that the mouse was pressed in the backdrop first.
120
+ if (e.target === e.currentTarget && this._mousePressedOutside) {
114
121
  this.props.onCloseModal();
115
122
  }
123
+ this._mousePressedOutside = false;
116
124
  };
117
125
 
118
126
  render(): React.Node {
@@ -124,7 +132,8 @@ export default class ModalBackdrop extends React.Component<Props> {
124
132
  return (
125
133
  <View
126
134
  style={styles.modalPositioner}
127
- onClick={this.handleClick}
135
+ onMouseDown={this.handleMouseDown}
136
+ onMouseUp={this.handleMouseUp}
128
137
  testId={testId}
129
138
  {...backdropProps}
130
139
  >
@@ -50,7 +50,7 @@ export default {
50
50
  },
51
51
  };
52
52
 
53
- export const simple: StoryComponentType = () => {
53
+ export const Simple: StoryComponentType = () => {
54
54
  const modal = (
55
55
  <OnePaneDialog
56
56
  testId="one-pane-dialog-above"
@@ -98,7 +98,7 @@ const styles = StyleSheet.create({
98
98
  },
99
99
  });
100
100
 
101
- export const kitchenSink: StoryComponentType = () => {
101
+ export const KitchenSink: StoryComponentType = () => {
102
102
  const modal = (
103
103
  <OnePaneDialog
104
104
  title="Single-line title"
@@ -142,7 +142,7 @@ export const kitchenSink: StoryComponentType = () => {
142
142
  );
143
143
  };
144
144
 
145
- export const withOpener: StoryComponentType = () => {
145
+ export const WithOpener: StoryComponentType = () => {
146
146
  type MyModalProps = {|
147
147
  closeModal: () => void,
148
148
  |};
@@ -191,7 +191,7 @@ export const withOpener: StoryComponentType = () => {
191
191
  );
192
192
  };
193
193
 
194
- withOpener.parameters = {
194
+ WithOpener.parameters = {
195
195
  viewport: {
196
196
  defaultViewport: null,
197
197
  },
@@ -2,6 +2,7 @@
2
2
  import * as React from "react";
3
3
  import * as ReactDOM from "react-dom";
4
4
  import {mount} from "enzyme";
5
+ import "jest-enzyme";
5
6
 
6
7
  import {ModalLauncherPortalAttributeName} from "./constants.js";
7
8
  import maybeGetPortalMountedModalHostElement from "./maybe-get-portal-mounted-modal-host-element.js";
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2018 Khan Academy
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.