Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Support stacking dialogs to prevent unmounting #2724

Merged
merged 1 commit into from
Mar 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion res/css/_common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,17 @@ textarea {
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 {
position: fixed;
z-index: 4000;
Expand All @@ -252,7 +263,7 @@ textarea {
.mx_Dialog {
background-color: $primary-bg-color;
color: $light-fg-color;
z-index: 4010;
z-index: 4012;
font-weight: 300;
font-size: 15px;
position: relative;
Expand All @@ -264,6 +275,10 @@ textarea {
overflow-y: auto;
}

.mx_Dialog_staticWrapper .mx_Dialog {
z-index: 4010;
}

.mx_Dialog_background {
position: fixed;
top: 0;
Expand All @@ -272,6 +287,17 @@ textarea {
height: 100%;
background-color: $dialog-backdrop-color;
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 {
Expand Down
96 changes: 82 additions & 14 deletions src/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import dis from './dispatcher';
import { _t } from './languageHandler';

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
Expand Down Expand Up @@ -106,7 +107,12 @@ class ModalManager {
// this modal. Remove all other modals from the stack when this modal
// is closed.
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]
// Neither the static nor priority modal will be in this list.
this._modals = [
/* {
elem: React component for this dialog
Expand All @@ -130,6 +136,18 @@ class ModalManager {
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) {
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
return this.createDialog(...rest);
Expand Down Expand Up @@ -166,8 +184,13 @@ class ModalManager {
* of other modals that are currently in the stack.
* Also, when closed, all modals will be removed
* 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 modal = {};

Expand All @@ -188,6 +211,13 @@ class ModalManager {
self._modals = [];
}

if (self._staticModal === modal) {
self._staticModal = null;

// XXX: This is destructive
self._modals = [];
}

self._reRender();
};

Expand All @@ -207,6 +237,9 @@ class ModalManager {
if (isPriorityModal) {
// XXX: This is destructive
this._priorityModal = modal;
} else if (isStaticModal) {
// This is intentionally destructive
this._staticModal = modal;
} else {
this._modals.unshift(modal);
}
Expand All @@ -216,12 +249,18 @@ class ModalManager {
}

closeAll() {
const modals = this._modals;
const modalsToClose = [...this._modals, this._priorityModal];
this._modals = [];
this._priorityModal = null;

if (this._staticModal && modalsToClose.length === 0) {
modalsToClose.push(this._staticModal);
this._staticModal = null;
}

for (let i = 0; i < modals.length; i++) {
const m = modals[i];
if (m.onFinished) {
for (let i = 0; i < modalsToClose.length; i++) {
const m = modalsToClose[i];
if (m && m.onFinished) {
m.onFinished(false);
}
}
Expand All @@ -230,13 +269,14 @@ class ModalManager {
}

_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
// to screen reader users again
dis.dispatch({
action: 'aria_unhide_main_app',
});
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer());
ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer());
return;
}

Expand All @@ -247,17 +287,45 @@ class ModalManager {
action: 'aria_hide_main_app',
});

if (this._staticModal) {
const classes = "mx_Dialog_wrapper mx_Dialog_staticWrapper "
+ (this._staticModal.className ? this._staticModal.className : '');

const staticDialog = (
<div className={classes}>
<div className="mx_Dialog">
{ 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];
const dialog = (
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
<div className="mx_Dialog">
{ modal.elem }
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 className="mx_Dialog_background" onClick={this.closeAll}></div>
</div>
<div className="mx_Dialog_background" onClick={this.closeAll}></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());
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/components/structures/MatrixChat.js
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,8 @@ export default React.createClass({
break;
case 'view_user_settings': {
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
this._viewSomethingBehindModal();
Expand Down
2 changes: 1 addition & 1 deletion src/stores/RoomViewStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class RoomViewStore extends Store {
const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog");
Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {
roomId: payload.room_id || this._state.roomId,
}, 'mx_SettingsDialog');
}, 'mx_SettingsDialog', /*isPriority=*/false, /*isStatic=*/true);
break;
}
}
Expand Down