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:
parent
dc7a2f0d1a
commit
9c29d688e7
|
@ -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>
|
|
@ -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");
|
||||
});
|
||||
});
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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..."
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -144,6 +144,12 @@
|
|||
color: var(--primary-medium);
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
|
||||
&--char-counter {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons-examples {
|
||||
|
|
|
@ -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..."
|
||||
|
||||
|
|
Loading…
Reference in New Issue