diff --git a/app/assets/javascripts/discourse/app/components/char-counter.hbs b/app/assets/javascripts/discourse/app/components/char-counter.hbs
new file mode 100644
index 00000000000..3ab7e455dd7
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/char-counter.hbs
@@ -0,0 +1,12 @@
+
+ {{yield}}
+
+ {{@value.length}}/{{@max}}
+
+
+ {{if (gt @value.length @max) (i18n "char_counter.exceeded")}}
+
+
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/tests/integration/components/char-counter-test.js b/app/assets/javascripts/discourse/tests/integration/components/char-counter-test.js
new file mode 100644
index 00000000000..86520006133
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/integration/components/char-counter-test.js
@@ -0,0 +1,48 @@
+import { module, test } from "qunit";
+import { setupRenderingTest } from "discourse/tests/helpers/component-test";
+import { fillIn, render } from "@ember/test-helpers";
+import { hbs } from "ember-cli-htmlbars";
+
+module("Integration | Component | char-counter", function (hooks) {
+ setupRenderingTest(hooks);
+
+ test("shows the number of characters", async function (assert) {
+ this.value = "Hello World";
+ this.max = 12;
+
+ await render(
+ hbs``
+ );
+
+ assert.dom(this.element).includesText("11/12");
+ });
+
+ test("updating value updates counter", async function (assert) {
+ this.max = 50;
+
+ await render(
+ hbs``
+ );
+
+ assert
+ .dom(this.element)
+ .includesText("/50", "initial value appears as expected");
+
+ await fillIn("textarea", "Hello World, this is a longer string");
+
+ assert
+ .dom(this.element)
+ .includesText("36/50", "updated value appears as expected");
+ });
+
+ test("exceeding max length", async function (assert) {
+ this.max = 10;
+ this.value = "Hello World";
+
+ await render(
+ hbs``
+ );
+
+ assert.dom(".char-counter.exceeded").exists("exceeded class is applied");
+ });
+});
diff --git a/app/assets/stylesheets/common/components/_index.scss b/app/assets/stylesheets/common/components/_index.scss
index b33546edc10..f9201fa36ce 100644
--- a/app/assets/stylesheets/common/components/_index.scss
+++ b/app/assets/stylesheets/common/components/_index.scss
@@ -4,6 +4,7 @@
@import "bookmark-modal";
@import "buttons";
@import "color-input";
+@import "char-counter";
@import "conditional-loading-section";
@import "convert-to-public-topic-modal";
@import "d-tooltip";
diff --git a/app/assets/stylesheets/common/components/char-counter.scss b/app/assets/stylesheets/common/components/char-counter.scss
new file mode 100644
index 00000000000..bb95b30d146
--- /dev/null
+++ b/app/assets/stylesheets/common/components/char-counter.scss
@@ -0,0 +1,18 @@
+.char-counter {
+ &__ratio {
+ display: block;
+ text-align: right;
+ margin-top: 0.5rem;
+ }
+
+ &.exceeded {
+ > textarea {
+ border-color: var(--danger);
+ outline-color: var(--danger);
+ }
+
+ &__ratio {
+ color: var(--danger);
+ }
+ }
+}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index d855d9c87aa..b2c7ed75611 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -4453,6 +4453,9 @@ en:
until: "Until:"
+ char_counter:
+ exceeded: "The maximum number of characters allowed has been exceeded."
+
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "type to filter..."
diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js
index 85e834963ae..664a1993bad 100644
--- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js
+++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js
@@ -1,28 +1,30 @@
import Controller from "@ember/controller";
import { action, computed } from "@ember/object";
import ModalFunctionality from "discourse/mixins/modal-functionality";
+import { tracked } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
+const DESCRIPTION_MAX_LENGTH = 280;
+
export default class ChatChannelEditDescriptionController extends Controller.extend(
ModalFunctionality
) {
@service chatApi;
- editedDescription = "";
+ @tracked editedDescription = this.model.description || "";
@computed("model.description", "editedDescription")
get isSaveDisabled() {
return (
this.model.description === this.editedDescription ||
- this.editedDescription?.length > 280
+ this.editedDescription?.length > DESCRIPTION_MAX_LENGTH
);
}
- onShow() {
- this.set("editedDescription", this.model.description || "");
+ get descriptionMaxLength() {
+ return DESCRIPTION_MAX_LENGTH;
}
onClose() {
- this.set("editedDescription", "");
this.clearFlash();
}
@@ -46,6 +48,6 @@ export default class ChatChannelEditDescriptionController extends Controller.ext
@action
onChangeChatChannelDescription(description) {
this.clearFlash();
- this.set("editedDescription", description);
+ this.editedDescription = description;
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-description.hbs b/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-description.hbs
index b988acd671e..059e2d147bf 100644
--- a/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-description.hbs
+++ b/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-description.hbs
@@ -1,15 +1,24 @@
-
{{i18n "chat.channel_edit_description_modal.description"}}
+
+
+