Show a modal from a lifecycle method (`oncreate`, `view`, etc.)
* // This "hack" is needed due to quirks with nested redraws in Mithril.
* setTimeout(() => app.modal.show(MyCoolModal, { attr: 'value' }), 0);
+ *
+ * @example
Stacking modals
+ * app.modal.show(MyCoolStackedModal, { attr: 'value' }, true);
*/
- show(componentClass: UnsafeModalClass, attrs: Record = {}): void {
+ show(componentClass: UnsafeModalClass, attrs: Record = {}, stackModal: boolean = false): void {
if (!(componentClass.prototype instanceof Modal)) {
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
const invalidModalWarning = 'The ModalManager can only show Modals.';
@@ -53,28 +69,52 @@ export default class ModalManagerState {
throw new Error(invalidModalWarning);
}
- if (this.closeTimeout) clearTimeout(this.closeTimeout);
-
- this.modal = { componentClass, attrs, key: this.key++ };
-
+ this.backdropShown = true;
m.redraw.sync();
+
+ // We use requestAnimationFrame here, since we need to wait for the backdrop to be added
+ // to the DOM before actually adding the modal to the modal list.
+ //
+ // This is because we use RAF inside the ModalManager onupdate lifecycle hook, and if we
+ // skip this RAF call, the hook will attempt to add a focus trap as well as lock scroll
+ // onto the newly added modal before it's in the DOM, creating an extra scrollbar.
+ requestAnimationFrame(() => {
+ // Set current modal
+ this.modal = { componentClass, attrs, key: this.key++ };
+
+ // We want to stack this modal
+ if (stackModal) {
+ // Remember previously opened modal and add new modal to the modal list
+ this.modalList.push(this.modal);
+ } else {
+ // Override last modals
+ this.modalList = [this.modal];
+ }
+
+ m.redraw();
+ });
}
/**
- * Closes the currently open dialog, if one is open.
+ * Closes the topmost currently open dialog, if one is open.
*/
close(): void {
if (!this.modal) return;
- // Don't hide the modal immediately, because if the consumer happens to call
- // the `show` method straight after to show another modal dialog, it will
- // cause Bootstrap's modal JS to misbehave. Instead we will wait for a tiny
- // bit to give the `show` method the opportunity to prevent this from going
- // ahead.
- this.closeTimeout = setTimeout(() => {
+ // If there are two modals, remove the most recent one
+ if (this.modalList.length > 1) {
+ // Remove last modal from list
+ this.modalList.pop();
+
+ // Open last modal from list
+ this.modal = this.modalList[this.modalList.length - 1];
+ } else {
+ // Reset state
this.modal = null;
- m.redraw();
- });
+ this.modalList = [];
+ }
+
+ m.redraw();
}
/**
diff --git a/framework/core/less/common/Modal.less b/framework/core/less/common/Modal.less
index 892057850..59a6acf3b 100644
--- a/framework/core/less/common/Modal.less
+++ b/framework/core/less/common/Modal.less
@@ -1,60 +1,58 @@
// ------------------------------------
// Modals
-// Kill the scroll on the body
-.modal-open {
- overflow: hidden;
-}
+.Modal {
+ padding: 0;
+ border-radius: @border-radius;
-// Modal background
-.modal-backdrop {
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: var(--zindex-modal-background);
- background-color: var(--overlay-bg);
- opacity: 0;
- transition: opacity 0.2s;
+ transform: scale(0.9);
+ transition: transform 0.2s ease-out, opacity 0.2s ease-out, top 0.2s ease-out;
+ z-index: 2;
+ position: relative;
+
+ width: auto;
+ margin: 10px;
+ max-width: 600px;
&.in {
+ transform: scale(1);
+ }
+}
+
+.Modal-backdrop {
+ background: var(--overlay-bg);
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ opacity: 0;
+ transition: opacity 0.2s ease-out;
+ z-index: ~"calc(var(--zindex-modal) + var(--modal-count) - 2)";
+
+ &[data-showing] {
opacity: 1;
}
}
-// Container that the modal scrolls within
.ModalManager {
- display: none;
- overflow: hidden;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
- z-index: var(--zindex-modal);
+ z-index: ~"calc(var(--zindex-modal) + var(--modal-number))";
-webkit-overflow-scrolling: touch;
-
- // When fading in the modal, animate it to slide down
- .Modal {
- transform: scale(0.9);
- transition: transform 0.2s ease-out;
- }
- &.in .Modal {
- transform: scale(1);
- }
-}
-.modal-open .ModalManager {
overflow-x: hidden;
overflow-y: auto;
-}
-// Shell div to position the modal with bottom padding
-.Modal {
- position: relative;
- width: auto;
- margin: 10px;
- max-width: 600px;
+ &-invisibleBackdrop {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ }
}
// Actual modal
@@ -129,9 +127,6 @@
}
@media @phone {
- .ModalManager.fade {
- opacity: 1;
- }
.ModalManager {
position: fixed;
left: 0;
@@ -139,24 +134,28 @@
bottom: 0;
top: 0;
overflow: auto;
- transition: transform 0.2s ease-out;
- transform: translate(0, 100vh);
- &.in {
- -webkit-transform: none !important;
- transform: none !important;
- }
&:before {
content: " ";
.header-background();
position: absolute;
+ z-index: 2;
}
}
.Modal {
max-width: 100%;
margin: 0;
-webkit-transform: none !important;
- transform: none !important;
+ transform: none !important;
+ top: 100vh;
+
+ &.fade {
+ opacity: 1;
+ }
+
+ &.in {
+ top: 0;
+ }
}
.Modal-content {
border-radius: 0;
@@ -174,19 +173,20 @@
@media @tablet-up {
.Modal {
+ border-radius: var(--border-radius);
+ box-shadow: 0 7px 15px var(--shadow-color);
+ width: 100%;
+ max-width: 600px;
margin: 120px auto;
}
.Modal-close {
position: absolute;
right: 10px;
top: 10px;
- z-index: 1;
+ z-index: 2;
}
.Modal-content {
-
- border: 0;
border-radius: var(--border-radius);
- box-shadow: 0 7px 15px var(--shadow-color);
}
.Modal--small {
max-width: 375px;
diff --git a/yarn.lock b/yarn.lock
index cb1c63b3c..5c71bbae8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1112,6 +1112,11 @@
prop-types "^15.7.2"
react-is "^16.6.3"
+"@types/body-scroll-lock@^3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@types/body-scroll-lock/-/body-scroll-lock-3.1.0.tgz#435f6abf682bf58640e1c2ee5978320b891970e7"
+ integrity sha512-3owAC4iJub5WPqRhxd8INarF2bWeQq1yQHBgYhN0XLBJMpd5ED10RrJ3aKiAwlTyL5wK7RkBD4SZUQz2AAAMdA==
+
"@types/eslint-scope@^3.7.3":
version "3.7.3"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224"
@@ -1488,6 +1493,11 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+body-scroll-lock@^4.0.0-beta.0:
+ version "4.0.0-beta.0"
+ resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz#4f78789d10e6388115c0460cd6d7d4dd2bbc4f7e"
+ integrity sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==
+
bootstrap@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.4.1.tgz#c3a347d419e289ad11f4033e3c4132b87c081d72"