@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 +10 -0
- package/dist/es/index.js +91 -83
- package/dist/index.js +15 -7
- package/package.json +3 -4
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +11 -11
- package/src/components/__tests__/close-button.test.js +1 -0
- package/src/components/__tests__/modal-backdrop.test.js +80 -6
- package/src/components/__tests__/modal-header.test.js +1 -0
- package/src/components/__tests__/modal-launcher.test.js +1 -0
- package/src/components/__tests__/modal-panel.test.js +1 -0
- package/src/components/__tests__/one-pane-dialog.test.js +1 -0
- package/src/components/modal-backdrop.js +14 -5
- package/src/components/one-pane-dialog.stories.js +4 -4
- package/src/util/maybe-get-portal-mounted-modal-host-element.test.js +1 -0
- package/LICENSE +0 -21
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
|
|
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
|
|
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$
|
|
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$
|
|
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.
|
|
468
|
-
//
|
|
469
|
-
//
|
|
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$
|
|
555
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1290
|
-
//
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
65
|
+
const backdrop = screen.getByTestId("modal-backdrop-test-id");
|
|
66
|
+
|
|
67
|
+
//Act
|
|
68
|
+
userEvent.click(backdrop);
|
|
60
69
|
|
|
61
|
-
|
|
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
|
});
|
|
@@ -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
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|