feat(quantic): Add a character limit to the follow-up question input#7908
feat(quantic): Add a character limit to the follow-up question input#7908mmitiche wants to merge 5 commits into
Conversation
…t with a live character counter
🦋 Changeset detectedLatest commit: 9dc8103 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
There was a problem hiding this comment.
Pull request overview
Adds a 300-character limit UX to QuanticGeneratedAnswerFollowUpInput, including a live counter, an over-limit error state, localized validation messaging, and unit test coverage for the new behaviors.
Changes:
- Introduces
MAX_FOLLOW_UP_QUESTION_LENGTH = 300, tracks a trimmedcharacterCount, and blocks submission (button + Enter) when over the limit. - Adds an input footer that renders a live character counter plus an over-limit validation message wired via ARIA attributes.
- Adds a new
quantic_FollowUpInputTooLonglabel with FR/ES translations, and extends Jest tests for the validation/counter behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.js | Implements trimmed character counting, over-limit state, submit prevention, ARIA wiring, and formatted validation message. |
| packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.html | Adds aria-invalid/aria-describedby, uses submit-prevented flag, and renders the new footer (message + counter). |
| packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.css | Adds an error border style for the invalid container state. |
| packages/quantic/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/tests/quanticGeneratedAnswerFollowUpInput.test.js | Adds mocks and a full validation/counter test suite for the new limit behavior. |
| packages/quantic/force-app/main/default/labels/CustomLabels.labels-meta.xml | Adds the new EN label quantic_FollowUpInputTooLong. |
| packages/quantic/force-app/main/translations/fr.translation-meta.xml | Adds FR translation for quantic_FollowUpInputTooLong. |
| packages/quantic/force-app/main/translations/es.translation-meta.xml | Adds ES translation for quantic_FollowUpInputTooLong. |
| .changeset/gold-views-talk.md | Declares a patch changeset for @coveo/quantic. |
| this.refs.askFollowUpInput.value.trim() === '' | ||
| ) { | ||
| return; | ||
| } | ||
| this.sendSubmitFollowUpEvent(); |
| get characterCounterText() { | ||
| return `${this.characterCount} / ${MAX_FOLLOW_UP_QUESTION_LENGTH}`; | ||
| } |
| /** | ||
| * Maximum number of characters allowed in a follow-up question. | ||
| */ | ||
| const MAX_FOLLOW_UP_QUESTION_LENGTH = 300; |
There was a problem hiding this comment.
yeah I would remove the comment its already pretty obvious
@coveo/atomic
@coveo/atomic-hosted-page
@coveo/atomic-legacy
@coveo/atomic-react
@coveo/auth
@coveo/bueno
@coveo/create-atomic
@coveo/create-atomic-component
@coveo/create-atomic-component-project
@coveo/create-atomic-result-component
@coveo/create-atomic-rollup-plugin
@coveo/headless
@coveo/headless-react
@coveo/shopify
commit: |
| } | ||
|
|
||
| get textareaAriaInvalid() { | ||
| return this.isOverCharacterLimit ? 'true' : 'false'; |
There was a problem hiding this comment.
Here im pretty sure you can just do:
return !!this.isOverCharacterLimit;
aria-invalid resolves true and false to "true" / "false"
There was a problem hiding this comment.
As a matter of fact you need to do this instead:
| return this.isOverCharacterLimit ? 'true' : 'false'; | |
| return this.isOverCharacterLimit ? 'true' : null; |
returning 'false' will indeed still display aria-invalid="false" which I think would resolved as true? Or maybe there's LWC magic there?
erocheleau
left a comment
There was a problem hiding this comment.
Looks good, 1 nitpick
| const element = createTestComponent(); | ||
| await flushPromises(); | ||
|
|
||
| const textarea = element.shadowRoot.querySelector(selectors.textarea); | ||
| textarea.value = 'hello'; | ||
| textarea.dispatchEvent(new CustomEvent('input')); | ||
| await flushPromises(); | ||
|
|
||
| const counter = element.shadowRoot.querySelector(selectors.counter); | ||
| expect(counter.textContent.trim()).toBe('5 / 300'); |
There was a problem hiding this comment.
Nitpick
| const element = createTestComponent(); | |
| await flushPromises(); | |
| const textarea = element.shadowRoot.querySelector(selectors.textarea); | |
| textarea.value = 'hello'; | |
| textarea.dispatchEvent(new CustomEvent('input')); | |
| await flushPromises(); | |
| const counter = element.shadowRoot.querySelector(selectors.counter); | |
| expect(counter.textContent.trim()).toBe('5 / 300'); | |
| const element = createTestComponent(); | |
| await flushPromises(); | |
| const textTest = 'hello'; | |
| const textarea = element.shadowRoot.querySelector(selectors.textarea); | |
| textarea.value = textTest; | |
| textarea.dispatchEvent(new CustomEvent('input')); | |
| await flushPromises(); | |
| const counter = element.shadowRoot.querySelector(selectors.counter); | |
| expect(counter.textContent.trim()).toBe(`${textTest.length} / 300`); |
| } | ||
|
|
||
| get textareaAriaInvalid() { | ||
| return this.isOverCharacterLimit ? 'true' : 'false'; |
There was a problem hiding this comment.
As a matter of fact you need to do this instead:
| return this.isOverCharacterLimit ? 'true' : 'false'; | |
| return this.isOverCharacterLimit ? 'true' : null; |
returning 'false' will indeed still display aria-invalid="false" which I think would resolved as true? Or maybe there's LWC magic there?
| border-color: var(--lwc-brandAccessible, #0176d3); | ||
| } | ||
|
|
||
| .follow-up-input__container--error { |
There was a problem hiding this comment.
This isn't more specific than .follow-up-input__container--focused so both rules will apply, so you could also add this:
| .follow-up-input__container--error { | |
| .follow-up-input__container--error, | |
| .follow-up-input__container--focused.follow-up-input__container--error { |
That way you're sure that even when focused it will also display the error color
| bubbles: true, | ||
| composed: true, | ||
| detail: { | ||
| value: this.refs.askFollowUpInput.value, |
There was a problem hiding this comment.
This value is emitted but not trimmed, is that what we want?
We trim it to count for the character limit, but we can submit with a value untrimmed?
| } | ||
|
|
||
| handleInput() { | ||
| this.characterCount = this.refs.askFollowUpInput.value.trim().length; |
There was a problem hiding this comment.
Why exactly are we trimming the input? It feels a little bit weird, because for example it would allow
hel lo
but not
hello
and at the end of the day, we send the value of the input filled untrimmed, so we will submit with
hello untrimmed.
SFINT-6844
Summary
Adds a 300-character limit to QuanticGeneratedAnswerFollowUpInput component, with a live character counter, an error state, and a localized validation message. Submission is blocked while the question exceeds the limit.
Changes
Demo
Screen.Recording.2026-06-30.at.10.01.04.AM.mov