DEV: Introduce new component-based DModal API (#21304)
Ember 4.x will be removing the 'named outlet' feature, which were previously relying on to render modal 'controllers' and their associated templates. This commit updates the modal.show API to accept a component class, and also introduces a declarative API which can be used by including the <DModal component directly in your template. For more information on the API design, and conversion instructions from the current API, see these Meta topics: DModal API: https://meta.discourse.org/t/268304 Conversion: https://meta.discourse.org/t/268057
This commit is contained in:
parent
45c504d024
commit
b3a23bd9d6
|
@ -0,0 +1,9 @@
|
|||
{{#if @inline}}
|
||||
{{yield}}
|
||||
{{else if @element}}
|
||||
{{#if @append}}
|
||||
{{#in-element @element insertBefore=null}}{{yield}}{{/in-element}}
|
||||
{{else}}
|
||||
{{#in-element @element}}{{yield}}{{/in-element}}
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -1,3 +1,5 @@
|
|||
{{! Remove when legacy modals are dropped (deprecation: discourse.modal-controllers) }}
|
||||
|
||||
<div
|
||||
id={{@id}}
|
||||
class={{concat-class "modal-body" @class}}
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
// Remove when legacy modals are dropped (deprecation: discourse.modal-controllers)
|
||||
|
||||
import Component from "@glimmer/component";
|
||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import { action } from "@ember/object";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { DEBUG } from "@glimmer/env";
|
||||
|
||||
const LEGACY_ERROR =
|
||||
"d-modal-body should only be used inside a legacy controller-based d-modal. https://meta.discourse.org/t/268057";
|
||||
|
||||
function pick(object, keys) {
|
||||
const result = {};
|
||||
|
@ -23,6 +29,14 @@ export default class DModalBody extends Component {
|
|||
|
||||
@action
|
||||
didInsert(element) {
|
||||
if (element.closest(".d-modal:not(.d-modal-legacy")) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(LEGACY_ERROR);
|
||||
if (DEBUG) {
|
||||
throw new Error(LEGACY_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
this.appEvents.trigger("modal-body:clearFlash");
|
||||
|
||||
const fixedParent = element.closest(".d-modal.fixed-modal");
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
{{! Remove when legacy modals are dropped (deprecation: discourse.modal-controllers) }}
|
||||
|
||||
{{! template-lint-disable no-pointer-down-event-binding }}
|
||||
{{! template-lint-disable no-invalid-interactive }}
|
||||
|
||||
<div
|
||||
class={{concat-class
|
||||
this.modalClass
|
||||
this.modalStyle
|
||||
(if this.hasPanels "has-panels")
|
||||
(if @hidden "hidden")
|
||||
"d-modal-legacy"
|
||||
}}
|
||||
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}}
|
||||
|
||||
<div class="modal-title-wrapper">
|
||||
{{#if this.title}}
|
||||
<div class="title">
|
||||
<h3 id="discourse-modal-title">{{this.title}}</h3>
|
||||
|
||||
{{#if this.subtitle}}
|
||||
<p class="subtitle">{{this.subtitle}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<span id="modal-header-after-title"></span>
|
||||
</div>
|
||||
|
||||
{{#if this.panels}}
|
||||
<ul class="modal-tabs">
|
||||
{{#each this.panels as |panel|}}
|
||||
<ModalTab
|
||||
@panel={{panel}}
|
||||
@panelsLength={{this.panels.length}}
|
||||
@selectedPanel={{@selectedPanel}}
|
||||
@onSelectPanel={{@onSelectPanel}}
|
||||
/>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="modal-alert"
|
||||
role="alert"
|
||||
class={{if
|
||||
this.flash
|
||||
(concat-class
|
||||
"alert" (concat "alert-" (or this.flash.messageClass "success"))
|
||||
)
|
||||
}}
|
||||
>
|
||||
{{~this.flash.text~}}
|
||||
</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>
|
|
@ -0,0 +1,253 @@
|
|||
// Remove when legacy modals are dropped (deprecation: discourse.modal-controllers)
|
||||
|
||||
import Component from "@glimmer/component";
|
||||
import I18n from "I18n";
|
||||
import { next, schedule } from "@ember/runloop";
|
||||
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";
|
||||
|
||||
@disableImplicitInjections
|
||||
export default class DModal extends Component {
|
||||
@service appEvents;
|
||||
@service modal;
|
||||
|
||||
@tracked wrapperElement;
|
||||
@tracked modalBodyData = {};
|
||||
@tracked flash;
|
||||
|
||||
get modalStyle() {
|
||||
if (this.args.modalStyle === "inline-modal") {
|
||||
return "inline-modal";
|
||||
} else {
|
||||
return "fixed-modal";
|
||||
}
|
||||
}
|
||||
|
||||
get submitOnEnter() {
|
||||
if ("submitOnEnter" in this.modalBodyData) {
|
||||
return this.modalBodyData.submitOnEnter;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
this.appEvents.on("modal-body:flash", this._flash);
|
||||
this.appEvents.on("modal-body:clearFlash", this._clearFlash);
|
||||
document.documentElement.addEventListener(
|
||||
"keydown",
|
||||
this._handleModalEvents
|
||||
);
|
||||
this.wrapperElement = element;
|
||||
}
|
||||
|
||||
@action
|
||||
cleanupListeners() {
|
||||
this.appEvents.off("modal:body-shown", this._modalBodyShown);
|
||||
this.appEvents.off("modal-body:flash", this._flash);
|
||||
this.appEvents.off("modal-body:clearFlash", this._clearFlash);
|
||||
document.documentElement.removeEventListener(
|
||||
"keydown",
|
||||
this._handleModalEvents
|
||||
);
|
||||
}
|
||||
|
||||
get ariaLabelledby() {
|
||||
if (this.modalBodyData.titleAriaElementId) {
|
||||
return this.modalBodyData.titleAriaElementId;
|
||||
} else if (this.args.titleAriaElementId) {
|
||||
return this.args.titleAriaElementId;
|
||||
} else if (this.args.title) {
|
||||
return "discourse-modal-title";
|
||||
}
|
||||
}
|
||||
|
||||
get modalClass() {
|
||||
return this.modalBodyData.modalClass || this.args.modalClass;
|
||||
}
|
||||
|
||||
triggerClickOnEnter(e) {
|
||||
if (!this.submitOnEnter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// skip when in a form or a textarea element
|
||||
if (
|
||||
e.target.closest("form") ||
|
||||
(document.activeElement && document.activeElement.nodeName === "TEXTAREA")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@action
|
||||
handleMouseDown(e) {
|
||||
if (!this.dismissable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
e.target.classList.contains("modal-middle-container") ||
|
||||
e.target.classList.contains("modal-outer-container")
|
||||
) {
|
||||
// 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
|
||||
// it unclickable.
|
||||
return this.args.closeModal?.("initiatedByClickOut");
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
_modalBodyShown(data) {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.fixed) {
|
||||
this.modal.hidden = false;
|
||||
}
|
||||
|
||||
this.modalBodyData = data;
|
||||
|
||||
next(() => {
|
||||
schedule("afterRender", () => {
|
||||
this._trapTab();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
_handleModalEvents(event) {
|
||||
if (this.args.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Escape" && this.dismissable) {
|
||||
next(() => this.args.closeModal("initiatedByESC"));
|
||||
}
|
||||
|
||||
if (event.key === "Enter" && this.triggerClickOnEnter(event)) {
|
||||
this.wrapperElement.querySelector(".modal-footer .btn-primary")?.click();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (event.key === "Tab") {
|
||||
this._trapTab(event);
|
||||
}
|
||||
}
|
||||
|
||||
_trapTab(event) {
|
||||
if (this.args.hidden) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const innerContainer = this.wrapperElement.querySelector(
|
||||
".modal-inner-container"
|
||||
);
|
||||
if (!innerContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let focusableElements =
|
||||
'[autofocus], a, input, select, textarea, summary, [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
if (!event) {
|
||||
// on first trap we don't allow to focus modal-close
|
||||
// and apply manual focus only if we don't have any autofocus element
|
||||
const autofocusedElement = innerContainer.querySelector("[autofocus]");
|
||||
if (
|
||||
!autofocusedElement ||
|
||||
document.activeElement !== autofocusedElement
|
||||
) {
|
||||
// if there's not autofocus, or the activeElement, is not the autofocusable element
|
||||
// attempt to focus the first of the focusable elements or just the modal-body
|
||||
// to make it possible to scroll with arrow down/up
|
||||
(
|
||||
autofocusedElement ||
|
||||
innerContainer.querySelector(
|
||||
focusableElements + ", button:not(.modal-close)"
|
||||
) ||
|
||||
innerContainer.querySelector(".modal-body")
|
||||
)?.focus();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
focusableElements += ", button:enabled";
|
||||
|
||||
const firstFocusableElement =
|
||||
innerContainer.querySelector(focusableElements);
|
||||
const focusableContent = innerContainer.querySelectorAll(focusableElements);
|
||||
const lastFocusableElement = focusableContent[focusableContent.length - 1];
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (document.activeElement === firstFocusableElement) {
|
||||
lastFocusableElement?.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === lastFocusableElement) {
|
||||
(
|
||||
innerContainer.querySelector(".modal-close") || firstFocusableElement
|
||||
)?.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
_clearFlash() {
|
||||
this.flash = null;
|
||||
}
|
||||
|
||||
@bind
|
||||
_flash(msg) {
|
||||
this.flash = msg;
|
||||
}
|
||||
}
|
|
@ -1,94 +1,101 @@
|
|||
{{! template-lint-disable no-pointer-down-event-binding }}
|
||||
{{! template-lint-disable no-invalid-interactive }}
|
||||
|
||||
<div
|
||||
<ConditionalInElement
|
||||
@element={{this.modal.containerElement}}
|
||||
@inline={{@inline}}
|
||||
>
|
||||
<div
|
||||
class={{concat-class
|
||||
"modal"
|
||||
"d-modal"
|
||||
this.modalClass
|
||||
this.modalStyle
|
||||
(if this.hasPanels "has-panels")
|
||||
(if @hidden "hidden")
|
||||
(if @inline "inline-modal" "fixed-modal")
|
||||
}}
|
||||
id={{if (not-eq this.modalStyle "inline-modal") "discourse-modal"}}
|
||||
data-keyboard="false"
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
aria-labelledby={{this.ariaLabelledby}}
|
||||
aria-labelledby={{if @title "discourse-modal-title"}}
|
||||
...attributes
|
||||
{{did-insert this.setupListeners}}
|
||||
{{will-destroy this.cleanupListeners}}
|
||||
{{on "mousedown" this.handleMouseDown}}
|
||||
>
|
||||
{{on "mouseup" this.handleMouseUp}}
|
||||
>
|
||||
<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}}">
|
||||
{{yield to="aboveHeader"}}
|
||||
|
||||
{{#if
|
||||
(or
|
||||
this.dismissable
|
||||
@title
|
||||
(has-block "headerBelowTitle")
|
||||
(has-block "headerAboveTitle")
|
||||
)
|
||||
}}
|
||||
<div class={{concat-class "modal-header" @headerClass}}>
|
||||
{{#if this.dismissable}}
|
||||
<DButton
|
||||
@icon="times"
|
||||
@action={{route-action "closeModal" "initiatedByCloseButton"}}
|
||||
@action={{this.handleCloseButton}}
|
||||
@class="btn-flat modal-close close"
|
||||
@title="modal.close"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{yield to="headerAboveTitle"}}
|
||||
|
||||
<div class="modal-title-wrapper">
|
||||
{{#if this.title}}
|
||||
{{#if @title}}
|
||||
<div class="title">
|
||||
<h3 id="discourse-modal-title">{{this.title}}</h3>
|
||||
<h3 id="discourse-modal-title">{{@title}}</h3>
|
||||
|
||||
{{#if this.subtitle}}
|
||||
<p class="subtitle">{{this.subtitle}}</p>
|
||||
{{#if @subtitle}}
|
||||
<p class="subtitle">{{@subtitle}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<span id="modal-header-after-title"></span>
|
||||
{{yield to="headerBelowTitle"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.panels}}
|
||||
<ul class="modal-tabs">
|
||||
{{#each this.panels as |panel|}}
|
||||
<ModalTab
|
||||
@panel={{panel}}
|
||||
@panelsLength={{this.panels.length}}
|
||||
@selectedPanel={{@selectedPanel}}
|
||||
@onSelectPanel={{@onSelectPanel}}
|
||||
/>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{yield to="belowHeader"}}
|
||||
|
||||
{{#if @flash}}
|
||||
<div
|
||||
id="modal-alert"
|
||||
role="alert"
|
||||
class={{if
|
||||
this.flash
|
||||
(concat-class
|
||||
"alert" (concat "alert-" (or this.flash.messageClass "success"))
|
||||
)
|
||||
class={{concat-class
|
||||
"alert"
|
||||
(concat "alert-" (or @flashType "success"))
|
||||
}}
|
||||
>
|
||||
{{~this.flash.text~}}
|
||||
{{~@flash~}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="modal-body" tabindex="-1">
|
||||
{{#if (has-block "body")}}
|
||||
{{yield to="body"}}
|
||||
{{else}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#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}}
|
||||
{{#if (has-block "footer")}}
|
||||
<div class="modal-footer">
|
||||
{{yield to="footer"}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{yield to="belowFooter"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#unless @inline}}
|
||||
<div class="modal-backdrop"></div>
|
||||
{{/unless}}
|
||||
</ConditionalInElement>
|
|
@ -1,121 +1,54 @@
|
|||
import Component from "@glimmer/component";
|
||||
import I18n from "I18n";
|
||||
import { next, schedule } from "@ember/runloop";
|
||||
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";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export const CLOSE_INITIATED_BY_BUTTON = "initiatedByCloseButton";
|
||||
export const CLOSE_INITIATED_BY_ESC = "initiatedByESC";
|
||||
export const CLOSE_INITIATED_BY_CLICK_OUTSIDE = "initiatedByClickOut";
|
||||
export const CLOSE_INITIATED_BY_MODAL_SHOW = "initiatedByModalShow";
|
||||
|
||||
@disableImplicitInjections
|
||||
export default class DModal extends Component {
|
||||
@service appEvents;
|
||||
@service modal;
|
||||
|
||||
@tracked wrapperElement;
|
||||
@tracked modalBodyData = {};
|
||||
@tracked flash;
|
||||
|
||||
get modalStyle() {
|
||||
if (this.args.modalStyle === "inline-modal") {
|
||||
return "inline-modal";
|
||||
} else {
|
||||
return "fixed-modal";
|
||||
}
|
||||
}
|
||||
|
||||
get submitOnEnter() {
|
||||
if ("submitOnEnter" in this.modalBodyData) {
|
||||
return this.modalBodyData.submitOnEnter;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
this.appEvents.on("modal-body:flash", this._flash);
|
||||
this.appEvents.on("modal-body:clearFlash", this._clearFlash);
|
||||
document.documentElement.addEventListener(
|
||||
"keydown",
|
||||
this._handleModalEvents
|
||||
this.handleDocumentKeydown
|
||||
);
|
||||
this.wrapperElement = element;
|
||||
this.trapTab();
|
||||
}
|
||||
|
||||
@action
|
||||
cleanupListeners() {
|
||||
this.appEvents.off("modal:body-shown", this._modalBodyShown);
|
||||
this.appEvents.off("modal-body:flash", this._flash);
|
||||
this.appEvents.off("modal-body:clearFlash", this._clearFlash);
|
||||
document.documentElement.removeEventListener(
|
||||
"keydown",
|
||||
this._handleModalEvents
|
||||
this.handleDocumentKeydown
|
||||
);
|
||||
}
|
||||
|
||||
get ariaLabelledby() {
|
||||
if (this.modalBodyData.titleAriaElementId) {
|
||||
return this.modalBodyData.titleAriaElementId;
|
||||
} else if (this.args.titleAriaElementId) {
|
||||
return this.args.titleAriaElementId;
|
||||
} else if (this.args.title) {
|
||||
return "discourse-modal-title";
|
||||
get dismissable() {
|
||||
if (!this.args.closeModal) {
|
||||
return false;
|
||||
} else if ("dismissable" in this.args) {
|
||||
return this.args.dismissable;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
get modalClass() {
|
||||
return this.modalBodyData.modalClass || this.args.modalClass;
|
||||
}
|
||||
|
||||
triggerClickOnEnter(e) {
|
||||
if (!this.submitOnEnter) {
|
||||
shouldTriggerClickOnEnter(event) {
|
||||
if (this.args.submitOnEnter === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// skip when in a form or a textarea element
|
||||
if (
|
||||
e.target.closest("form") ||
|
||||
(document.activeElement && document.activeElement.nodeName === "TEXTAREA")
|
||||
event.target.closest("form") ||
|
||||
document.activeElement?.nodeName === "TEXTAREA"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
@ -124,7 +57,11 @@ export default class DModal extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
handleMouseDown(e) {
|
||||
handleMouseUp(e) {
|
||||
if (e.button !== 0) {
|
||||
return; // Non-default mouse button
|
||||
}
|
||||
|
||||
if (!this.dismissable) {
|
||||
return;
|
||||
}
|
||||
|
@ -133,53 +70,34 @@ export default class DModal extends Component {
|
|||
e.target.classList.contains("modal-middle-container") ||
|
||||
e.target.classList.contains("modal-outer-container")
|
||||
) {
|
||||
// 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
|
||||
// it unclickable.
|
||||
return this.args.closeModal?.("initiatedByClickOut");
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
_modalBodyShown(data) {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.fixed) {
|
||||
this.modal.hidden = false;
|
||||
}
|
||||
|
||||
this.modalBodyData = data;
|
||||
|
||||
next(() => {
|
||||
schedule("afterRender", () => {
|
||||
this._trapTab();
|
||||
});
|
||||
return this.args.closeModal?.({
|
||||
initiatedBy: CLOSE_INITIATED_BY_CLICK_OUTSIDE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
_handleModalEvents(event) {
|
||||
@action
|
||||
handleDocumentKeydown(event) {
|
||||
if (this.args.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Escape" && this.dismissable) {
|
||||
next(() => this.args.closeModal("initiatedByESC"));
|
||||
this.args.closeModal({ initiatedBy: CLOSE_INITIATED_BY_ESC });
|
||||
}
|
||||
|
||||
if (event.key === "Enter" && this.triggerClickOnEnter(event)) {
|
||||
if (event.key === "Enter" && this.shouldTriggerClickOnEnter(event)) {
|
||||
this.wrapperElement.querySelector(".modal-footer .btn-primary")?.click();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (event.key === "Tab") {
|
||||
this._trapTab(event);
|
||||
this.trapTab(event);
|
||||
}
|
||||
}
|
||||
|
||||
_trapTab(event) {
|
||||
@action
|
||||
trapTab(event) {
|
||||
if (this.args.hidden) {
|
||||
return true;
|
||||
}
|
||||
|
@ -239,13 +157,8 @@ export default class DModal extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
_clearFlash() {
|
||||
this.flash = null;
|
||||
}
|
||||
|
||||
@bind
|
||||
_flash(msg) {
|
||||
this.flash = msg;
|
||||
@action
|
||||
handleCloseButton() {
|
||||
this.args.closeModal({ initiatedBy: CLOSE_INITIATED_BY_BUTTON });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,24 @@
|
|||
<DModal
|
||||
@modalClass={{concat-class
|
||||
<div class="modal-container" {{did-insert this.modal.setContainerElement}}>
|
||||
</div>
|
||||
|
||||
{{#if this.modal.modalBodyComponent}}
|
||||
<this.modal.modalBodyComponent
|
||||
@model={{this.modal.opts.model}}
|
||||
@closeModal={{this.closeModal}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{! Legacy modals depend on this wrapper being in the DOM at all times. Eventually this will be dropped.
|
||||
For now, we mitigate the potential impact on things like tests by removing the `modal` and `d-modal` classes when inactive }}
|
||||
<DModalLegacy
|
||||
@modalClass={{if
|
||||
this.modal.isLegacy
|
||||
(concat-class
|
||||
"modal"
|
||||
"d-modal"
|
||||
this.modal.modalClass
|
||||
(if this.modal.opts.panels "has-tabs")
|
||||
)
|
||||
}}
|
||||
@title={{this.modal.title}}
|
||||
@titleAriaElementId={{this.modal.opts.titleAriaElementId}}
|
||||
|
@ -9,9 +26,8 @@
|
|||
@selectedPanel={{this.modal.selectedPanel}}
|
||||
@onSelectPanel={{this.modal.onSelectPanel}}
|
||||
@hidden={{this.modal.hidden}}
|
||||
@class="hidden"
|
||||
@errors={{this.modal.errors}}
|
||||
@closeModal={{this.closeModal}}
|
||||
>
|
||||
{{outlet "modalBody"}}
|
||||
</DModal>
|
||||
</DModalLegacy>
|
|
@ -6,7 +6,7 @@ export default class ModalContainer extends Component {
|
|||
@service modal;
|
||||
|
||||
@action
|
||||
closeModal(initiatedBy) {
|
||||
this.modal.close(initiatedBy);
|
||||
closeModal(data) {
|
||||
this.modal.close(data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Remove when legacy modals are dropped (deprecation: discourse.modal-controllers)
|
||||
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
|
||||
/**
|
||||
|
@ -17,6 +19,11 @@ import { getOwner } from "discourse-common/lib/get-owner";
|
|||
* @returns {Controller} The modal controller instance
|
||||
*/
|
||||
export default function showModal(name, opts) {
|
||||
if (typeof name !== "string") {
|
||||
throw new Error(
|
||||
"`discourse/lib/show-modal` can only be used with the legacy controller-based API. To use the new component-based API, inject the modal service and call modal.show(). https://meta.discourse.org/t/268057"
|
||||
);
|
||||
}
|
||||
opts = opts || {};
|
||||
|
||||
let container = getOwner(this);
|
||||
|
|
|
@ -1,16 +1,80 @@
|
|||
import Service, { inject as service } from "@ember/service";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { getOwner } from "@ember/application";
|
||||
import I18n from "I18n";
|
||||
import { dasherize } from "@ember/string";
|
||||
import { action } from "@ember/object";
|
||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { CLOSE_INITIATED_BY_MODAL_SHOW } from "discourse/components/d-modal";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
|
||||
const LEGACY_OPTS = new Set([
|
||||
"admin",
|
||||
"templateName",
|
||||
"title",
|
||||
"titleTranslated",
|
||||
"modalClass",
|
||||
"titleAriaElementId",
|
||||
"panels",
|
||||
]);
|
||||
|
||||
@disableImplicitInjections
|
||||
export default class ModalService extends Service {
|
||||
class ModalService extends Service {
|
||||
@tracked modalBodyComponent;
|
||||
@tracked opts = {};
|
||||
@tracked containerElement;
|
||||
#resolveShowPromise;
|
||||
|
||||
@action
|
||||
setContainerElement(element) {
|
||||
this.containerElement = element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a modal
|
||||
*
|
||||
* @param {Component} modal - a reference to the component class for the modal
|
||||
* @param {Object} [options] - options
|
||||
* @param {string} [options.model] - An object which will be passed as the `@model` argument on the component
|
||||
*
|
||||
* @returns {Promise} A promise that resolves when the modal is closed, with any data passed to closeModal
|
||||
*/
|
||||
show(modal, opts) {
|
||||
this.close({ initiatedBy: CLOSE_INITIATED_BY_MODAL_SHOW });
|
||||
|
||||
const promise = new Promise((resolve) => {
|
||||
this.#resolveShowPromise = resolve;
|
||||
});
|
||||
|
||||
this.opts = opts || {};
|
||||
this.modalBodyComponent = modal;
|
||||
|
||||
const unsupportedOpts = Object.keys(opts).filter((key) =>
|
||||
LEGACY_OPTS.has(key)
|
||||
);
|
||||
if (unsupportedOpts.length > 0) {
|
||||
throw new Error(
|
||||
`${unsupportedOpts.join(
|
||||
", "
|
||||
)} are not supported in the component-based modal API. See https://meta.discourse.org/t/268057`
|
||||
);
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
close(data) {
|
||||
this.#resolveShowPromise?.(data);
|
||||
this.#resolveShowPromise = this.modalBodyComponent = null;
|
||||
this.opts = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all logic below when legacy modals are dropped (deprecation: discourse.modal-controllers)
|
||||
export default class ModalServiceWithLegacySupport extends ModalService {
|
||||
@service appEvents;
|
||||
|
||||
@tracked name;
|
||||
@tracked opts = {};
|
||||
@tracked selectedPanel;
|
||||
@tracked hidden = true;
|
||||
|
||||
|
@ -35,7 +99,7 @@ export default class ModalService extends Service {
|
|||
}
|
||||
|
||||
get modalClass() {
|
||||
if (!this.#isRendered) {
|
||||
if (!this.isLegacy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -50,7 +114,22 @@ export default class ModalService extends Service {
|
|||
this.modalClassOverride = value;
|
||||
}
|
||||
|
||||
show(name, opts = {}) {
|
||||
show(modal, opts = {}) {
|
||||
if (typeof modal !== "string") {
|
||||
return super.show(modal, opts);
|
||||
}
|
||||
|
||||
deprecated(
|
||||
"Defining modals using a controller is deprecated. Use the component-based API instead.",
|
||||
{
|
||||
id: "discourse.modal-controllers",
|
||||
since: "3.1",
|
||||
dropFrom: "3.2",
|
||||
url: "https://meta.discourse.org/t/268057",
|
||||
}
|
||||
);
|
||||
|
||||
const name = modal;
|
||||
const container = getOwner(this);
|
||||
const route = container.lookup("route:application");
|
||||
|
||||
|
@ -96,10 +175,14 @@ export default class ModalService extends Service {
|
|||
}
|
||||
controller.set("flashMessage", null);
|
||||
|
||||
return controller;
|
||||
return (this.activeController = controller);
|
||||
}
|
||||
|
||||
close(initiatedBy) {
|
||||
if (!this.isLegacy) {
|
||||
super.close(...arguments);
|
||||
}
|
||||
|
||||
const controllerName = this.name;
|
||||
const controller = controllerName
|
||||
? getOwner(this).lookup(`controller:${controllerName}`)
|
||||
|
@ -137,18 +220,26 @@ export default class ModalService extends Service {
|
|||
this.onSelectPanel =
|
||||
null;
|
||||
|
||||
this.opts = {};
|
||||
super.close();
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (this.isLegacy) {
|
||||
$(".d-modal.fixed-modal").modal("hide");
|
||||
} else {
|
||||
throw "hide/reopen are not supported for component-based modals";
|
||||
}
|
||||
}
|
||||
|
||||
reopen() {
|
||||
if (this.isLegacy) {
|
||||
$(".d-modal.fixed-modal").modal("show");
|
||||
} else {
|
||||
throw "hide/reopen are not supported for component-based modals";
|
||||
}
|
||||
}
|
||||
|
||||
get #isRendered() {
|
||||
return !!this.name;
|
||||
get isLegacy() {
|
||||
return this.name && !this.modalBodyComponent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,5 +8,6 @@ globalThis.deprecationWorkflow.config = {
|
|||
{ handler: "silence", matchId: "route-disconnect-outlet" },
|
||||
{ handler: "silence", matchId: "this-property-fallback" },
|
||||
{ handler: "silence", matchId: "discourse.select-kit" },
|
||||
{ handler: "silence", matchId: "discourse.modal-controllers" },
|
||||
],
|
||||
};
|
||||
|
|
|
@ -38,7 +38,7 @@ acceptance("Do not disturb", function (needs) {
|
|||
|
||||
await click(tiles[0]);
|
||||
|
||||
assert.ok(query(".d-modal.hidden"), "modal is hidden");
|
||||
assert.dom(".d-modal").doesNotExist("modal is hidden");
|
||||
|
||||
assert.ok(
|
||||
exists(".header-dropdown-toggle .do-not-disturb-background .d-icon-moon"),
|
||||
|
@ -68,10 +68,9 @@ acceptance("Do not disturb", function (needs) {
|
|||
"Enter"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
query(".d-modal.hidden"),
|
||||
"DND modal is hidden after making a choice"
|
||||
);
|
||||
assert
|
||||
.dom(".d-modal")
|
||||
.doesNotExist("DND modal is hidden after making a choice");
|
||||
|
||||
assert.ok(
|
||||
exists(".header-dropdown-toggle .do-not-disturb-background .d-icon-moon"),
|
||||
|
|
|
@ -12,7 +12,7 @@ import showModal from "discourse/lib/show-modal";
|
|||
import { registerTemporaryModule } from "../helpers/temporary-module-helper";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
|
||||
acceptance("Modal", function (needs) {
|
||||
acceptance("Legacy Modal", function (needs) {
|
||||
let _translations;
|
||||
needs.hooks.beforeEach(() => {
|
||||
_translations = I18n.translations;
|
|
@ -0,0 +1,113 @@
|
|||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, settled, triggerKeyEvent, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { getOwner } from "@ember/application";
|
||||
import Component from "@glimmer/component";
|
||||
import { setComponentTemplate } from "@glimmer/manager";
|
||||
import {
|
||||
CLOSE_INITIATED_BY_BUTTON,
|
||||
CLOSE_INITIATED_BY_CLICK_OUTSIDE,
|
||||
CLOSE_INITIATED_BY_ESC,
|
||||
CLOSE_INITIATED_BY_MODAL_SHOW,
|
||||
} from "discourse/components/d-modal";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
class MyModalClass extends Component {
|
||||
@action
|
||||
closeWithCustomData() {
|
||||
this.args.closeModal({ hello: "world" });
|
||||
}
|
||||
}
|
||||
setComponentTemplate(
|
||||
hbs`
|
||||
<DModal
|
||||
@closeModal={{@closeModal}}
|
||||
@title="Hello World"
|
||||
>
|
||||
Modal content is {{@model.text}}
|
||||
<button class='custom-data' {{on "click" this.closeWithCustomData}}></button>
|
||||
</DModal>
|
||||
`,
|
||||
MyModalClass
|
||||
);
|
||||
|
||||
acceptance("Modal service: component-based API", function () {
|
||||
test("displays correctly", async function (assert) {
|
||||
await visit("/");
|
||||
|
||||
assert.dom(".d-modal").doesNotExist("there is no modal at first");
|
||||
|
||||
const modalService = getOwner(this).lookup("service:modal");
|
||||
|
||||
let promise = modalService.show(MyModalClass, {
|
||||
model: { text: "working" },
|
||||
});
|
||||
await settled();
|
||||
assert.dom(".d-modal").exists("modal should appear");
|
||||
|
||||
assert.dom(".d-modal .title h3").hasText("Hello World");
|
||||
assert.dom(".d-modal .modal-body").hasText("Modal content is working");
|
||||
|
||||
await click(".modal-outer-container");
|
||||
assert.dom(".d-modal").doesNotExist("disappears on click outside");
|
||||
assert.deepEqual(
|
||||
await promise,
|
||||
{ initiatedBy: CLOSE_INITIATED_BY_CLICK_OUTSIDE },
|
||||
"promise resolves with correct initiator"
|
||||
);
|
||||
|
||||
promise = modalService.show(MyModalClass, { model: { text: "working" } });
|
||||
await settled();
|
||||
assert.dom(".d-modal").exists("modal reappears");
|
||||
|
||||
await triggerKeyEvent("#main-outlet", "keydown", "Escape");
|
||||
assert.dom(".d-modal").doesNotExist("disappears on escape");
|
||||
assert.deepEqual(
|
||||
await promise,
|
||||
{ initiatedBy: CLOSE_INITIATED_BY_ESC },
|
||||
"promise resolves with correct initiator"
|
||||
);
|
||||
|
||||
promise = modalService.show(MyModalClass, { model: { text: "working" } });
|
||||
await settled();
|
||||
assert.dom(".d-modal").exists("modal reappears");
|
||||
|
||||
await click(".d-modal .modal-close");
|
||||
assert.dom(".d-modal").doesNotExist("disappears when close button clicked");
|
||||
assert.deepEqual(
|
||||
await promise,
|
||||
{ initiatedBy: CLOSE_INITIATED_BY_BUTTON },
|
||||
"promise resolves with correct initiator"
|
||||
);
|
||||
|
||||
promise = modalService.show(MyModalClass, { model: { text: "working" } });
|
||||
await settled();
|
||||
assert.dom(".d-modal").exists("modal reappears");
|
||||
|
||||
await click(".d-modal .modal-close");
|
||||
assert.dom(".d-modal").doesNotExist("disappears when close button clicked");
|
||||
assert.deepEqual(
|
||||
await promise,
|
||||
{ initiatedBy: CLOSE_INITIATED_BY_BUTTON },
|
||||
"promise resolves with correct initiator"
|
||||
);
|
||||
|
||||
promise = modalService.show(MyModalClass, { model: { text: "first" } });
|
||||
await settled();
|
||||
assert.dom(".d-modal").exists("modal reappears");
|
||||
|
||||
modalService.show(MyModalClass, { model: { text: "second" } });
|
||||
await settled();
|
||||
assert
|
||||
.dom(".d-modal .modal-body")
|
||||
.hasText("Modal content is second", "new modal replaces old");
|
||||
assert.deepEqual(
|
||||
await promise,
|
||||
{ initiatedBy: CLOSE_INITIATED_BY_MODAL_SHOW },
|
||||
"first modal promise resolves with correct initiator"
|
||||
);
|
||||
});
|
||||
|
||||
// (See also, `tests/integration/component/d-modal-test.js`)
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { click, render, settled } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
|
||||
module("Integration | Component | d-modal", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("title and subtitle", async function (assert) {
|
||||
await render(
|
||||
hbs`<DModal @inline={{true}} @title="Modal Title" @subtitle="Modal Subtitle" />`
|
||||
);
|
||||
assert.dom(".d-modal .title h3").hasText("Modal Title");
|
||||
assert.dom(".d-modal .subtitle").hasText("Modal Subtitle");
|
||||
});
|
||||
|
||||
test("named blocks", async function (assert) {
|
||||
await render(
|
||||
hbs`
|
||||
<DModal @inline={{true}}>
|
||||
<:aboveHeader>aboveHeaderContent</:aboveHeader>
|
||||
<:headerAboveTitle>headerAboveTitleContent</:headerAboveTitle>
|
||||
<:headerBelowTitle>headerBelowTitleContent</:headerBelowTitle>
|
||||
<:belowHeader>belowHeaderContent</:belowHeader>
|
||||
<:body>bodyContent</:body>
|
||||
<:footer>footerContent</:footer>
|
||||
<:belowFooter>belowFooterContent</:belowFooter>
|
||||
</DModal>
|
||||
`
|
||||
);
|
||||
|
||||
assert.dom(".d-modal").includesText("aboveHeaderContent");
|
||||
assert.dom(".d-modal").includesText("headerAboveTitleContent");
|
||||
assert.dom(".d-modal").includesText("headerBelowTitleContent");
|
||||
assert.dom(".d-modal").includesText("belowHeaderContent");
|
||||
assert.dom(".d-modal").includesText("bodyContent");
|
||||
assert.dom(".d-modal").includesText("footerContent");
|
||||
assert.dom(".d-modal").includesText("belowFooterContent");
|
||||
});
|
||||
|
||||
test("flash", async function (assert) {
|
||||
await render(
|
||||
hbs`<DModal @inline={{true}} @flash="Some message" @flashType="error"/> `
|
||||
);
|
||||
assert.dom(".d-modal .alert.alert-error").hasText("Some message");
|
||||
});
|
||||
|
||||
test("dismissable", async function (assert) {
|
||||
let closeModalCalled = false;
|
||||
this.closeModal = () => (closeModalCalled = true);
|
||||
this.set("dismissable", false);
|
||||
|
||||
await render(
|
||||
hbs`<DModal @inline={{true}} @closeModal={{this.closeModal}} @dismissable={{this.dismissable}}/>`
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(".d-modal .modal-close")
|
||||
.doesNotExist("close button is not shown when dismissable=false");
|
||||
|
||||
this.set("dismissable", true);
|
||||
await settled();
|
||||
assert
|
||||
.dom(".d-modal .modal-close")
|
||||
.exists("close button is visible when dismissable=true");
|
||||
|
||||
await click(".d-modal .modal-close");
|
||||
assert.true(
|
||||
closeModalCalled,
|
||||
"closeModal is called when close button clicked"
|
||||
);
|
||||
|
||||
closeModalCalled = false;
|
||||
});
|
||||
});
|
|
@ -1,15 +1,57 @@
|
|||
<StyleguideExample @title="<DModal>">
|
||||
<DModal
|
||||
@closeModal={{@dummyAction}}
|
||||
@modalStyle="inline-modal"
|
||||
@title={{i18n "styleguide.sections.modal.header"}}
|
||||
>
|
||||
<DModalBody>
|
||||
{{html-safe @dummy.lorem}}
|
||||
</DModalBody>
|
||||
{{! template-lint-disable no-potential-path-strings}}
|
||||
|
||||
<div class="modal-footer">
|
||||
<StyleguideExample @title="<DModal>">
|
||||
<Styleguide::Component>
|
||||
<DModal
|
||||
@closeModal={{fn (mut this.inline) true}}
|
||||
@inline={{this.inline}}
|
||||
@title={{this.title}}
|
||||
@subtitle={{this.subtitle}}
|
||||
@flash={{this.flash}}
|
||||
@flashType={{this.flashType}}
|
||||
@errors={{this.errors}}
|
||||
@dismissable={{this.dismissable}}
|
||||
>
|
||||
<:body>
|
||||
{{this.body}}
|
||||
</:body>
|
||||
|
||||
<:footer>
|
||||
{{i18n "styleguide.sections.modal.footer"}}
|
||||
</div>
|
||||
</:footer>
|
||||
</DModal>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Controls>
|
||||
<Styleguide::Controls::Row @name="@inline">
|
||||
<DToggleSwitch @state={{this.inline}} {{on "click" this.toggleInline}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="@dismissable">
|
||||
<DToggleSwitch
|
||||
@state={{this.dismissable}}
|
||||
{{on "click" this.toggleDismissable}}
|
||||
/>
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="@title">
|
||||
<Input @value={{this.title}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="@subtitle">
|
||||
<Input @value={{this.subtitle}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="<:body>">
|
||||
<Textarea @value={{this.body}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="@flash">
|
||||
<Input @value={{this.flash}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="@flashType">
|
||||
<ComboBox
|
||||
@value={{this.flashType}}
|
||||
@content={{this.flashTypes}}
|
||||
@onChange={{fn (mut this.flashType)}}
|
||||
@valueProperty={{null}}
|
||||
@nameProperty={{null}}
|
||||
/>
|
||||
</Styleguide::Controls::Row>
|
||||
</Styleguide::Controls>
|
||||
</StyleguideExample>
|
|
@ -0,0 +1,39 @@
|
|||
import { action } from "@ember/object";
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default class extends Component {
|
||||
@tracked inline = true;
|
||||
@tracked dismissable = true;
|
||||
@tracked title = I18n.t("styleguide.sections.modal.header");
|
||||
@tracked body = this.args.dummy.shortLorem;
|
||||
@tracked subtitle = "";
|
||||
@tracked flash = "";
|
||||
@tracked flashType = "success";
|
||||
|
||||
flashTypes = ["success", "info", "warning", "error"];
|
||||
|
||||
@action
|
||||
toggleInline() {
|
||||
this.inline = !this.inline;
|
||||
if (!this.inline) {
|
||||
// Make sure there is a way to dismiss the modal
|
||||
this.dismissable = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleDismissable() {
|
||||
this.dismissable = !this.dismissable;
|
||||
if (!this.dismissable) {
|
||||
// Make sure there is a way to dismiss the modal
|
||||
this.inline = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleShowFooter() {
|
||||
this.showFooter = !this.showFooter;
|
||||
}
|
||||
}
|
|
@ -262,6 +262,8 @@ export function createData(store) {
|
|||
}),
|
||||
|
||||
lorem: cooked,
|
||||
shortLorem:
|
||||
"Lorem ipsum dolor sit amet, et nec quis viderer prompta, ex omnium ponderum insolens eos, sed discere invenire principes in. Fuisset constituto per ad. Est no scripta propriae facilisis, viderer impedit deserunt in mel. Quot debet facilisis ne vix, nam in detracto tacimates. At quidam petentium vulputate pro. Alia iudico repudiandae ad vel, erat omnis epicuri eos id. Et illum dolor graeci vel, quo feugiat consulatu ei.",
|
||||
|
||||
topicTimerUpdateDate: "2017-10-18 18:00",
|
||||
|
||||
|
|
Loading…
Reference in New Issue