FEATURE: Add word count and indicator when exceeded max (#19367)

**This PR creates a new core reusable component wraps a character counter around any input.**

The component accepts the arguments: `max` (the maximum character limit), `value` (the value of text to be monitored).

It can be used for example, like so:
```hbs
  <CharCounter @max="50" @value={{this.charCounterContent}}>
    <textarea
      placeholder={{i18n "styleguide.sections.char_counter.placeholder"}}
      {{on "input" (action (mut this.charCounterContent) value="target.value")}}
      class="styleguide--char-counter"></textarea>
  </CharCounter>
```

**This PR also:**
1. Applies this component to the chat plugins edit channel's *Edit Description** modal, thereby replacing the simple text area which provided no visual indication when text exceeded the max allowed characters.
2. Adds an example to the `/styleguide` route
This commit is contained in:
Keegan George 2023-02-20 03:06:43 -08:00 committed by GitHub
parent dc7a2f0d1a
commit 9c29d688e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 137 additions and 15 deletions

View File

@ -0,0 +1,12 @@
<div
class={{concat-class "char-counter" (if (gt @value.length @max) "exceeded")}}
...attributes
>
{{yield}}
<small class="char-counter__ratio">
{{@value.length}}/{{@max}}
</small>
<span aria-live="polite" class="sr-only">
{{if (gt @value.length @max) (i18n "char_counter.exceeded")}}
</span>
</div>

View File

@ -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`<CharCounter @value={{this.value}} @max={{this.max}}></CharCounter>`
);
assert.dom(this.element).includesText("11/12");
});
test("updating value updates counter", async function (assert) {
this.max = 50;
await render(
hbs`<CharCounter @value={{this.charCounterContent}} @max={{this.max}}><textarea {{on "input" (action (mut this.charCounterContent) value="target.value")}}></textarea></CharCounter>`
);
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`<CharCounter @value={{this.value}} @max={{this.max}}></CharCounter>`
);
assert.dom(".char-counter.exceeded").exists("exceeded class is applied");
});
});

View File

@ -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";

View File

@ -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);
}
}
}

View File

@ -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..."

View File

@ -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;
}
}

View File

@ -1,15 +1,24 @@
<DModalBody @title="chat.channel_edit_description_modal.title">
<textarea
class="chat-channel-edit-description-modal__description-input"
placeholder={{i18n "chat.channel_edit_description_modal.input_placeholder"}}
{{on
"input"
(action "onChangeChatChannelDescription" value="target.value")
}}
>{{this.model.description}}</textarea>
<span class="chat-channel-edit-description-modal__description">
{{i18n "chat.channel_edit_description_modal.description"}}
</span>
<CharCounter
@value={{this.editedDescription}}
@max={{this.descriptionMaxLength}}
>
<textarea
{{on
"input"
(action this.onChangeChatChannelDescription value="target.value")
}}
class="chat-channel-edit-description-modal__description-input"
placeholder={{i18n
"chat.channel_edit_description_modal.input_placeholder"
}}
>
{{this.editedDescription}}
</textarea>
</CharCounter>
</DModalBody>
<div class="modal-footer">

View File

@ -142,6 +142,15 @@ input.channel-members-view__search-input {
}
// Channel info edit description modal
.chat-channel-edit-description-modal {
.exceeded-word-count {
.chat-channel-edit-description-modal__description-input {
outline: 1px solid var(--danger);
border: 1px solid var(--danger);
}
}
}
.chat-channel-edit-description-modal__description-input {
display: flex;
margin: 0;
@ -150,6 +159,6 @@ input.channel-members-view__search-input {
.chat-channel-edit-description-modal__description {
display: flex;
padding: 0.75rem 0 0.5rem;
padding-bottom: 0.75rem;
color: var(--primary-medium);
}

View File

@ -0,0 +1,10 @@
<StyleguideExample @title="char-counter">
<CharCounter @max="50" @value={{this.charCounterContent}}>
<textarea
placeholder={{i18n "styleguide.sections.char_counter.placeholder"}}
{{on "input" (action (mut this.charCounterContent) value="target.value")}}
class="styleguide--char-counter"
>
</textarea>
</CharCounter>
</StyleguideExample>

View File

@ -144,6 +144,12 @@
color: var(--primary-medium);
margin: 0 0 0 10px;
}
&--char-counter {
display: block;
width: 100%;
margin-bottom: 0;
}
}
.buttons-examples {

View File

@ -88,3 +88,7 @@ en:
description: "Description"
header: "Header"
hover_to_see: "Hover to see a tooltip"
char_counter:
title: "Character Counter"
placeholder: "Enter your text here..."