Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
314 changes: 252 additions & 62 deletions dev/messages-ai-chat.html

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions dev/playground/messages.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Messages</title>
<script type="module" src="../common.js"></script>

<script type="module">
import '@vaadin/checkbox-group';
import '@vaadin/message-input';
import '@vaadin/message-list';
import '@vaadin/tooltip';
import '@vaadin/component-base/src/styles/user-colors.js';

const list = document.querySelector('vaadin-message-list');
list.items = [
{
text: 'Nature does not hurry, yet everything gets accomplished.',
time: 'yesterday',
userName: 'Matt Mambo',
userColorIndex: 1,
theme: 'self',
},
{
text: "I've reviewed your Q3 financial documents. Here's a summary:\n\n**Key Findings:**\n- Revenue increased 12% compared to Q2\n- Operating costs remained stable\n- The chart shows positive growth trends\n\n**Concerns:**\n- Marketing spend is 15% over budget\n- Cash flow projections need revision\n\nWould you like me to elaborate on any of these points?",
time: 'right now',
userName: 'Assistant',
theme: 'full-width',
},
{
text: 'Using your talent, hobby or profession in a way that makes you contribute with something good to this world is truly the way to go.',
time: 'right now',
userName: 'Linsey Listy',
userColorIndex: 2,
},
];

const input = document.querySelector('vaadin-message-input');
input.addEventListener('submit', (e) => {
const message = e.detail.value;
list.items = [
...list.items,
{
text: message,
time: 'now',
userName: 'You',
userColorIndex: 0,
},
];
});

document.querySelector('#variant').addEventListener('change', function () {
list.setAttribute('theme', this.value.join(' '));
});
</script>
</head>

<body>
<vaadin-checkbox-group label="Variant" id="variant">
<vaadin-checkbox label="Bubble" value="bubble"></vaadin-checkbox>
<vaadin-checkbox label="One to One" value="one-to-one"></vaadin-checkbox>
</vaadin-checkbox-group>
<h2 class="heading">Default</h2>
<vaadin-message-list announce-messages markdown>
<vaadin-message slot="typing-indicator" typing user-name="Assistant"><span>Typing…</span></vaadin-message>
<vaadin-message slot="typing-indicator" typing user-name="Assistant" theme="ellipsis"><span>Typing…</span></vaadin-message>
<vaadin-message slot="typing-indicator" typing user-name="Assistant" theme="text"><span>Typing…</span></vaadin-message>
</vaadin-message-list>
<vaadin-message-input>
<vaadin-tooltip slot="tooltip" text="Press Enter to send"></vaadin-tooltip>
</vaadin-message-input>
<h2 class="heading">Icon-Button Variant</h2>
<vaadin-message-input theme="icon-button"></vaadin-message-input>
</body>
</html>
1 change: 1 addition & 0 deletions packages/aura/src/color.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ vaadin-drawer-toggle,
vaadin-dialog,
vaadin-menu-bar-button,
vaadin-menu-bar-item,
vaadin-message,
vaadin-message::part(attachment),
vaadin-notification-container,
vaadin-notification-card,
Expand Down
26 changes: 26 additions & 0 deletions packages/aura/src/components/message-list.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
--vaadin-message-attachment-border-width: 1px;
}

vaadin-message:not([theme~='full-width']) {
--aura-accent-color-light: var(--vaadin-user-color);
--aura-accent-color-dark: var(--vaadin-user-color);
}

vaadin-message::part(attachment) {
--aura-surface-level: 4;
background: var(--vaadin-message-attachment-background, var(--aura-surface-color) padding-box);
Expand All @@ -21,3 +26,24 @@ vaadin-message::part(attachment-icon) {
background: color-mix(in srgb, var(--aura-accent-color) 10%, transparent);
color: var(--aura-accent-color);
}

vaadin-message-list[theme~='bubble'] > vaadin-message:not([theme~='full-width'], [typing][theme~='text']) {
--aura-surface-level: 3;
--aura-surface-opacity: 0.6;

&::part(content) {
background: var(--aura-surface-color) padding-box;
border: 1px solid var(--vaadin-border-color-secondary);
box-shadow: var(--aura-shadow-xs);
}

&[theme~='self'] {
--aura-surface-level: 2;
--aura-surface-opacity: 1;

&::part(content) {
background: var(--aura-accent-surface) padding-box;
border-color: var(--aura-accent-border-color);
}
}
}
1 change: 1 addition & 0 deletions packages/aura/src/surface.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ vaadin-grid,
vaadin-grid-pro,
vaadin-master-detail-layout::part(detail),
vaadin-menu-bar-button,
vaadin-message,
vaadin-message::part(attachment),
vaadin-message-input,
vaadin-radio-button::part(radio),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,36 @@ export const messageInputStyles = css`
:host {
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
max-height: 50vh;
flex-shrink: 0;
border: var(--vaadin-input-field-border-width, 1px) solid
var(--vaadin-input-field-border-color, var(--vaadin-border-color));
border-radius: var(--vaadin-input-field-border-radius, var(--vaadin-radius-m));
background: var(--vaadin-input-field-background, var(--vaadin-background-color));
padding: var(
--vaadin-input-field-padding,
var(--vaadin-padding-block-container) var(--vaadin-padding-inline-container)
);
gap: var(--vaadin-input-field-gap, var(--vaadin-gap-s));
}

:host([hidden]) {
display: none !important;
}

:host(:focus-within) {
:host(:has(textarea:focus)) {
outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
outline-offset: calc(var(--vaadin-input-field-border-width, 1px) * -1);
}

@scope {
:scope:has(textarea:focus) {
outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
outline-offset: calc(var(--vaadin-input-field-border-width, 1px) * -1);
}
}

:host([disabled]) {
--vaadin-input-field-value-color: var(--vaadin-input-field-disabled-text-color, var(--vaadin-text-color-disabled));
--vaadin-input-field-background: var(
Expand All @@ -38,8 +51,20 @@ export const messageInputStyles = css`

::slotted([slot='textarea']) {
flex: 1;
--vaadin-input-field-border-width: 0 !important;
--vaadin-input-field-padding: 0px !important;
--vaadin-input-field-border-width: 0px !important;
--vaadin-focus-ring-width: 0;
--vaadin-input-field-background: transparent !important;
--vaadin-input-field-disabled-background: transparent !important;
--vaadin-input-field-border-radius: 0px !important;
align-self: center;
}

::slotted([slot='button']) {
margin-inline-start: auto;
}

slot:is([name='header'], [name='footer'])::slotted(*) {
width: 100%;
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@ export const messageInputButtonStyles = css`
:host {
flex: none;
align-self: end;
margin: var(
--vaadin-input-field-padding,
var(--vaadin-padding-block-container) var(--vaadin-padding-inline-container)
);
--vaadin-button-border-width: 0;
--vaadin-button-background: transparent;
--vaadin-button-text-color: var(--vaadin-text-color);
--vaadin-button-text-color: var(--vaadin-text-color-secondary);
--vaadin-button-padding: 0;
--vaadin-button-border-radius: var(--vaadin-radius-s);
}

:host(:is([focus-ring], :focus-visible)) {
outline-offset: 0.125em;
}

:host(:hover) {
--vaadin-button-text-color: var(--vaadin-text-color);
}
`;
8 changes: 7 additions & 1 deletion packages/message-input/src/vaadin-message-input-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ export const MessageInputMixin = (superClass) =>

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

}

focus(options) {
Expand All @@ -136,7 +138,11 @@ export const MessageInputMixin = (superClass) =>
__buttonPropsChanged(button, disabled, effectiveI18n, value) {
if (button) {
button.disabled = disabled || !value;
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

button.setAttribute('aria-label', effectiveI18n.send);
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions packages/message-input/src/vaadin-message-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,16 @@ class MessageInput extends MessageInputMixin(
/** @protected */
render() {
return html`
<slot name="header"></slot>

<slot name="prefix"></slot>

<slot name="textarea"></slot>

<slot name="button"></slot>

<slot name="footer"></slot>

<slot name="tooltip"></slot>
`;
}
Expand Down
97 changes: 97 additions & 0 deletions packages/message-list/src/styles/vaadin-message-base-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@
import '@vaadin/component-base/src/styles/style-props.js';
import { css } from 'lit';

CSS.registerProperty({
name: '--_vaadin-message-typing-mask-pos',
syntax: '<length-percentage>',
inherits: false,
initialValue: 0,
});

export const messageStyles = css`
:host {
display: flex;
flex-direction: row;
box-sizing: border-box;
padding: var(--vaadin-message-padding, var(--vaadin-padding-s) var(--vaadin-padding-m));
gap: var(--vaadin-message-gap, var(--vaadin-gap-xs) var(--vaadin-gap-s));
outline-offset: calc(var(--vaadin-focus-ring-width) * -1);
Expand All @@ -29,6 +37,9 @@ export const messageStyles = css`
flex-direction: column;
flex-grow: 1;
gap: inherit;
background: var(--vaadin-message-content-background, transparent);
padding: var(--vaadin-message-content-padding, 0);
border-radius: var(--vaadin-message-content-border-radius, 0);
}

[part='header'] {
Expand All @@ -41,6 +52,7 @@ export const messageStyles = css`
}

[part='name'] {
display: var(--vaadin-message-name-display, inline);
font-size: var(--vaadin-message-name-font-size, inherit);
font-weight: var(--vaadin-message-name-font-weight, 500);
color: var(--vaadin-message-name-color, var(--vaadin-text-color));
Expand All @@ -62,6 +74,12 @@ export const messageStyles = css`

::slotted([slot='avatar']) {
flex: none;
visibility: var(--vaadin-message-avatar-visibility, visible);
display: var(--vaadin-message-avatar-display);
}

::slotted(vaadin-avatar-group) {
width: fit-content;
}

::slotted(vaadin-markdown) {
Expand All @@ -73,6 +91,7 @@ export const messageStyles = css`
flex-wrap: wrap;
gap: var(--vaadin-gap-s);
padding-bottom: var(--vaadin-gap-xs);
justify-content: var(--vaadin-message-attachments-alignment, start);
}

[part~='attachment'] {
Expand Down Expand Up @@ -132,4 +151,82 @@ export const messageStyles = css`
padding: var(--vaadin-message-attachment-padding, var(--vaadin-padding-s));
padding-inline-start: 0;
}

:host([typing]:not([theme~='text'])) [part='message'],
:host([typing][theme~='text']) [part='content'] {
mask-image: linear-gradient(
90deg,
hsla(0, 0%, 0%, 0.4) calc(var(--_vaadin-message-typing-mask-pos) - max(60px, 60%)),
hsl(0, 0%, 0%) calc(var(--_vaadin-message-typing-mask-pos) - max(40px, 40%)),
hsl(0, 0%, 0%),
calc(var(--_vaadin-message-typing-mask-pos) - max(20px, 20%)),
hsla(0, 0%, 0%, 0.4) var(--_vaadin-message-typing-mask-pos)
);
animation: --_vaadin-message-typing-slide 1.5s ease-in-out infinite;
width: fit-content !important;
color: var(--vaadin-text-color) !important;

[part='time'] {
display: none;
}

[part='header'] {
display: contents;
}
}

@keyframes --_vaadin-message-typing-slide {
100% {
--_vaadin-message-typing-mask-pos: calc(100% + max(60px, 60%));
}
}

:host([typing][theme~='ellipsis']) {
[part='message'] {
line-height: inherit !important;
color: var(--vaadin-text-color-secondary) !important;
}

[part='message']::before {
content: '';
display: block;
width: var(--vaadin-icon-size, 1lh);
height: var(--vaadin-icon-size, 1lh);
mask: var(--_vaadin-icon-ellipsis);
background: currentColor;
}

[part='header'],
[part='message'] slot {
display: none;
}

[part='content'] {
align-self: center;
}
}

:host([typing][theme~='text']) {
display: block;

slot[name='avatar'] {
display: none;
}

[part='content'] {
display: block;
}

[part='header'],
[part='name'],
[part='message'] {
display: contents;
color: inherit;
font-size: inherit;
}

[part='message'] {
text-transform: lowercase;
}
}
`;
Loading
Loading