Skip to content

Design for message-list typing indication - #12418

Draft
jouni wants to merge 1 commit into
mainfrom
design/message-list-and-input
Draft

Design for message-list typing indication#12418
jouni wants to merge 1 commit into
mainfrom
design/message-list-and-input

Conversation

@jouni

@jouni jouni commented Aug 11, 2026

Copy link
Copy Markdown
Member

Includes a bunch of unrelated fixes and improvements to message-list and message-input, vaadin-message-input-button, as well as upload-file-list.

Opening this PR so that I can get Claude and the team to point out all the flaws and errors :)
The changes should be picked individually where possible.

Changes

Message List

  • --vaadin-message-list-max-width custom property, for restricting the width of the messages in the list and centering them in the scrolling viewport.
    • Alternatively, we might add a --vaadin-message-max-width property instead. Should consider which one makes more sense to devs.
  • footer slot: sticky part where you can place non-message content. Shares the same max-width as the messages. The idea is that you can put the Message Input component here, so that it’s within the same scrolling container. I’m not 100% sure about this, though, and perhaps this should be left for application code, as we would need to add a resize observer that measures the footer content height and adjusts the list scroll-margin so that messages are scrolled into view when navigating with the keyboard. Both Claude and ChatGPT have this behavior, that the scrolling region covers the input area as well.
  • typing slot for a message that’s always the last one in the list, where a "user is typing" indication is shown.
  • bubble theme variant, which adds a visual container around the message content, and has a max-width that is less than 100%.
  • one-to-one theme variant: works together with the bubble variant, and hides the avatars and names of both participants in the message list (expecting the context, e.g. a header or a menu item, to indicate who you are talking with).
  • userTyping JS property: an array of users who are currently typing a message. When set, a message is added to the typing slot in the DOM, with an avatar-group indicating all the users currently typing a message.
  • typingMessageText JS property: a string which is appended after the list of users' names who are currently typing a message.
  • typingMessageTheme JS property: a string that sets the theme attribute on the message in the typing slot (e.g., set to full-width in a one-to-one discussion with an AI assistant).
  • Bugfix: scroll to the last message after the markdown component has finished upgrading

Message

Two theme variants meant to be used together with the message list bubble variant:

  • self: indicating the messages of the current user. These messages use a different background color and are aligned to the other side of the message list, and always hide the avatar and name.
  • full-width theme variant: hides the bubble background and allows the message to take full width of the list. Meant to be used for "assistant"/AI responses.
  • --vaadin-user-color custom property set on the host, to allow custom styling based on the colorIndex property.
  • typing-ellipsis theme variant: hides the text from the message in the typing slot, and shows an animated ellipsis instead.

Message Input

New slots for additional content:

  • prefix: before the input/textarea
  • header: above the prefix, input, and submit button
  • footer: below the prefix, input, and submit button
  • click listener on the host that focuses the input/textarea
  • Fix/enhancement: don't override the content of a custom submit button, allowing to use a custom icon button, for example.

Upload File List

  • empty attribute applied when the list is empty, allowing styles to hide the element in that case, so that layouts can avoid adding gaps when not needed (which is the case when placing the upload-file-list in the message-input header slot).

@vaadin-review-bot vaadin-review-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the changes — left 8 comments.

Finding
⚠️ The usersTyping JSDoc documents userName/userColorIndex, but the code reads user.name and feeds objects to vaadin-avatar-group.items, which expects name/colorIndex.
⚠️ The new host click listener runs this._textArea.focus() for any click anywhere in the component, including the new slotted controls.
⚠️ For a custom icon-only slot="button", the observer overwrites any developer-provided aria-label with i18n.send and never removes it.
🧹 New public properties usersTyping, typingMessageText, typingMessageTheme are missing from the sibling .d.ts, and the two string props lack @attr tags.
🧹 The new __colorIndexChanged observer sets --vaadin-user-color on the message, which _renderMessages already writes inline in the list template.
👀 .typing="${item.typing}" binds to a typing property that vaadin-message does not define, so the binding does nothing.
👀 @keyframes typing and @keyframes typing2 are defined but never referenced by any animation declaration.
⚠️ typingMessageText is baked into the typing element only at creation and has no observer, so later changes are ignored.

Comment thread packages/message-list/src/vaadin-message-list-mixin.js
this._tooltipController = new TooltipController(this);
this.addController(this._tooltipController);

this.addEventListener('click', () => this._textArea.focus());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ The new host click listener runs this._textArea.focus() for any click anywhere in the component, including the new slotted controls.

This PR adds header, prefix, footer, and custom button slots that hold interactive controls (in the dev demo: a paperclip upload button in prefix, an upload-file-list in header, prompt buttons in footer). Clicking any of them bubbles to the host and immediately pulls focus into the textarea, so the control the user just activated loses focus.

This hurts keyboard and screen-reader users and is generically wrong — the intent (focus the textarea when clicking the input's own padding) should be limited to the host itself, not the whole slotted subtree.

vaadin-message-input-mixin.js:128 · correctness · confirmed

button.textContent = effectiveI18n.send;
if (button.localName === 'vaadin-message-input-button') {
button.textContent = effectiveI18n.send;
} else if (button.textContent.trim().length === 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ For a custom icon-only slot="button", the observer overwrites any developer-provided aria-label with i18n.send and never removes it.

A developer supplies <vaadin-button slot="button" aria-label="Send message"><vaadin-icon .../></vaadin-button>. Because button.textContent.trim().length === 0, __buttonPropsChanged overwrites the intended label with the generic "Send". In the dev demo the button has a "Send prompt" tooltip, so its accessible name becomes the mismatched "Send".

The branch also never clears the label: a button that starts icon-only (gets aria-label="Send") and later gains visible text keeps a stale aria-label, so screen readers announce "Send" instead of the visible text. Check for an existing label before setting one, and clear it when text is present.

vaadin-message-input-mixin.js:143 · correctness · confirmed

* }>
* ```
*/
usersTyping: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 New public properties usersTyping, typingMessageText, typingMessageTheme are missing from the sibling .d.ts, and the two string props lack @attr tags.

CONVENTIONS.md line 143: "When adding a public property, method, or config-object option to a .js module, it must also be declared in the sibling hand-maintained .d.ts." vaadin-message-list-mixin.d.ts still declares only items, markdown, and announceMessages, so TypeScript users cannot type-check the three new properties.

CONVENTIONS.md line 35 also requires camelCase simple-type props to carry an @attr {type} kebab-name tag; typingMessageText and typingMessageTheme (both String) have none. Add the .d.ts declarations and @attr {string} typing-message-text / typing-message-theme tags.

vaadin-message-list-mixin.js:81 · conventions · confirmed

}
}

/** @private */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 The new __colorIndexChanged observer sets --vaadin-user-color on the message, which _renderMessages already writes inline in the list template.

vaadin-message-list-mixin.js line 258 already emits style="--vaadin-user-color: var(--vaadin-user-color-${item.userColorIndex});..." on the same <vaadin-message>, so the new observer duplicates that write on every list-rendered message — the only supported render path.

The two sources target the same inline style and must be kept in sync by future edits, or the logic silently diverges. Keep one mechanism; the observer is redundant for list-rendered messages.

vaadin-message-mixin.js:119 · reuse · confirmed

Comment thread packages/message-list/src/vaadin-message-list-mixin.js Outdated
Comment thread packages/message-list/src/styles/vaadin-message-base-styles.js Outdated
avatars.maxItemsVisible = 100;
avatars.slot = 'avatar';
const typingText = document.createElement('div');
typingText.innerHTML = `<span></span> ${this.typingMessageText}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ typingMessageText is baked into the typing element only at creation and has no observer, so later changes are ignored.

The label text is set once via typingText.innerHTML = \ ${this.typingMessageText}`when the element is first created. The update branch (line 226) only rewrites the inner(the user-name list), never the trailing label, andtypingMessageText` has no observer to re-render.

So setting typingMessageText after the first typing indicator has rendered silently keeps the original text. Confirming needs a run where typingMessageText is changed between two usersTyping updates.

vaadin-message-list-mixin.js:214 · correctness · plausible

@jouni
jouni force-pushed the design/message-list-and-input branch 2 times, most recently from b1ea1ce to 41dca05 Compare August 12, 2026 09:12
@jouni
jouni force-pushed the design/message-list-and-input branch from 41dca05 to d006c5d Compare August 12, 2026 09:14
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
D Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants