Support stacking dialogs to prevent unmounting
Fixes https://github.com/vector-im/riot-web/issues/8371
This commit is contained in:
parent
8d7837829e
commit
0978ab3da0
4 changed files with 114 additions and 19 deletions
|
@ -228,6 +228,17 @@ textarea {
|
||||||
color: $roomsublist-label-bg-color;
|
color: $roomsublist-label-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Expected z-indexes for dialogs:
|
||||||
|
4000 - Default wrapper index
|
||||||
|
4009 - Static dialog background
|
||||||
|
4010 - Static dialog itself
|
||||||
|
4011 - Standard dialog background
|
||||||
|
4012 - Standard dialog itself
|
||||||
|
|
||||||
|
These are set up such that the static dialog always appears
|
||||||
|
underneath the standard dialogs.
|
||||||
|
*/
|
||||||
|
|
||||||
.mx_Dialog_wrapper {
|
.mx_Dialog_wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 4000;
|
z-index: 4000;
|
||||||
|
@ -252,7 +263,7 @@ textarea {
|
||||||
.mx_Dialog {
|
.mx_Dialog {
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
color: $light-fg-color;
|
color: $light-fg-color;
|
||||||
z-index: 4010;
|
z-index: 4012;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -264,6 +275,10 @@ textarea {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_Dialog_staticWrapper .mx_Dialog {
|
||||||
|
z-index: 4010;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_Dialog_background {
|
.mx_Dialog_background {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -272,6 +287,17 @@ textarea {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: $dialog-backdrop-color;
|
background-color: $dialog-backdrop-color;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
z-index: 4011;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Dialog_background.mx_Dialog_staticBackground {
|
||||||
|
z-index: 4009;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Dialog_wrapperWithStaticUnder .mx_Dialog_background {
|
||||||
|
// Roughly half of what it would normally be - we don't want to black out
|
||||||
|
// the app, just make it clear that the dialogs are stacked.
|
||||||
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog_lightbox .mx_Dialog_background {
|
.mx_Dialog_lightbox .mx_Dialog_background {
|
||||||
|
|
88
src/Modal.js
88
src/Modal.js
|
@ -26,6 +26,7 @@ import dis from './dispatcher';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||||
|
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrap an asynchronous loader function with a react component which shows a
|
* Wrap an asynchronous loader function with a react component which shows a
|
||||||
|
@ -106,7 +107,12 @@ class ModalManager {
|
||||||
// this modal. Remove all other modals from the stack when this modal
|
// this modal. Remove all other modals from the stack when this modal
|
||||||
// is closed.
|
// is closed.
|
||||||
this._priorityModal = null;
|
this._priorityModal = null;
|
||||||
|
// The modal to keep open underneath other modals if possible. Useful
|
||||||
|
// for cases like Settings where the modal should remain open while the
|
||||||
|
// user is prompted for more information/errors.
|
||||||
|
this._staticModal = null;
|
||||||
// A list of the modals we have stacked up, with the most recent at [0]
|
// A list of the modals we have stacked up, with the most recent at [0]
|
||||||
|
// Neither the static nor priority modal will be in this list.
|
||||||
this._modals = [
|
this._modals = [
|
||||||
/* {
|
/* {
|
||||||
elem: React component for this dialog
|
elem: React component for this dialog
|
||||||
|
@ -130,6 +136,18 @@ class ModalManager {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOrCreateStaticContainer() {
|
||||||
|
let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID);
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement("div");
|
||||||
|
container.id = STATIC_DIALOG_CONTAINER_ID;
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
createTrackedDialog(analyticsAction, analyticsInfo, ...rest) {
|
createTrackedDialog(analyticsAction, analyticsInfo, ...rest) {
|
||||||
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
||||||
return this.createDialog(...rest);
|
return this.createDialog(...rest);
|
||||||
|
@ -166,8 +184,13 @@ class ModalManager {
|
||||||
* of other modals that are currently in the stack.
|
* of other modals that are currently in the stack.
|
||||||
* Also, when closed, all modals will be removed
|
* Also, when closed, all modals will be removed
|
||||||
* from the stack.
|
* from the stack.
|
||||||
|
* @param {boolean} isStaticModal if true, this modal will be displayed under other
|
||||||
|
* modals in the stack. When closed, all modals will
|
||||||
|
* also be removed from the stack. This is not compatible
|
||||||
|
* with being a priority modal. Only one modal can be
|
||||||
|
* static at a time.
|
||||||
*/
|
*/
|
||||||
createDialogAsync(prom, props, className, isPriorityModal) {
|
createDialogAsync(prom, props, className, isPriorityModal, isStaticModal) {
|
||||||
const self = this;
|
const self = this;
|
||||||
const modal = {};
|
const modal = {};
|
||||||
|
|
||||||
|
@ -188,6 +211,13 @@ class ModalManager {
|
||||||
self._modals = [];
|
self._modals = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self._staticModal === modal) {
|
||||||
|
self._staticModal = null;
|
||||||
|
|
||||||
|
// XXX: This is destructive
|
||||||
|
self._modals = [];
|
||||||
|
}
|
||||||
|
|
||||||
self._reRender();
|
self._reRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -207,6 +237,9 @@ class ModalManager {
|
||||||
if (isPriorityModal) {
|
if (isPriorityModal) {
|
||||||
// XXX: This is destructive
|
// XXX: This is destructive
|
||||||
this._priorityModal = modal;
|
this._priorityModal = modal;
|
||||||
|
} else if (isStaticModal) {
|
||||||
|
// This is intentionally destructive
|
||||||
|
this._staticModal = modal;
|
||||||
} else {
|
} else {
|
||||||
this._modals.unshift(modal);
|
this._modals.unshift(modal);
|
||||||
}
|
}
|
||||||
|
@ -216,12 +249,18 @@ class ModalManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAll() {
|
closeAll() {
|
||||||
const modals = this._modals;
|
const modalsToClose = [...this._modals, this._priorityModal];
|
||||||
this._modals = [];
|
this._modals = [];
|
||||||
|
this._priorityModal = null;
|
||||||
|
|
||||||
for (let i = 0; i < modals.length; i++) {
|
if (this._staticModal && modalsToClose.length === 0) {
|
||||||
const m = modals[i];
|
modalsToClose.push(this._staticModal);
|
||||||
if (m.onFinished) {
|
this._staticModal = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < modalsToClose.length; i++) {
|
||||||
|
const m = modalsToClose[i];
|
||||||
|
if (m && m.onFinished) {
|
||||||
m.onFinished(false);
|
m.onFinished(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,13 +269,14 @@ class ModalManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
_reRender() {
|
_reRender() {
|
||||||
if (this._modals.length == 0 && !this._priorityModal) {
|
if (this._modals.length === 0 && !this._priorityModal && !this._staticModal) {
|
||||||
// If there is no modal to render, make all of Riot available
|
// If there is no modal to render, make all of Riot available
|
||||||
// to screen reader users again
|
// to screen reader users again
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'aria_unhide_main_app',
|
action: 'aria_unhide_main_app',
|
||||||
});
|
});
|
||||||
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer());
|
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer());
|
||||||
|
ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,17 +287,45 @@ class ModalManager {
|
||||||
action: 'aria_hide_main_app',
|
action: 'aria_hide_main_app',
|
||||||
});
|
});
|
||||||
|
|
||||||
const modal = this._priorityModal ? this._priorityModal : this._modals[0];
|
if (this._staticModal) {
|
||||||
const dialog = (
|
const classes = "mx_Dialog_wrapper mx_Dialog_staticWrapper "
|
||||||
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
|
+ (this._staticModal.className ? this._staticModal.className : '');
|
||||||
|
|
||||||
|
const staticDialog = (
|
||||||
|
<div className={classes}>
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
{ modal.elem }
|
{ this._staticModal.elem }
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.closeAll}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
ReactDOM.render(staticDialog, this.getOrCreateStaticContainer());
|
||||||
|
} else {
|
||||||
|
// This is safe to call repeatedly if we happen to do that
|
||||||
|
ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer());
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = this._priorityModal ? this._priorityModal : this._modals[0];
|
||||||
|
if (modal) {
|
||||||
|
const classes = "mx_Dialog_wrapper "
|
||||||
|
+ (this._staticModal ? "mx_Dialog_wrapperWithStaticUnder " : '')
|
||||||
|
+ (modal.className ? modal.className : '');
|
||||||
|
|
||||||
|
const dialog = (
|
||||||
|
<div className={classes}>
|
||||||
|
<div className="mx_Dialog">
|
||||||
|
{modal.elem}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background" onClick={this.closeAll}></div>
|
<div className="mx_Dialog_background" onClick={this.closeAll}></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
ReactDOM.render(dialog, this.getOrCreateContainer());
|
ReactDOM.render(dialog, this.getOrCreateContainer());
|
||||||
|
} else {
|
||||||
|
// This is safe to call repeatedly if we happen to do that
|
||||||
|
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -584,7 +584,8 @@ export default React.createClass({
|
||||||
break;
|
break;
|
||||||
case 'view_user_settings': {
|
case 'view_user_settings': {
|
||||||
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
|
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
|
||||||
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog');
|
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog',
|
||||||
|
/*isPriority=*/false, /*isStatic=*/true);
|
||||||
|
|
||||||
// View the welcome or home page if we need something to look at
|
// View the welcome or home page if we need something to look at
|
||||||
this._viewSomethingBehindModal();
|
this._viewSomethingBehindModal();
|
||||||
|
|
|
@ -120,7 +120,7 @@ class RoomViewStore extends Store {
|
||||||
const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog");
|
const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog");
|
||||||
Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {
|
Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {
|
||||||
roomId: payload.room_id || this._state.roomId,
|
roomId: payload.room_id || this._state.roomId,
|
||||||
}, 'mx_SettingsDialog');
|
}, 'mx_SettingsDialog', /*isPriority=*/false, /*isStatic=*/true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue