DEV: Convert DButton to a Glimmer Component (#17767)
This change is intended to be backwards-compatible with all the previous arguments to `DButton`. A deprecation warning will be triggered when a string is passed to the `@action` argument. This kind of action bubbling has been deprecated in Ember for some time, and should be updated to use closure actions. Co-authored-by: Dan Gebhardt <dan@cerebris.com> Co-authored-by: David Taylor <david@taylorhq.com>
This commit is contained in:
parent
3fd0423b1b
commit
a88902950a
|
@ -1,7 +0,0 @@
|
||||||
import DButton from "discourse/components/d-button";
|
|
||||||
|
|
||||||
export default DButton.extend({
|
|
||||||
click() {
|
|
||||||
$("input.bulk-select:not(checked)").click();
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<DButton
|
||||||
|
@translatedTitle={{this.translatedTitle}}
|
||||||
|
@label={{@label}}
|
||||||
|
@action={{@action}}
|
||||||
|
@icon={{@icon}}
|
||||||
|
@forwardEvent={{@forwardEvent}}
|
||||||
|
class="btn-primary create {{if @disabledSubmit "disabled"}}"
|
||||||
|
...attributes
|
||||||
|
/>
|
|
@ -1,10 +1,9 @@
|
||||||
import Button from "discourse/components/d-button";
|
import Component from "@glimmer/component";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { translateModKey } from "discourse/lib/utilities";
|
import { translateModKey } from "discourse/lib/utilities";
|
||||||
|
|
||||||
export default Button.extend({
|
export default class ComposerSaveButton extends Component {
|
||||||
classNameBindings: [":btn-primary", ":create", "disableSubmit:disabled"],
|
get translatedTitle() {
|
||||||
translatedTitle: I18n.t("composer.title", {
|
return I18n.t("composer.title", { modifier: translateModKey("Meta+") });
|
||||||
modifier: translateModKey("Meta+"),
|
}
|
||||||
}),
|
}
|
||||||
});
|
|
||||||
|
|
|
@ -1,161 +1,137 @@
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
import { action } from "@ember/object";
|
||||||
import { empty, equal, notEmpty } from "@ember/object/computed";
|
import { empty, equal, notEmpty } from "@ember/object/computed";
|
||||||
import Component from "@ember/component";
|
import GlimmerComponentWithDeprecatedParentView from "discourse/components/glimmer-component-with-deprecated-parent-view";
|
||||||
|
import deprecated from "discourse-common/lib/deprecated";
|
||||||
import DiscourseURL from "discourse/lib/url";
|
import DiscourseURL from "discourse/lib/url";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { computed } from "@ember/object";
|
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
|
|
||||||
export default Component.extend({
|
const ACTION_AS_STRING_DEPRECATION_ARGS = [
|
||||||
tagName: "button",
|
"DButton no longer supports @action as a string. Please refactor to use an closure action instead.",
|
||||||
// subclasses need this
|
{ id: "discourse.d-button-action-string" },
|
||||||
layoutName: "components/d-button",
|
];
|
||||||
form: null,
|
|
||||||
type: "button",
|
|
||||||
title: null,
|
|
||||||
translatedTitle: null,
|
|
||||||
label: null,
|
|
||||||
translatedLabel: null,
|
|
||||||
ariaLabel: null,
|
|
||||||
ariaExpanded: null,
|
|
||||||
ariaControls: null,
|
|
||||||
translatedAriaLabel: null,
|
|
||||||
forwardEvent: false,
|
|
||||||
preventFocus: false,
|
|
||||||
onKeyDown: null,
|
|
||||||
router: service(),
|
|
||||||
|
|
||||||
isLoading: computed({
|
export default class DButton extends GlimmerComponentWithDeprecatedParentView {
|
||||||
set(key, value) {
|
@service router;
|
||||||
this.set("forceDisabled", !!value);
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
classNameBindings: [
|
@notEmpty("args.icon")
|
||||||
"isLoading:is-loading",
|
btnIcon;
|
||||||
"btnLink::btn",
|
|
||||||
"btnLink",
|
|
||||||
"noText",
|
|
||||||
"btnType",
|
|
||||||
],
|
|
||||||
attributeBindings: [
|
|
||||||
"form",
|
|
||||||
"isDisabled:disabled",
|
|
||||||
"computedTitle:title",
|
|
||||||
"computedAriaLabel:aria-label",
|
|
||||||
"computedAriaExpanded:aria-expanded",
|
|
||||||
"ariaControls:aria-controls",
|
|
||||||
"tabindex",
|
|
||||||
"type",
|
|
||||||
],
|
|
||||||
|
|
||||||
isDisabled: computed("disabled", "forceDisabled", function () {
|
@equal("args.display", "link")
|
||||||
return this.forceDisabled || this.disabled;
|
btnLink;
|
||||||
}),
|
|
||||||
|
|
||||||
forceDisabled: false,
|
@empty("computedLabel")
|
||||||
|
noText;
|
||||||
|
|
||||||
btnIcon: notEmpty("icon"),
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
if (typeof this.args.action === "string") {
|
||||||
|
deprecated(...ACTION_AS_STRING_DEPRECATION_ARGS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
btnLink: equal("display", "link"),
|
get forceDisabled() {
|
||||||
|
return !!this.args.isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
@discourseComputed("icon", "computedLabel")
|
get isDisabled() {
|
||||||
btnType(icon, translatedLabel) {
|
return this.forceDisabled || this.args.disabled;
|
||||||
if (icon) {
|
}
|
||||||
return translatedLabel ? "btn-icon-text" : "btn-icon";
|
|
||||||
} else if (translatedLabel) {
|
get btnType() {
|
||||||
|
if (this.args.icon) {
|
||||||
|
return this.computedLabel ? "btn-icon-text" : "btn-icon";
|
||||||
|
} else if (this.computedLabel) {
|
||||||
return "btn-text";
|
return "btn-text";
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
noText: empty("computedLabel"),
|
get computedTitle() {
|
||||||
|
if (this.args.title) {
|
||||||
@discourseComputed("title", "translatedTitle")
|
return I18n.t(this.args.title);
|
||||||
computedTitle(title, translatedTitle) {
|
|
||||||
if (title) {
|
|
||||||
return I18n.t(title);
|
|
||||||
}
|
}
|
||||||
return translatedTitle;
|
return this.args.translatedTitle;
|
||||||
},
|
}
|
||||||
|
|
||||||
@discourseComputed("label", "translatedLabel")
|
get computedLabel() {
|
||||||
computedLabel(label, translatedLabel) {
|
if (this.args.label) {
|
||||||
if (label) {
|
return I18n.t(this.args.label);
|
||||||
return I18n.t(label);
|
|
||||||
}
|
}
|
||||||
return translatedLabel;
|
return this.args.translatedLabel;
|
||||||
},
|
}
|
||||||
|
|
||||||
@discourseComputed("ariaLabel", "translatedAriaLabel")
|
get computedAriaLabel() {
|
||||||
computedAriaLabel(ariaLabel, translatedAriaLabel) {
|
if (this.args.ariaLabel) {
|
||||||
if (ariaLabel) {
|
return I18n.t(this.args.ariaLabel);
|
||||||
return I18n.t(ariaLabel);
|
|
||||||
}
|
}
|
||||||
if (translatedAriaLabel) {
|
if (this.args.translatedAriaLabel) {
|
||||||
return translatedAriaLabel;
|
return this.args.translatedAriaLabel;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
@discourseComputed("ariaExpanded")
|
get computedAriaExpanded() {
|
||||||
computedAriaExpanded(ariaExpanded) {
|
if (this.args.ariaExpanded === true) {
|
||||||
if (ariaExpanded === true) {
|
|
||||||
return "true";
|
return "true";
|
||||||
}
|
}
|
||||||
if (ariaExpanded === false) {
|
if (this.args.ariaExpanded === false) {
|
||||||
return "false";
|
return "false";
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
keyDown(e) {
|
keyDown(e) {
|
||||||
if (this.onKeyDown) {
|
if (this.args.onKeyDown) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.onKeyDown(e);
|
this.args.onKeyDown(e);
|
||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter") {
|
||||||
this._triggerAction(e);
|
this._triggerAction(e);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
click(event) {
|
click(event) {
|
||||||
return this._triggerAction(event);
|
return this._triggerAction(event);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
mouseDown(event) {
|
mouseDown(event) {
|
||||||
if (this.preventFocus) {
|
if (this.args.preventFocus) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_triggerAction(event) {
|
_triggerAction(event) {
|
||||||
let { action, route, href } = this;
|
const { action: actionVal, route, href } = this.args;
|
||||||
|
|
||||||
if (action || route || href?.length) {
|
if (actionVal || route || href?.length) {
|
||||||
if (action) {
|
if (actionVal) {
|
||||||
if (typeof action === "string") {
|
const { actionParam, forwardEvent } = this.args;
|
||||||
// Note: This is deprecated in new Embers and needs to be removed in the future.
|
|
||||||
// There is already a warning in the console.
|
if (typeof actionVal === "string") {
|
||||||
this.sendAction("action", this.actionParam);
|
deprecated(...ACTION_AS_STRING_DEPRECATION_ARGS);
|
||||||
} else if (typeof action === "object" && action.value) {
|
if (this.parentView?.send) {
|
||||||
if (this.forwardEvent) {
|
this.parentView.send(actionVal, actionParam);
|
||||||
action.value(this.actionParam, event);
|
|
||||||
} else {
|
} else {
|
||||||
action.value(this.actionParam);
|
throw new Error(
|
||||||
|
"DButton could not find a target for the action. Use a closure action instead"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (typeof this.action === "function") {
|
} else if (typeof actionVal === "object" && actionVal.value) {
|
||||||
if (this.forwardEvent) {
|
if (forwardEvent) {
|
||||||
action(this.actionParam, event);
|
actionVal.value(actionParam, event);
|
||||||
} else {
|
} else {
|
||||||
action(this.actionParam);
|
actionVal.value(actionParam);
|
||||||
|
}
|
||||||
|
} else if (typeof actionVal === "function") {
|
||||||
|
if (forwardEvent) {
|
||||||
|
actionVal(actionParam, event);
|
||||||
|
} else {
|
||||||
|
actionVal(actionParam);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (route) {
|
||||||
|
|
||||||
if (route) {
|
|
||||||
this.router.transitionTo(route);
|
this.router.transitionTo(route);
|
||||||
}
|
} else if (href?.length) {
|
||||||
|
|
||||||
if (href?.length) {
|
|
||||||
DiscourseURL.routeTo(href);
|
DiscourseURL.routeTo(href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,5 +140,5 @@ export default Component.extend({
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import {
|
||||||
|
CustomComponentManager,
|
||||||
|
setInternalComponentManager,
|
||||||
|
} from "@glimmer/manager";
|
||||||
|
import EmberGlimmerComponentManager from "@glimmer/component/-private/ember-component-manager";
|
||||||
|
|
||||||
|
class GlimmerComponentWithParentViewManager extends CustomComponentManager {
|
||||||
|
create(owner, componentClass, args, environment, dynamicScope) {
|
||||||
|
const result = super.create(...arguments);
|
||||||
|
|
||||||
|
result.component.parentView = dynamicScope.view;
|
||||||
|
dynamicScope.view = result.component;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component has a lightly-extended version of Ember's default Glimmer component manager.
|
||||||
|
* It gives Glimmer components the ability to reference their parent view which can be useful
|
||||||
|
* when building backwards-compatible versions of components. Any use of the parentView property
|
||||||
|
* of the component should be considered deprecated.
|
||||||
|
*/
|
||||||
|
export default class GlimmerComponentWithDeprecatedParentView extends Component {}
|
||||||
|
|
||||||
|
setInternalComponentManager(
|
||||||
|
new GlimmerComponentWithParentViewManager(
|
||||||
|
(owner) => new EmberGlimmerComponentManager(owner)
|
||||||
|
),
|
||||||
|
GlimmerComponentWithDeprecatedParentView
|
||||||
|
);
|
|
@ -1,7 +0,0 @@
|
||||||
import Button from "discourse/components/d-button";
|
|
||||||
|
|
||||||
export default Button.extend({
|
|
||||||
label: "topic.reply.title",
|
|
||||||
icon: "reply",
|
|
||||||
action: "showLogin",
|
|
||||||
});
|
|
|
@ -1,16 +1,36 @@
|
||||||
{{#if this.isLoading}}
|
{{! template-lint-disable no-down-event-binding }}
|
||||||
{{~d-icon "spinner" class="loading-icon"~}}
|
<button
|
||||||
{{else}}
|
{{! For legacy compatibility. Prefer passing class as attributes. }}
|
||||||
{{#if this.icon}}
|
class="{{@class}} {{if @isLoading "is-loading"}} {{if this.btnLink "btn-link" "btn"}} {{if this.noText "no-text"}} {{this.btnType}}"
|
||||||
{{~d-icon this.icon~}}
|
{{! For legacy compatibility. Prefer passing these as html attributes. }}
|
||||||
|
id={{@id}}
|
||||||
|
form={{@form}}
|
||||||
|
aria-controls={{@ariaControls}}
|
||||||
|
aria-expanded={{this.computedAriaExpanded}}
|
||||||
|
tabindex={{@tabindex}}
|
||||||
|
type={{or @type "button"}}
|
||||||
|
...attributes
|
||||||
|
disabled={{this.isDisabled}}
|
||||||
|
title={{this.computedTitle}}
|
||||||
|
aria-label={{this.computedAriaLabel}}
|
||||||
|
{{on "keydown" this.keyDown}}
|
||||||
|
{{on "click" this.click}}
|
||||||
|
{{on "mousedown" this.mouseDown}}
|
||||||
|
>
|
||||||
|
{{#if @isLoading}}
|
||||||
|
{{~d-icon "spinner" class="loading-icon"~}}
|
||||||
|
{{else}}
|
||||||
|
{{#if @icon}}
|
||||||
|
{{~d-icon @icon~}}
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{~#if this.computedLabel~}}
|
{{~#if this.computedLabel~}}
|
||||||
<span class="d-button-label">{{html-safe this.computedLabel}}{{#if this.ellipsis}}…{{/if}}</span>
|
<span class="d-button-label">{{html-safe this.computedLabel}}{{#if this.ellipsis}}…{{/if}}</span>
|
||||||
{{~else if (not (has-block))~}}
|
{{~else if (not (has-block))~}}
|
||||||
​
|
​
|
||||||
{{! Zero-width space character, so icon-only button height = regular button height }}
|
{{! Zero-width space character, so icon-only button height = regular button height }}
|
||||||
{{~/if~}}
|
{{~/if~}}
|
||||||
|
|
||||||
{{yield}}
|
{{yield}}
|
||||||
|
</button>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { module, test } from "qunit";
|
import { module, test } from "qunit";
|
||||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||||
import { render, triggerKeyEvent } from "@ember/test-helpers";
|
import { click, render, triggerKeyEvent } from "@ember/test-helpers";
|
||||||
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
|
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { hbs } from "ember-cli-htmlbars";
|
import { hbs } from "ember-cli-htmlbars";
|
||||||
|
import ClassicComponent from "@ember/component";
|
||||||
|
|
||||||
module("Integration | Component | d-button", function (hooks) {
|
module("Integration | Component | d-button", function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
@ -241,4 +242,41 @@ module("Integration | Component | d-button", function (hooks) {
|
||||||
await triggerKeyEvent(".btn", "keydown", "Enter");
|
await triggerKeyEvent(".btn", "keydown", "Enter");
|
||||||
assert.strictEqual(this.foo, "bar");
|
assert.strictEqual(this.foo, "bar");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("@action function is triggered on click", async function (assert) {
|
||||||
|
this.set("foo", null);
|
||||||
|
this.set("action", () => {
|
||||||
|
this.set("foo", "bar");
|
||||||
|
});
|
||||||
|
|
||||||
|
await render(hbs`<DButton @action={{this.action}} />`);
|
||||||
|
|
||||||
|
await click(".btn");
|
||||||
|
|
||||||
|
assert.strictEqual(this.foo, "bar");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("@action can sendAction when passed a string", async function (assert) {
|
||||||
|
this.set("foo", null);
|
||||||
|
this.set("legacyActionTriggered", () => this.set("foo", "bar"));
|
||||||
|
|
||||||
|
this.classicComponent = ClassicComponent.extend({
|
||||||
|
actions: {
|
||||||
|
myLegacyAction() {
|
||||||
|
this.legacyActionTriggered();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await render(
|
||||||
|
hbs`<this.classicComponent @legacyActionTriggered={{this.legacyActionTriggered}}>
|
||||||
|
<DButton @action="myLegacyAction" />
|
||||||
|
</this.classicComponent>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".btn");
|
||||||
|
|
||||||
|
assert.strictEqual(this.foo, "bar");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue