DEV: Convert d-modal and d-modal-body to glimmer components

This commit is contained in:
David Taylor 2023-05-12 18:42:43 +01:00
parent 11e7e949b7
commit 771c4de7f1
4 changed files with 199 additions and 158 deletions

View File

@ -0,0 +1,10 @@
<div
id={{@id}}
class={{concat-class "modal-body" @class}}
tabindex="-1"
{{did-insert this.didInsert}}
{{will-destroy this.willDestroy}}
...attributes
>
{{yield}}
</div>

View File

@ -1,54 +1,63 @@
import { attributeBindings, classNames } from "@ember-decorators/component"; import Component from "@glimmer/component";
import Component from "@ember/component";
import { scheduleOnce } from "@ember/runloop"; import { scheduleOnce } from "@ember/runloop";
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
@classNames("modal-body") function pick(object, keys) {
@attributeBindings("tabindex") const result = {};
for (const key of keys) {
if (key in object) {
result[key] = object[key];
}
}
return result;
}
@disableImplicitInjections
export default class DModalBody extends Component { export default class DModalBody extends Component {
fixed = false; @service appEvents;
submitOnEnter = true;
dismissable = true;
tabindex = -1;
didInsertElement() { @tracked fixed = false;
super.didInsertElement(...arguments);
@action
didInsert(element) {
this._modalAlertElement = document.getElementById("modal-alert"); this._modalAlertElement = document.getElementById("modal-alert");
if (this._modalAlertElement) { if (this._modalAlertElement) {
this._clearFlash(); this._clearFlash();
} }
let fixedParent = this.element.closest(".d-modal.fixed-modal"); const fixedParent = element.closest(".d-modal.fixed-modal");
if (fixedParent) { if (fixedParent) {
this.set("fixed", true); this.fixed = true;
$(fixedParent).modal("show"); $(fixedParent).modal("show");
} }
scheduleOnce("afterRender", this, this._afterFirstRender); scheduleOnce("afterRender", () => this._afterFirstRender(element));
this.appEvents.on("modal-body:flash", this, "_flash");
this.appEvents.on("modal-body:clearFlash", this, "_clearFlash");
} }
willDestroyElement() { @action
super.willDestroyElement(...arguments); willDestroy() {
this.appEvents.off("modal-body:flash", this, "_flash"); this.appEvents.off("modal-body:flash", this, "_flash");
this.appEvents.off("modal-body:clearFlash", this, "_clearFlash"); this.appEvents.off("modal-body:clearFlash", this, "_clearFlash");
this.appEvents.trigger("modal:body-dismissed"); this.appEvents.trigger("modal:body-dismissed");
} }
_afterFirstRender() { _afterFirstRender(element) {
const maxHeight = this.maxHeight; const maxHeight = this.args.maxHeight;
if (maxHeight) { if (maxHeight) {
const maxHeightFloat = parseFloat(maxHeight) / 100.0; const maxHeightFloat = parseFloat(maxHeight) / 100.0;
if (maxHeightFloat > 0) { if (maxHeightFloat > 0) {
const viewPortHeight = $(window).height(); const viewPortHeight = $(window).height();
this.element.style.maxHeight = element.style.maxHeight =
Math.floor(maxHeightFloat * viewPortHeight) + "px"; Math.floor(maxHeightFloat * viewPortHeight) + "px";
} }
} }
this.appEvents.trigger( this.appEvents.trigger(
"modal:body-shown", "modal:body-shown",
this.getProperties( pick(this.args, [
"title", "title",
"rawTitle", "rawTitle",
"fixed", "fixed",
@ -56,8 +65,8 @@ export default class DModalBody extends Component {
"rawSubtitle", "rawSubtitle",
"submitOnEnter", "submitOnEnter",
"dismissable", "dismissable",
"headerClass" "headerClass",
) ])
); );
} }

View File

@ -1,56 +1,78 @@
<div class="modal-outer-container"> {{! template-lint-disable no-down-event-binding }}
<div class="modal-middle-container"> {{! template-lint-disable no-invalid-interactive }}
<div class="modal-inner-container">
<PluginOutlet @name="above-modal-header" @connectorTagName="div" />
<div class="modal-header {{this.headerClass}}">
{{#if this.dismissable}}
<DButton
@icon="times"
@action={{route-action "closeModal" "initiatedByCloseButton"}}
@class="btn-flat modal-close close"
@title="modal.close"
/>
{{/if}}
{{#if this.title}} <div
<div class="title"> class={{concat-class
<h3 id="discourse-modal-title">{{this.title}}</h3> "modal"
"d-modal"
this.modalClass
this.modalStyle
(if this.hasPanels "has-panels")
}}
id={{if (not-eq this.modalStyle "inline-modal") "discourse-modal"}}
data-keyboard="false"
aria-modal="true"
role="dialog"
aria-labelledby={{this.ariaLabelledby}}
...attributes
{{did-insert this.setupListeners}}
{{will-destroy this.cleanupListeners}}
{{on "mousedown" this.handleMouseDown}}
>
<div class="modal-outer-container">
<div class="modal-middle-container">
<div class="modal-inner-container">
<PluginOutlet @name="above-modal-header" @connectorTagName="div" />
<div class="modal-header {{this.headerClass}}">
{{#if this.dismissable}}
<DButton
@icon="times"
@action={{route-action "closeModal" "initiatedByCloseButton"}}
@class="btn-flat modal-close close"
@title="modal.close"
/>
{{/if}}
{{#if this.subtitle}} {{#if this.title}}
<p class="subtitle">{{this.subtitle}}</p> <div class="title">
{{/if}} <h3 id="discourse-modal-title">{{this.title}}</h3>
</div>
{{/if}}
{{#if this.panels}} {{#if this.subtitle}}
<ul class="modal-tabs"> <p class="subtitle">{{this.subtitle}}</p>
{{#each this.panels as |panel|}} {{/if}}
<ModalTab </div>
@panel={{panel}} {{/if}}
@panelsLength={{this.panels.length}}
@selectedPanel={{this.selectedPanel}}
@onSelectPanel={{this.onSelectPanel}}
/>
{{/each}}
</ul>
{{/if}}
</div>
<div id="modal-alert" role="alert"></div> {{#if this.panels}}
<ul class="modal-tabs">
{{yield}} {{#each this.panels as |panel|}}
<ModalTab
{{#each this.errors as |error|}} @panel={{panel}}
<div class="alert alert-error"> @panelsLength={{this.panels.length}}
<button @selectedPanel={{@selectedPanel}}
type="button" @onSelectPanel={{@onSelectPanel}}
class="close" />
data-dismiss="alert" {{/each}}
aria-label={{i18n "modal.dismiss_error"}} </ul>
>×</button> {{/if}}
{{error}}
</div> </div>
{{/each}}
<div id="modal-alert" role="alert"></div>
{{yield}}
{{#each this.errors as |error|}}
<div class="alert alert-error">
<button
type="button"
class="close"
data-dismiss="alert"
aria-label={{i18n "modal.dismiss_error"}}
>×</button>
{{error}}
</div>
{{/each}}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,80 +1,102 @@
import { import Component from "@glimmer/component";
attributeBindings,
classNameBindings,
} from "@ember-decorators/component";
import Component from "@ember/component";
import I18n from "I18n"; import I18n from "I18n";
import { next, schedule } from "@ember/runloop"; import { next, schedule } from "@ember/runloop";
import discourseComputed, { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
@classNameBindings( @disableImplicitInjections
":modal",
":d-modal",
"modalClass",
"modalStyle",
"hasPanels"
)
@attributeBindings(
"dataKeyboard:data-keyboard",
"ariaModal:aria-modal",
"role",
"ariaLabelledby:aria-labelledby"
)
export default class DModal extends Component { export default class DModal extends Component {
submitOnEnter = true; @service appEvents;
dismissable = true;
title = null;
titleAriaElementId = null;
subtitle = null;
role = "dialog";
headerClass = null;
// // We handle ESC ourselves @tracked wrapperElement;
dataKeyboard = "false"; @tracked modalBodyData = {};
// // Inform screen readers of the modal
ariaModal = "true";
init() { get modalStyle() {
super.init(...arguments); if (this.args.modalStyle === "inline-modal") {
return "inline-modal";
// If we need to render a second modal for any reason, we can't } else {
// use `elementId` return "fixed-modal";
if (this.modalStyle !== "inline-modal") {
this.set("elementId", "discourse-modal");
this.set("modalStyle", "fixed-modal");
} }
} }
didInsertElement() { get submitOnEnter() {
super.didInsertElement(...arguments); if ("submitOnEnter" in this.modalBodyData) {
return this.modalBodyData.submitOnEnter;
} else {
return true;
}
}
this.appEvents.on("modal:body-shown", this, "_modalBodyShown"); get dismissable() {
if ("dismissable" in this.modalBodyData) {
return this.modalBodyData.dismissable;
} else {
return true;
}
}
get title() {
if (this.modalBodyData.title) {
return I18n.t(this.modalBodyData.title);
} else if (this.modalBodyData.rawTitle) {
return this.modalBodyData.rawTitle;
} else {
return this.args.title;
}
}
get subtitle() {
if (this.modalBodyData.subtitle) {
return I18n.t(this.modalBodyData.subtitle);
}
return this.modalBodyData.rawSubtitle || this.args.subtitle;
}
get headerClass() {
return this.modalBodyData.headerClass;
}
get panels() {
return this.args.panels;
}
get errors() {
return this.args.errors;
}
@action
setupListeners(element) {
this.appEvents.on("modal:body-shown", this._modalBodyShown);
document.documentElement.addEventListener( document.documentElement.addEventListener(
"keydown", "keydown",
this._handleModalEvents this._handleModalEvents
); );
this.wrapperElement = element;
} }
willDestroyElement() { @action
super.willDestroyElement(...arguments); cleanupListeners() {
this.appEvents.off("modal:body-shown", this._modalBodyShown);
this.appEvents.off("modal:body-shown", this, "_modalBodyShown");
document.documentElement.removeEventListener( document.documentElement.removeEventListener(
"keydown", "keydown",
this._handleModalEvents this._handleModalEvents
); );
} }
@discourseComputed("title", "titleAriaElementId") get ariaLabelledby() {
ariaLabelledby(title, titleAriaElementId) { if (this.args.titleAriaElementId) {
if (titleAriaElementId) { return this.args.titleAriaElementId;
return titleAriaElementId; } else if (this.args.title) {
}
if (title) {
return "discourse-modal-title"; return "discourse-modal-title";
} }
}
return; get modalClass() {
return this.modalBodyData.modalClass || this.args.modalClass;
} }
triggerClickOnEnter(e) { triggerClickOnEnter(e) {
@ -93,7 +115,8 @@ export default class DModal extends Component {
return true; return true;
} }
mouseDown(e) { @action
handleMouseDown(e) {
if (!this.dismissable) { if (!this.dismissable) {
return; return;
} }
@ -105,46 +128,21 @@ export default class DModal extends Component {
// Send modal close (which bubbles to ApplicationRoute) if clicked outside. // Send modal close (which bubbles to ApplicationRoute) if clicked outside.
// We do this because some CSS of ours seems to cover the backdrop and makes // We do this because some CSS of ours seems to cover the backdrop and makes
// it unclickable. // it unclickable.
return this.attrs.closeModal?.("initiatedByClickOut"); return this.args.closeModal?.("initiatedByClickOut");
} }
} }
@bind
_modalBodyShown(data) { _modalBodyShown(data) {
if (this.isDestroying || this.isDestroyed) { if (this.isDestroying || this.isDestroyed) {
return; return;
} }
if (data.fixed) { if (data.fixed) {
this.element.classList.remove("hidden"); this.wrapperElement.classList.remove("hidden");
} }
if (data.title) { this.modalBodyData = data;
this.set("title", I18n.t(data.title));
} else if (data.rawTitle) {
this.set("title", data.rawTitle);
}
if (data.subtitle) {
this.set("subtitle", I18n.t(data.subtitle));
} else if (data.rawSubtitle) {
this.set("subtitle", data.rawSubtitle);
} else {
// if no subtitle provided, makes sure the previous subtitle
// of another modal is not used
this.set("subtitle", null);
}
if ("submitOnEnter" in data) {
this.set("submitOnEnter", data.submitOnEnter);
}
if ("dismissable" in data) {
this.set("dismissable", data.dismissable);
} else {
this.set("dismissable", true);
}
this.set("headerClass", data.headerClass || null);
schedule("afterRender", () => { schedule("afterRender", () => {
this._trapTab(); this._trapTab();
@ -153,16 +151,16 @@ export default class DModal extends Component {
@bind @bind
_handleModalEvents(event) { _handleModalEvents(event) {
if (this.element.classList.contains("hidden")) { if (this.wrapperElement.classList.contains("hidden")) {
return; return;
} }
if (event.key === "Escape" && this.dismissable) { if (event.key === "Escape" && this.dismissable) {
next(() => this.attrs.closeModal("initiatedByESC")); next(() => this.args.closeModal("initiatedByESC"));
} }
if (event.key === "Enter" && this.triggerClickOnEnter(event)) { if (event.key === "Enter" && this.triggerClickOnEnter(event)) {
this.element.querySelector(".modal-footer .btn-primary")?.click(); this.wrapperElement.querySelector(".modal-footer .btn-primary")?.click();
event.preventDefault(); event.preventDefault();
} }
@ -172,11 +170,13 @@ export default class DModal extends Component {
} }
_trapTab(event) { _trapTab(event) {
if (this.element.classList.contains("hidden")) { if (this.wrapperElement.classList.contains("hidden")) {
return true; return true;
} }
const innerContainer = this.element.querySelector(".modal-inner-container"); const innerContainer = this.wrapperElement.querySelector(
".modal-inner-container"
);
if (!innerContainer) { if (!innerContainer) {
return; return;
} }