聊天输入栏
让用户写一条消息并把它发出去,一切都收在一个胶囊里。随所写内容增高的输入区、附加到消息上的文件、你自己的操作,以及只在消息可以发送时才启用的发送按钮。它只报告消息,自己绝不发送任何东西。
- 类别
- 文本输入
- 令牌
comp.chat-composer- 层级
- 功能
- Web
tanrei-chat-composerschedule - SwiftUI
TanreiChatComposerradio_button_unchecked - Jetpack Compose
TanreiChatComposerradio_button_unchecked - Flutter
TanreiChatComposerradio_button_unchecked - React Native
ChatComposerradio_button_unchecked
生成自 docs/status/status.json, specs/chat-composer.spec.json
两种布局的聊天输入栏。写一条消息、附加一个文件,然后发送。不会发送到任何地方。
聊天输入栏是写消息的那一个地方:在一段对话里、在某项工作下方的讨论串里,或者在求助请求里。它画成一个胶囊。胶囊里有输入区、附件、你自己的操作和发送按钮。
组成部分是一个嵌入式文本输入框、一个文件选择器、若干文件项和若干按钮。嵌入式文本输入框是键入的部分。文件选择器打开平台的文件选择器,并判断选中的内容。一个文件项就是一个附件。按钮包括发送按钮、停止按钮、附加按钮和你自己的按钮。聊天输入栏一样都不绘制。它添加的是把它们联系在一起的东西:胶囊、消息能否发送、它保留的文件、计数器、一个实时区域,以及表单的值。
它不负责发送
聊天输入栏不打开任何连接。用户发送时,它触发带有文本和附件的 send,发送由你的产品来做。消息有没有送达,要由你来了解,也由你来说明。
以下情况请改用别的组件:
在 Web 上它是 <tanrei-chat-composer>。其他平台尚未实现。
<tanrei-chat-composer label="消息" placeholder="输入消息" send-label="发送"
attachments="own" attach-label="添加附件" remove-label="移除 {name}">
</tanrei-chat-composer>composer.addEventListener('send', (event) => {
const { value, attachments } = event.detail;
post(value, attachments);
});import { ChatComposer } from '@tanrei/react';
<ChatComposer
label="消息"
placeholder="输入消息"
sendLabel="发送"
attachments="own"
attachLabel="添加附件"
removeLabel="移除 {name}"
onSend={(event) => post(event.detail.value, event.detail.attachments)}
/>除非你设置了 keepAfterSend,否则输入栏在交出消息的同时清空自己,焦点回到输入区。
设置 attachments="given" 后,文件项由你的产品提供。如果你在文件一附加上就发送它,并且希望每一项都显示自己的状态和进度,就用这种方式。在 select 时添加一个 <tanrei-file-item> 子元素,在这一项的 remove 时把它移除。只要有一项正在上传,或者失败了但可以重试,就什么都不能发送。
<tanrei-chat-composer label="消息" send-label="发送" attachments="given"
attach-label="添加附件" remove-label="移除 {name}" retry-label="重试 {name}">
<tanrei-file-item value="a" name="brief.pdf" size-text="820 KB" status="uploading" progress="0.4"></tanrei-file-item>
</tanrei-chat-composer>胶囊是聊天输入栏自己的部件,不能单独拿出来用。在下一层,各个成员由你自己放置,方框也由你来绘制。
<div class="my-composer" role="group" aria-label="消息">
<div class="row">
<tanrei-file-picker multiple>
<tanrei-button slot="trigger" variant="text" label="添加附件"><span slot="icon">attach_file</span></tanrei-button>
</tanrei-file-picker>
<tanrei-embedded-text-input label="消息" multiline max-rows="6" enter-key="action"></tanrei-embedded-text-input>
<tanrei-button label="发送" disabled><span slot="icon">send</span></tanrei-button>
</div>
</div>各个成员带来的是:高度会增长的键入部分、按键、每个文件项,以及选择器。聊天输入栏添加的那些事,这时就由你的代码来做:绘制方框并在方框上显示焦点,判断消息能否发送,把文本的长度与上限作比较,接收拖放和粘贴,保留文件,并在发送之后清空一切。
| 情况 | inline(默认) |
stacked |
|---|---|---|
| 一行文本 | 一行:附加按钮、前置操作、输入区、计数器、后置操作、发送按钮 | 输入区和发送按钮在同一行。其余的一切都放在下面的一行里 |
| 多行 | 每个按钮都留在底边 | 发送按钮留在输入区那一行的底边 |
| 适用于 | 简短的消息、狭窄的面板 | 操作很多、消息分段落来写 |
宽度小于 comp.chat-composer.layout.inline-min-width 的胶囊,无论 layout 设成什么,都按 stacked 布局。下面那一行没有东西可放的 stacked 输入栏,画成 inline 的样子。行数绝不会改变布局。
只有以下各项全部成立时,发送按钮才启用。每有一项不成立,sendablechange 就带上一个以名称表示的原因。
| 原因 | 以下情况下消息不能发送 |
|---|---|
disabled |
disabled 为 true |
sending |
sending 为 true |
blocked |
blocked 为 true |
empty |
没有任何非空白字符,也没有附件 |
length |
文本比 maxLength 更长 |
uploading |
有文件项的状态是 uploading |
failed |
有文件项失败了,但可以重试 |
Enter 发送,Shift+Enter 另起一行。设置 sendKey="modifier" 后,Enter 另起一行。无论 sendKey 取哪个值,Ctrl+Enter 和 Command+Enter 都会发送。输入法的 Enter 绝不发送。
设计系统为每个成员设置的样式,适用于使用它的每一个地方。要单独调整某一个输入栏,就在那个元素上设置令牌。胶囊读取它自己的令牌,成员的令牌则会传到它里面的那个成员。
<tanrei-chat-composer style="--tanrei-comp-chat-composer-container-shape: 12px" …></tanrei-chat-composer>构成
| 部件 | 说明 |
|---|---|
1 container | The capsule: the one container the chat composer draws, behind everything it holds — its fill, its edge and its corners. The chat composer's own part. It carries the state answer: its edge is the focus indication of the typing area inside it, and it is drawn in its error condition and as receiving while files are dragged over it. |
2 strip · 可选 | The attachments, one File item each, at the top of the capsule: an arrangement of the chat composer's own — where the items stand and how they wrap — holding File items that are drawn from their own tokens. |
3 input | The typing area: the embedded text input — typing alone, with no container, edge or focus indication of its own — growing with its lines up to maxRows and scrolling inside itself beyond. A member, drawn from its own tokens. |
4 attach-button · 可选 | The File picker's trigger: an icon-only Button the chat composer brings, or the product's own in the attach slot. Shown while attachments is not none. |
5 leading-actions · 可选 | The product's Buttons in the leading slot: before the typing area in the inline layout, and at the start of the row beneath it in the stacked one, after the attach button: laid out, never restyled. |
6 trailing-actions · 可选 | The product's Buttons in the trailing slot: before the send button in the inline layout, and at the end of the row beneath the typing area in the stacked one: laid out, never restyled. |
7 counter · 可选 | The number of characters, and the limit, before the trailing actions. The chat composer's own part. |
8 send-button | The Button that sends, on the typing area's line in both layouts — at its end, and at its bottom edge once the text has more lines than one: one the chat composer brings, or the product's own in the send slot; while a stoppable sending is under way, the stop button in its place. A member, drawn from the Button's own tokens. |
9 drop-indicator · 可选 | The capsule's own fill and edge while files are dragged over it: the receiving condition of the container, not a second shape. It takes no layout space. |
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | string | "" | The text. — Embedded text input The chat composer shows what the product sets and reports what a person types with input; it never trims, shortens or rewrites it. |
label | string | "" | The accessible name. Required: the embedded text input draws no label and asks no Field for one, so this is the only name it has. Its holder gives it — a composed component from its own label, a product from the words that stand beside its box. Never drawn. — Embedded text input Also the name of the chat composer as a group. It is not drawn: a composer has a name and no label row. Required, unless a Field around the chat composer names it (CCM-28). |
placeholder | string | "" | A hint drawn while the value is empty. Never the name. — Embedded text input |
layout | inline | stacked | "inline" | Where the actions stand in the capsule. inline, the default: on the typing area's line — the attach button and the leading actions before it, the counter, the trailing actions and the send button after it — and at the bottom of the capsule once the text has more lines than one. stacked: the typing area and the send button on one line — the send button at its end, and at the bottom edge of the typing area once the text has more lines than one — and everything else in one row beneath them, inside the capsule: the attach button and the leading actions at its start, the counter and the trailing actions at its end — for a composer with many actions, and for messages written in paragraphs. A capsule too narrow for inline is laid out stacked whatever this says; the number of lines never changes the layout (CCM-4). |
minRows | number | 1 | The fewest lines a multi-line input is tall. 1 by default — a holder's typing part begins as one line and grows — where the Text input, which is a box, begins at 2. — Embedded text input One line by default, at which the capsule is fully rounded. |
maxRows | number | 6 | The most lines the typing area grows to before it scrolls inside itself. Given to the embedded text input as its maxRows, with a default of the chat composer's own: six lines, where the embedded text input alone has no limit. 0 sets no limit, which a composer at the bottom of a screen should not choose. |
maxLength | number | 0 | The most user-perceived characters a message may hold; 0 is no limit. It refuses no edit and shortens no paste: text beyond it stays, is marked, and stops sending until the person has cut it (CCM-18). |
counter | boolean | false | Draws the number of characters, and maxLength where one is set, before the trailing actions. |
sendKey | enter | modifier | "enter" | What Enter does on a hardware keyboard. enter: Enter sends and Shift+Enter begins a new line — for short messages sent often. modifier: Enter begins a new line and Ctrl+Enter or Command+Enter sends — for long messages written in paragraphs. Ctrl+Enter and Command+Enter send under both; the return key of an on-screen keyboard begins a new line under both (CCM-12). |
attachments | none | own | given | "none" | Whether files can be attached, and who holds them. none: text alone; no attach button, and files dropped or pasted are not taken. own: the chat composer keeps the files a person attached and draws a File item for each, and send carries them — nothing written by the product until then. given: the product gives the File items and the chat composer lays them out and keeps no file — for a product that begins sending a file the moment it is attached and reports each one's progress. |
attachmentForm | auto | tile | row | "auto" | The form of the File items the chat composer draws itself, while attachments is own. auto: a tile with its picture for a file the platform can draw as an image, and a row — glyph, name and state on a line of the strip — for every other file. tile: every file a tile, a file that is no image by the glyph of its format and its name. row: every file a row, and no picture is drawn. A file the chat composer refused is a row under every value (CCM-44). While attachments is given the product sets each File item's own form, and this is not read. |
multiple | boolean | true | Whether more than one file can be attached. Given to the file picker as its multiple; the default is true, where a file picker alone offers one. |
accept | strings | | The kinds of file that are taken: file name extensions with their dot (".pdf") and media types, a whole family as "image/*". The picker is asked to offer these, and a file of another kind — dropped, or chosen in a picker that did not filter — is refused (reject, reason type). Empty takes every kind. It enforces the limit; constraintsText says it. — File picker |
maxSize | number | null | The largest size of one file, in bytes. A larger file is refused (reject, reason size). Null sets no limit. It is judged from the size the platform reports; the file is not read. — File picker |
maxFiles | number | null | The most files one choice or one drop may bring, while multiple is true. Files beyond it are refused (reject, reason count). Null sets no limit; ignored while multiple is false. The file picker counts nothing across choices: a product that keeps a list sets this to the room that is left — zero once the list is full, which refuses every file — or judges the room itself. — File picker The most files one message may carry while multiple is true; the room that is left is what the file picker is given (CCM-22). |
sending | boolean | false | What was sent is on its way, or its answer is still arriving: the product's to set and to clear. While it is true nothing more can be sent; the typing area stays open, so the next message can be written. |
stoppable | boolean | false | While sending is true, puts a stop button in the send button's place, which asks the product to stop what is under way (stop) — for an answer that arrives over time and can be cut short. Ignored while sending is false. |
blocked | boolean | false | The product's own reason that nothing can be sent now — no connection, a limit on how often, a check of its own. The send button is disabled and the send key sends nothing; the product says why in its own words, around the composer or with announcement. |
keepAfterSend | boolean | false | Keeps the text and the attachments after send, for a product that clears them itself once it knows the message arrived. False: the chat composer empties itself as it hands the message over (CCM-15). |
disabled | boolean | false | Nothing can be written, attached, removed or sent: every member and every Button in a slot is disabled. Inside a Field, disable the Field. |
error | boolean | false | Draws the capsule in its error condition and exposes the typing area as invalid. A Field sets it with its own error; the words are the Field's errorText. It does not stop sending by itself: blocked does. |
sendLabel | string | "" | The accessible name of the send button the chat composer brings ("Send"). Empty reads the product's words; with neither, CCM-31 says what happens. |
stopLabel | string | "" | The accessible name of the stop button the chat composer brings ("Stop"). Empty reads the product's words. Needed while stoppable is used. |
attachLabel | string | "" | The accessible name of the attach button the chat composer brings ("Attach files"). Empty reads the product's words. Needed while attachments is not none. |
removeLabel | string | "" | The accessible name of the remove action: the whole name in the product's language with one place, {name}, which the file item fills with the file's name ("Remove {name}", "{name}を削除"), so that the actions of two files are told apart by their names and the product still writes one word, not one per file. Empty reads the product's words. — File item Given to every File item the chat composer draws. |
retryLabel | string | "" | The accessible name of the retry action, with the place {name} filled by the file's name as removeLabel is ("Retry {name}", "{name}を再試行"). Empty reads the product's words. — File item Given to every File item the chat composer draws. |
typeErrorText | string | "" | While attachments is own: the sentence on the File item of a file refused for its kind ("This kind of file cannot be added."). Empty reads the product's words. The key the file upload reads. |
sizeErrorText | string | "" | While attachments is own: the sentence on the File item of a file refused for its size ("This file is too large."). Empty reads the product's words. The key the file upload reads. |
countErrorText | string | "" | While attachments is own: the sentence on the File item of a file refused because no more can be added ("No more files can be added."). Empty reads the product's words. The key the file upload reads. |
announcement | string | "" | A message the product has assistive technology hear through the chat composer's one live region when it sets or changes it ("Message sent", "The message is 12 characters too long"). It is not drawn. |
插槽
| 插槽 | 说明 |
|---|---|
leading | The product's own Buttons after the attach button: before the typing area in the inline layout, at the start of the row beneath it in the stacked one: what adds to the message (a mention, an emoji, formatting, a menu of further sources). Icon-only Buttons of the text variant are the quiet form; the chat composer lays them out inside the capsule and never restyles them. |
trailing | The product's own Buttons before the send button in the inline layout, and at the end of the row beneath the typing area in the stacked one: what stands beside sending (a voice message, a time to send at). Laid out inside the capsule and never restyled. |
attach | The product's own Button in place of the attach button the chat composer brings (another glyph, a labelled Button). Exactly one; the chat composer gives it the behaviour — it opens the platform's picker — and never restyles it. Ignored while attachments is none. |
send | The product's own Button in place of the send button the chat composer brings (a labelled Button, another variant or size). Exactly one; the chat composer gives it the behaviour — it sends, and is disabled while nothing can be sent — and never restyles it. |
stop | The product's own Button in place of the stop button the chat composer brings, shown in the send button's place while sending and stoppable are true. Exactly one. |
事件
| 事件 | 说明 |
|---|---|
input | The person changed the text; carries the new value (the embedded text input's input). Emitted for every change, not when the product sets value and not when the chat composer empties itself after send. A commit when focus leaves is not reported: a composer's text is committed by sending. |
send | The person sent what the chat composer holds, by the send button or the keyboard, while it could be sent. Carries the text exactly as written and the attachments in the order of the strip: the kept files as the platform's own handles while attachments is own, the values of the File items that have not failed while it is given. The product does the sending. Unless keepAfterSend is true the chat composer is empty once the event has been emitted. |
stop | The person activated the stop button while sending and stoppable were true. The product stops what is under way and sets sending. |
sendableChange | Whether what the chat composer holds can be sent changed, or the reasons it cannot did. Carries the answer and the reasons, by name: disabled, sending, blocked, empty, length, uploading, failed. For a product that says in its own words what holds a message back, or that mirrors the state elsewhere. |
sendBlocked | The person asked to send by the keyboard while it could not be sent for a reason other than being empty or disabled. Carries the reasons. For a product that answers the attempt, with announcement or with a Field's error text. |
select | Files were chosen in the picker, dropped on the capsule or pasted, and passed accept, maxSize and the count (the file picker's select). While attachments is given the product adds a File item for each; while it is own the chat composer has already kept them. |
reject | Files of one arrival were refused (the file picker's reject), each with its reason: type, size or count. The reasons are names for the product's code, not words. |
attachmentsChange | The files the chat composer keeps changed by what a person did: an arrival that kept at least one file, or a kept file removed by its File item. Carries the files now kept, in order. Emitted only while attachments is own, and never for what the product or the chat composer itself did (files handed over, the emptying after send, a form reset). |
条目
该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。
file
One attachment, as a File item of the strip: its glyph by the file's format or its picture, its name, its size, where it stands — waiting, being sent, done or failed — and the actions to try it again and to remove it. The File item's own props and events, by the same names and with its own descriptions; fourteen of them are listed here, the ones a strip of attachments is drawn with, and a product may set any other on its items. While attachments is given the product adds it, changes it and removes it; while it is own the chat composer draws one itself for each file it keeps and for each file it refused, and the product gives none.
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | string | "" | What identifies the item to the product: given back by remove and retry, and carried by send. Unique within one chat composer. |
name | string | "" | The file's name as the person knows it ("report-2026.pdf"). The file item's name for assistive technology in every form, also where it is not drawn. |
sizeText | string | "" | The file's size as text, formatted by the product for the reader's locale ("1.2 MB"). Empty draws none. |
status | pending | uploading | done | error | "pending" | Where the file stands. pending: attached, and nothing is happening to it yet. uploading: the product is sending it; nothing can be sent until it has arrived. done: the product has it. error: it failed or was refused; errorText says why. |
statusText | string | "" | The status in the product's words ("Waiting", "Uploading", "Uploaded"), drawn last on the second line and read with the file. The mark and its color are never the only sign of a status. |
progress | number | null | How far the sending has come, from 0 to 1, while status is uploading. Null draws the bar as under way without saying how far. Ignored in every other status. |
errorText | string | "" | Why the file failed and what to do, in the product's words ("Larger than 10 MB", "The connection was lost"). Drawn in the status's place while status is error, in every form, and never cut. |
retryable | boolean | false | Whether a failed item offers the retry action. A failed item that can be tried again holds the message back until it is retried or removed; one that cannot is left out of what is sent (CCM-9, CCM-11). |
form | row | tile | "row" | How the file is shown. row: one line across the width the layout gives — the glyph or a small picture, the name, the second line, the bar, the actions at the end — for a column of files. tile: a box of a width and a height, holding the picture or the glyph, with the texts beneath it and the actions at its corner — for a strip of attached pictures. The product chooses by where the file stands. A design system may add values. |
preview | boolean | false | Whether a picture of the file is drawn in the glyph's place, where the file is an image and the product gave an address (previewSrc) or the file itself in the platform's idiom. False draws the glyph, also for an image. The picture is drawn by the platform; the file item reads no file. |
previewSrc | string | "" | The address of a picture of the file, as the platform's images take one: a small rendition the product's server made of a file it already has. With preview it is drawn for a file of any format. Empty, the picture comes from the file itself where the product gave it. |
nameHidden | boolean | false | In the tile form: the name is not drawn beneath a tile that draws a picture, for a dense strip of pictures. The name stays the file item's name for assistive technology, and is drawn all the same wherever no picture is (FIT-8). Not read in the row form. |
width | number | null | In the tile form: the width of the box, in the platform's layout unit. Null makes it as wide as it is high. |
height | number | null | In the tile form: the height of the box, in the platform's layout unit. Null reads the File item's own token for it. |
| 事件 | 说明 |
|---|---|
remove | The person asked to remove this item (the File item's remove). Carries the item's value. The product removes the item, and stops sending the file if it was. It reaches the product only while attachments is given: an item the chat composer drew for a file it keeps is removed by the chat composer, which emits attachmentsChange. |
retry | The person asked to try a failed item again (the File item's retry). Carries the item's value. The product sends the file again and sets the item's status. Only while attachments is given: an item the chat composer draws itself is never retryable. |
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 条目 | tanrei-file-item | TanreiFileItem | TanreiFileItem | TanreiFileItem | FileItem |
value · 属性 | value | id | key | key | value |
name · 属性 | name | name | name | name | name |
sizeText · 属性 | sizeText | sizeText | sizeText | sizeText | sizeText |
status · 属性 | status | status | status | status | status |
statusText · 属性 | statusText | statusText | statusText | statusText | statusText |
progress · 属性 | progress | progress | progress | progress | progress |
errorText · 属性 | errorText | errorText | errorText | errorText | errorText |
retryable · 属性 | retryable | isRetryable | retryable | retryable | retryable |
form · 属性 | form | form | form | form | form |
preview · 属性 | preview | showsPreview | showPreview | showPreview | preview |
previewSrc · 属性 | previewSrc | previewURL | previewModel | previewImage | previewSource |
nameHidden · 属性 | nameHidden | isNameHidden | nameHidden | nameHidden | nameHidden |
width · 属性 | width | width | width | width | width |
height · 属性 | height | height | height | height | height |
remove · 事件 | remove | onRemove | onRemove | onRemove | onRemove |
retry · 事件 | retry | onRetry | onRetry | onRetry | onRetry |
| 备注 | The File item's own element, written by the product as a child of the chat composer in its default slot while attachments="given"; it stays the product's light DOM and is laid out by the strip. Attributes as the File item's (size-text, status-text, error-text, preview-src, name-hidden, form; progress absent for null). remove and retry bubble and are composed, with detail.value. While attachments is own the chat composer renders the File items itself, in its shadow root, and sets each one's file property to the kept File. | A value in the attachments array, identified by id. Passed only while the product gives the File items. | A value in the attachments list, identified by key. Passed only while the product gives the File items. | A value in the attachments list, identified by id. Passed only while the product gives the File items. | An object in the attachments array, identified by id. Passed only while the product gives the File items. |
状态
行为
以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。
| ID | 需求 |
|---|---|
CCM-1 | THE chat composer SHALL draw one container of its own, the capsule (CCM-41), and SHALL lay out inside it, from top to bottom: WHILE attachments is not none and it holds at least one File item, the strip of attachments (CCM-43); and below it the typing area, the actions and the send button, as layout says (CCM-2, CCM-3). It SHALL draw nothing outside the capsule; SHALL be as wide as its layout gives it and as tall as what it holds, never less than comp.chat-composer.container.height; SHALL draw no label, no help, no error text and no requirement mark of its own: those are a Field's (CCM-28) or the product's; and SHALL stand where the product places it — it SHALL NOT fix or stick itself to an edge of the screen, cover content, or reserve room outside its own box. |
CCM-2 | WHILE it is laid out inline THE chat composer SHALL draw one row inside the capsule, in reading order: the attach button WHILE attachments is not none, the Buttons of the leading slot, the typing area, the counter WHILE it is drawn (CCM-19), the Buttons of the trailing slot, and the send button. The typing area SHALL take all the room the others leave; neighbours within the group before the typing area and within the group after it SHALL stand comp.chat-composer.actions.gap apart, and each group comp.chat-composer.container.gap from the typing area. The row SHALL stand comp.chat-composer.container.padding-block from the capsule's top edge, or comp.chat-composer.strip.offset below the strip, and comp.chat-composer.container.padding-block from its bottom edge; at each inline edge it SHALL stand comp.chat-composer.container.padding-inline-with-button from the capsule's edge WHERE a Button is the outermost part there and comp.chat-composer.container.padding-inline WHERE the typing area is. The typing area SHALL have comp.chat-composer.input.padding-block above its first line and below its last. WHILE the text has one line every Button and the counter SHALL be centred on that line; WHEN the text has more lines the row SHALL grow with the typing area and every Button and the counter SHALL stay at the bottom edge of the row, level with the last line, at the distance from the capsule's bottom edge they had with one line. |
CCM-3 | WHILE it is laid out stacked THE chat composer SHALL draw, inside the capsule, the typing area and the send button on one line, and everything else in one row beneath that line. The line SHALL stand comp.chat-composer.container.padding-block from the capsule's top edge, or comp.chat-composer.strip.offset below the strip: the typing area from comp.chat-composer.container.padding-inline inside the capsule's start edge, taking all the room the send button leaves, with comp.chat-composer.input.padding-block above its first line and below its last; and the send button — or the stop button in its place (CCM-17) — after it, comp.chat-composer.container.gap from the typing area and comp.chat-composer.container.padding-inline-with-button from the capsule's end edge. WHILE the text has one line the send button SHALL be centred on that line; WHEN the text has more lines the line SHALL grow with the typing area and the send button SHALL stay at its bottom edge, level with the last line of the text, at the distance from the row beneath that it had with one line, as the Buttons of the inline layout stay at theirs (CCM-2). The row beneath SHALL stand comp.chat-composer.container.row-gap below that line, comp.chat-composer.container.padding-inline-with-button from each inline edge and comp.chat-composer.container.padding-block from the capsule's bottom edge, and SHALL hold: at its start the attach button WHILE attachments is not none and the Buttons of the leading slot, and at its end the counter WHILE it is drawn (CCM-19) and the Buttons of the trailing slot, neighbours comp.chat-composer.actions.gap apart and centred on one line. It SHALL never hold the send button: in both layouts the send button is on the line being written. WHERE the row is too short for all it holds THE chat composer SHALL wrap the start group onto further lines, comp.chat-composer.actions.gap apart, and keep the end group together at the end of the last line; it SHALL NOT cut, shrink, overlap or hide a Button. WHILE the row would hold nothing — no attach button, no Button in either slot and no counter — THE chat composer SHALL draw no row and keep no room for one, and is then drawn as it is laid out inline. |
CCM-4 | WHILE the capsule's own inline size is less than comp.chat-composer.layout.inline-min-width THE chat composer SHALL be laid out stacked whatever layout says, and inline again WHEN it is that wide and layout is inline — by its own width, never the window's. THE chat composer SHALL NOT change between the two layouts for the number of lines the text has or for the attachments it holds: a layout that changed as the second line began would move every Button from under the person's hand and, by giving the text another width, could give it one line again. WHEN layout names a value the chat composer does not know THE chat composer SHALL behave as inline, and in debug builds report the unknown value. |
CCM-5 | THE chat composer SHALL hold exactly one embedded text input as its typing area, with multiline true, and leave it what it is: typing alone, with no container, no edge, no padding and no focus indication of its own (ETI-1, ETI-11); exactly as tall as its lines between minRows and maxRows, scrolling inside itself beyond maxRows with the caret kept in view (ETI-7); changing its height over its own motion (ETI-8); editing in the platform's own text element and reporting every change (ETI-2); given value and placeholder, named by label (ETI-13), and drawn from the embedded text input's own tokens in every state. THE chat composer SHALL give it no other look and SHALL draw nothing over it. WHEN the person presses a point of the capsule that no control covers — its padding, the room beside a short text, the gaps of the strip — THE chat composer SHALL pass the press on to the typing area by the way the embedded text input offers what holds it (ETI-12), so that the typing area has focus and the caret stands where a press on the nearest point of the text would put it, and SHALL show no pressed state (CCM-42). IF minRows is not a number of at least 1 THE chat composer SHALL use 1; IF maxRows is neither 0 nor a number of at least minRows THE chat composer SHALL use minRows. |
CCM-6 | THE chat composer SHALL have exactly one send button: the one element in the send slot — a Button of the product's, with the product's own label, glyph, variant and size, left as it is — or, WHILE that slot is empty, a Button it brings: icon-only, of the medium size and of the variant comp.chat-composer.send-button.variant names, showing the send glyph of the system's icon set through the platform's icon helper, and named by sendLabel. Either one SHALL be drawn from the Button's own tokens and SHALL answer hover, focus and a press as the Button says (BTN-5, BTN-8, BTN-9, BTN-10); the chat composer SHALL give it its behaviour (CCM-10, CCM-11) and SHALL NOT restyle it. |
CCM-7 | WHILE attachments is not none THE chat composer SHALL hold one File picker, given accept, multiple, maxSize and maxFiles, whose trigger is the one element in the attach slot — a Button of the product's, left as it is — or, WHILE that slot is empty, a Button it brings: icon-only, of the text variant and the medium size, showing the attach_file glyph of the system's icon set, and named by attachLabel. WHEN the trigger is activated THE chat composer SHALL have the file picker open the platform's own picker and judge what is chosen (FPK-6, FPK-9); the file picker draws nothing but its trigger (FPK-2). WHILE attachments is none THE chat composer SHALL hold no picker, draw no attach button and no strip, SHALL NOT draw an element in the attach slot, and in debug builds SHALL report one that is there. |
CCM-8 | THE chat composer SHALL lay out the Buttons of the leading slot and of the trailing slot in the order given, at the places CCM-2 and CCM-3 name, each at the size, the variant and the width the product gave it; SHALL add to them no name, role, state or look; and SHALL NOT read what they do — a mention, an emoji, a recording and a menu of further actions are the product's, which writes into the message by setting value and hands over files as CCM-22 says. IF a slot holds an element that is not a Button THE chat composer SHALL lay it out in its place as it is and in debug builds report it. |
CCM-9 | THE chat composer SHALL hold, as state of its own that no member holds, whether what it holds can be sent, and SHALL answer yes only WHILE all of these are true: disabled is false; sending is false; blocked is false; there is something to send — value holds at least one character that is not white space, or at least one attachment would be carried (CCM-11); WHILE maxLength is greater than 0, value is no longer than maxLength (CCM-18); no File item's status is uploading; and no File item's status is error WHILE its retryable is true. For each that is not true it SHALL hold a reason by name — disabled, sending, blocked, empty, length, uploading, failed — and the names are for the product's code, not words. WHEN the answer or the set of reasons changes, by what a person did or by what the product set, THE chat composer SHALL emit sendableChange exactly once with both; it SHALL NOT emit it when it is first presented. |
CCM-10 | WHILE what it holds cannot be sent (CCM-9) THE chat composer SHALL disable the send button as that Button's own disabled does — drawn, exposed and left out of the focus order as the Button says — and enable it again WHEN it can be sent, never enabling a Button in the send slot that the product disabled itself. It SHALL add no state to the Button that a Button does not have: a send button that cannot send is a disabled Button and nothing else, and what holds the message back is shown where it is — on the counter (CCM-19), on the File item still on its way or failed (CCM-20), and in the product's own words. |
CCM-11 | WHEN the person activates the send button by any means the Button has, or asks to send by the keyboard (CCM-12), WHILE what it holds can be sent THE chat composer SHALL emit send exactly once, carrying value exactly as it stands — no character trimmed, joined or replaced — and the attachments in the order of the strip: WHILE attachments is own, the files it keeps, as the platform's own handles; WHILE attachments is given, the value of every File item whose status is not error. THE chat composer SHALL NOT send: it SHALL open no connection and pass the text and the files to no one but the product, as its file picker passes them to no one (FPK-7). |
CCM-12 | THE chat composer SHALL decide what the embedded text input's action is (ETI-6): a request to send (CCM-11). It SHALL give the embedded text input enterKey action WHILE sendKey is enter and enterKey line-break WHILE sendKey is modifier, and SHALL ask to send WHEN the embedded text input emits action, so that on a hardware keyboard, WHILE no input method is composing (CCM-13): WHILE sendKey is enter, Enter with no modifier asks to send and inserts no line break, and Shift+Enter inserts a line break; WHILE sendKey is modifier, Enter, with or without Shift, inserts a line break; and under both values Ctrl+Enter and, on Apple platforms, Command+Enter ask to send and insert nothing. THE return key of an on-screen keyboard SHALL insert a line break under both values, and that keyboard SHALL be asked for a return key and not for a send or a done key (ETI-6): there the send button sends. WHEN sendKey names a value the chat composer does not know THE chat composer SHALL behave as enter, and in debug builds report the unknown value. No other key SHALL send, and Escape SHALL do nothing to the chat composer. |
CCM-13 | WHILE an input method is composing text in the typing area THE chat composer SHALL leave every key to the input method and the platform's text element, as the embedded text input does (ETI-4): the Enter that chooses a candidate or commits the composition SHALL NOT send, SHALL NOT be counted as a request to send, and SHALL leave the committed text in the typing area; the first Enter pressed after the composition has ended SHALL be read as CCM-12 says. THE chat composer SHALL send for no key it reads itself: it has no key listener that could send beside the embedded text input's action. |
CCM-14 | WHEN the person asks to send by the keyboard WHILE what it holds cannot be sent THE chat composer SHALL emit no send, SHALL insert no line break for a key that would have sent, and SHALL keep the text, the attachments and the focus as they are; and WHILE a reason other than empty and disabled holds, SHALL emit sendBlocked exactly once with the reasons (CCM-9), so that the product can say in its own words what holds the message back (announcement). A request to send WHILE the only reason is empty SHALL do nothing at all. |
CCM-15 | WHEN send has been emitted WHILE keepAfterSend is false THE chat composer SHALL empty itself at once, without waiting to learn what became of the message: value becomes empty and, WHILE attachments is own, it lets go of every file it keeps and removes every File item it drew, those of refused files with them. It SHALL emit neither input nor attachmentsChange for this, since send has said it. WHILE attachments is given the File items are the product's and THE chat composer SHALL remove none: the product removes the items it gave. WHILE keepAfterSend is true THE chat composer SHALL keep everything as it is, and SHALL offer the product, in the platform's idiom, a way to empty it that does exactly what this sentence says for false. Whether the message arrived is the product's to know and to say; the chat composer never says it. |
CCM-16 | WHEN send has been emitted THE chat composer SHALL move focus to the typing area, whether the person sent with the send button, with a key or through assistive technology, so that the next message can be written at once and an on-screen keyboard stays open; WHILE disabled is true it SHALL move focus nowhere. |
CCM-17 | WHILE sending is true THE chat composer SHALL send nothing more (CCM-9), SHALL leave the typing area and the attachments as open to the person as they were, and SHALL expose itself as busy. WHILE sending and stoppable are both true THE chat composer SHALL put in the send button's place a stop button — the one element in the stop slot, or, WHILE that slot is empty, a Button it brings: icon-only, of the size and the variant of the send button it brings, showing the stop glyph of the system's icon set, and named by stopLabel — enabled WHILE disabled is false; WHEN it is activated THE chat composer SHALL emit stop exactly once and SHALL change nothing itself: the product stops what is under way and sets sending. No key SHALL stop. WHEN the button in that place changes while it has focus THE chat composer SHALL keep focus on the button now there, or move it to the typing area WHERE that button is disabled. |
CCM-18 | WHILE maxLength is greater than 0 THE chat composer SHALL count the user-perceived characters of value (grapheme clusters: an emoji, or a letter with its accents, counts once) and SHALL keep the limit to itself — the embedded text input has none: it SHALL refuse no edit, shorten no paste and cut no value, during a composition or after it. WHILE the count is greater than maxLength THE chat composer SHALL hold the reason length (CCM-9), draw the capsule in its error condition and expose the typing area as invalid, as error does (CCM-27), and end both WHEN the count is within the limit again. IF maxLength is not a number above zero THE chat composer SHALL set no limit. |
CCM-19 | WHILE counter is true and disabled is false THE chat composer SHALL draw the counter at the place CCM-2 and CCM-3 name: the count of user-perceived characters in value, followed by a slash and maxLength WHILE maxLength is greater than 0, on one line in comp.chat-composer.counter.typescale and comp.chat-composer.counter.color, as text of its own direction isolated from the text around it, with digits of equal width so that the row does not shift as the number changes. WHILE the count is greater than maxLength it SHALL draw it in comp.chat-composer.counter.error.color; the two numbers say it without the color. THE chat composer SHALL give what the counter shows to the embedded text input as the first part of its description (ETI-13), and the counter SHALL NOT be a live region. |
CCM-20 | WHILE attachments is not none THE chat composer SHALL show each attachment as one File item in the strip, and as nothing else: the item's glyph by the file's format or its picture, its name, its second line, its bar while it is sent, and its retry and remove actions are drawn, exposed to assistive technology and answered as the File item says (FIT-1, FIT-4, FIT-5, FIT-6, FIT-10, FIT-11), from the File item's own tokens, with removeLabel and retryLabel as the names of its actions. THE chat composer SHALL decide only where the items stand (CCM-43) and, for the items it draws itself, which of the File item's own props they are given (CCM-44); it SHALL draw no second kind of attachment, SHALL NOT restyle an item, and SHALL NOT reorder the items or let a person reorder them: the order of the strip is the order given or kept, and reordering is a List's. |
CCM-21 | WHILE attachments is given THE chat composer SHALL keep no file and SHALL lay out the File items the product gives, in the order given, each with the form, the picture and every other prop the product gave it, changing none: WHEN files arrive — chosen in the picker, dropped (CCM-23) or pasted (CCM-24) — it SHALL have its file picker judge them and emit select and reject (FPK-9, FPK-31), and draw of them only what the product gives back as File items; WHEN an item's remove or retry action is activated the item emits remove or retry with its value (FIT-11) and THE chat composer SHALL change nothing itself. |
CCM-22 | WHILE attachments is own THE chat composer SHALL keep the files a person attaches itself. WHEN files arrive — chosen in its file picker and judged by it (FPK-9), or dropped on the capsule, pasted or handed over by the product, which it SHALL have its file picker judge by the same rule (FPK-31) — it SHALL first remove the File items of the files an earlier arrival refused; SHALL pass over, as if it had not arrived, a file that passed and that it already keeps; WHILE multiple is true SHALL keep each file that passed after the files it keeps, refusing with the reason count each one beyond the room maxFiles leaves, and WHILE multiple is false SHALL let go of the file it keeps and keep the new one in its place; SHALL draw each kept file as a File item given the file itself, whose name is the file's as the platform reports it (FIT-33) and whose status is pending, in the order it keeps them, and after them each file the arrival refused as a File item in error, not retryable and given no handle of the file, with the product's sentence for its reason as its errorText (typeErrorText, sizeErrorText or countErrorText); SHALL remove an item itself when its remove action is activated (FIT-11); SHALL hold of a file only the platform's own handle; and SHALL emit for one arrival select, then reject, then attachmentsChange, each once and only where it applies, and attachmentsChange once for a removal of a kept file. It SHALL offer the product, in the platform's idiom, the files it keeps, a way to hand it files — which it treats as an arrival, for a product whose own action in a slot makes a file (a photograph, a recording) — and a way to let go of them all. The file upload keeps its own files by the same rule, in sentences of its own: the chat composer holds no file upload and cites none of them. |
CCM-23 | WHILE attachments is not none and disabled is false, WHILE files are dragged over any part of the capsule THE chat composer SHALL draw the capsule as receiving — filled with comp.chat-composer.receiving.container.color, its edge comp.chat-composer.receiving.outline.width wide in comp.chat-composer.receiving.outline.color, at the capsule's own shape and inside its own box, in place of its resting, hovered, focused and error drawing — so that nothing moves and no second shape is drawn; WHEN the files are let go over it THE chat composer SHALL end the condition at once, take the drop so that the platform does not open or navigate to a file, and have the files judged exactly as a choice in the picker (FPK-31, CCM-21, CCM-22). WHILE what is dragged holds no file THE chat composer SHALL draw nothing for it and leave the drag to the typing area and the platform. Dragging SHALL never be the only way to attach: the attach button opens the picker by keyboard, by touch and through assistive technology. |
CCM-24 | WHEN the person pastes into the typing area WHILE attachments is not none and disabled is false, and what is pasted holds files and no plain text, THE chat composer SHALL take those files as an arrival (CCM-21, CCM-22) and insert nothing into the text. WHERE what is pasted holds plain text — with or without a file beside it, as a copied table carries a picture of itself — THE chat composer SHALL leave the paste to the typing area, which inserts the text, and SHALL take no file from it. WHILE attachments is none every paste SHALL be the typing area's. |
CCM-25 | WHEN the platform's picker closes with at least one file kept (attachments own) or handed to the product (attachments given) THE chat composer SHALL move focus to the typing area; WHEN it closes with nothing chosen, or with every file refused, THE chat composer SHALL leave focus on the attach button. A drop and a paste SHALL move focus nowhere. WHEN a File item leaves the strip while focus is in it — which the File item leaves to what holds it (FIT-30) — THE chat composer SHALL move focus to the remove action of the item that followed it, or of the item before it WHERE none follows, or to the typing area WHERE no item is left, so that a person who removes several files in a row goes on from where they were. |
CCM-26 | WHILE disabled is true THE chat composer SHALL disable every member as that member's own disabled does — the embedded text input (ETI-15), the attach button, the send or the stop button, and every File item (FIT-21) — and every Button in the leading and the trailing slot, giving each back as it was WHEN disabled becomes false and never enabling one the product disabled itself; SHALL draw the capsule from the disabled tokens of its condition (CCM-42); SHALL NOT draw the counter; SHALL take no drop and no paste of files and pass on no press on the capsule; SHALL emit no send, stop, sendBlocked, select, reject or attachmentsChange; and SHALL expose itself as disabled. The text, the File items and the files it keeps SHALL stay, readable by assistive technology. |
CCM-27 | WHILE error is true THE chat composer SHALL draw the capsule in its error condition — every state from the error tokens, its edge at rest in comp.chat-composer.error.outline.color, so that the capsule itself is marked and not only a text beside it (CCM-42) — and SHALL give error to the embedded text input, which exposes the typing area as invalid and draws nothing for it (ETI-14); it SHALL draw nothing else: the words of the error are a Field's or the product's. THE chat composer SHALL enter that condition itself only for the limit (CCM-18), SHALL never set its own error prop, and SHALL NOT hold a message back for error alone. |
CCM-28 | WHILE the chat composer is the control of a Field THE chat composer SHALL take from it what a Field gives its control (FLD-8, FLD-10, FLD-11, FLD-14) and pass it on to the embedded text input, which asks no Field itself (ETI-13): the Field's label as its own name and as the typing area's WHILE its own label is empty; the Field's supporting text with, in error, the error text after it as the last part of the typing area's description; error WHILE the Field is in error; and disabled WHILE the Field is disabled. It SHALL draw none of the Field's rows itself. |
CCM-29 | THE chat composer SHALL expose itself to assistive technology as a group named by label, holding, in the order they are drawn: WHILE there are File items, one list of them, each exposed as an item of that list (FIT-19); then, WHILE it is laid out inline (CCM-2), the attach button, the Buttons of the leading slot, the typing area — a text box of several lines named by label and described by the counter WHILE it is drawn (ETI-13) — the Buttons of the trailing slot, and the send or the stop button; and WHILE it is laid out stacked (CCM-3), the typing area, the send or the stop button, and then what the row beneath them holds: the attach button, the Buttons of the leading slot and the Buttons of the trailing slot; each a button named as CCM-6, CCM-7 and CCM-17 say. Tab and Shift+Tab SHALL move through them in the order of the layout it is drawn in, which is the order a person reads them in, each control one stop — the actions of each File item among them, in the order of the strip — and THE chat composer SHALL add no stop, no roving focus and no key of its own: Backspace and Delete in an empty typing area SHALL remove no attachment, and the arrow keys SHALL stay the typing area's, though the embedded text input lets what holds it take a key first (ETI-21). WHERE the visual size of a Button it brings is below the minimum target THE chat composer SHALL leave the hit area the Button extends to sys.target.touch for touch input and sys.target.pointer for fine pointers (BTN-5) uncovered by a neighbour and inside the capsule. |
CCM-30 | THE chat composer SHALL have exactly one live region, polite and never assertive, for itself and for every File item it holds, since a File item announces nothing (FIT-19), and SHALL announce through it, once each and in the product's words alone: announcement WHEN the product sets or changes it to a text that is not empty; WHILE attachments is given, WHEN a File item is added after the chat composer was first presented its name and its statusText, and WHEN an item's status becomes done or error its name and its statusText or errorText; WHILE attachments is own, for each arrival one message, in the order of the strip — the name of each file it newly kept and, for each refused file, its name and its sentence. It SHALL announce no progress and nothing of its own making — not that a message was sent, not why one cannot be, not a count: a sent message is told by the typing area standing empty under the focus (CCM-15, CCM-16), and anything more by the product's announcement. |
CCM-31 | THE chat composer SHALL have no words of its own: it SHALL take sendLabel, stopLabel, attachLabel, removeLabel, retryLabel, typeErrorText, sizeErrorText and countErrorText from the instance's prop and, WHILE that is empty, from the product's words of the keys sendLabel, stopLabel, attachLabel, removeLabel, retryLabel, fileTypeErrorText, fileSizeErrorText and fileCountErrorText, and every other word — label, placeholder, announcement, a File item's texts, the names of the product's own Buttons — from the product, and from nowhere else. IF neither gives a name to a button the chat composer brings THE chat composer SHALL draw the button without a name and in debug builds report a control that assistive technology cannot name as a functional failure; IF label is empty and no Field names it, likewise. |
CCM-32 | WHILE the text size is enlarged THE chat composer SHALL grow with its texts: the typing area and the File items as they say (ETI-24, FIT-24), the counter with its text, the Buttons as the Button says, the capsule with what it holds — its corners with the height of one line (CCM-41) — and comp.chat-composer.layout.inline-min-width with the text size, so that a composer whose text has doubled is laid out stacked sooner. At 200 % in a width of 320 CSS pixels, or the platform's equivalent, nothing SHALL be cut, overlap or need scrolling sideways: the typing area keeps the capsule's width but for the send button beside it, the row of actions beneath wraps (CCM-3), and the strip wraps, a row never wider than the strip and a tile never wider than it either (CCM-43). |
CCM-33 | WHERE the platform draws with a person's own forced colors (the web's forced-colors mode, and a platform's equivalent where it has one) THE chat composer SHALL draw the capsule on the platform's own background with an edge in the platform's text color, comp.chat-composer.outline.width wide, at rest — so that the capsule is still seen where its fill is taken away; in the platform's highlight color, at the focused width of comp.chat-composer.outline.width (CCM-42), WHILE the typing area has focus; in the platform's highlight color at twice comp.chat-composer.receiving.outline.width WHILE it is receiving; and in the platform's disabled text color WHILE disabled. In its error condition the edge SHALL stay the platform's text color: the typing area is exposed as invalid and the counter's two numbers still say the limit. THE counter SHALL be drawn in the platform's text color, and every member SHALL draw as it says there (ETI-23, FIT-25). |
CCM-34 | WHERE the layout direction is right-to-left THE chat composer SHALL mirror its layout: the attach button and the leading actions at the right, the counter, the trailing actions and the send button at the left, with the send button outermost; in the stacked layout the send button at the left end of the typing area's line, and in the row beneath the attach button and the leading actions at the right and the counter and the trailing actions at the left; and the strip filled from the right, each line from right to left. It SHALL take its direction from where it is used and have no direction setting of its own; SHALL draw the send glyph through the icon helper, which mirrors a glyph the icon set marks as directional; and SHALL leave the direction of the text itself to the typing area, which sets each paragraph by its own first strong character (ETI-20). |
CCM-35 | WHEN the capsule's fill or edge changes — between rest, hover, focus, the error condition, disabled and receiving — or the counter changes color, THE chat composer SHALL animate the change over comp.chat-composer.motion.state-duration with comp.chat-composer.motion.state-easing, and SHALL make it at once where motion is reduced. THE capsule SHALL size itself to what it holds, so that its height follows the typing area's at every frame of the embedded text input's own change of height (ETI-8, ETI-10), with no duration, delay or curve of its own; its corners SHALL keep their radius throughout and its Buttons their distance from its bottom edge, or — the send button of the stacked layout — from the row beneath (CCM-2, CCM-3, CCM-41): the capsule's shape is never animated apart from its height, and nothing in it moves but what the growing text pushes. A File item that is added or removed, a change between the inline and the stacked layout, and the change between the send and the stop button SHALL take effect at once. |
CCM-36 | WHERE the platform has forms that submit named values (the web) and the chat composer stands in one THE chat composer SHALL take part in it as one control: it SHALL submit value under its name and, WHILE attachments is own, every file it keeps under the name given for its files, WHILE it is not disabled; SHALL be disabled WHILE the form disables the set it is in; SHALL become empty WHEN the form is reset; and WHEN send has been emitted and the product has not cancelled it, SHALL ask the form to submit as a submit button of the platform's own does — the form's validation first, then its submit event, which the product may cancel (BTN-27). In a form THE chat composer SHALL NOT empty itself after send, whatever keepAfterSend says: a submission that leaves the page takes the composer with it, and a product that cancels the submission to send by itself empties the composer when it knows the message arrived (CCM-15). |
CCM-37 | WHERE the platform shows markup before a component is defined (the web) THE chat composer SHALL let a product write, as its children, the platform's own text area and submit button, so that a form holding it works with no script: the person writes in the text area and the button submits the form. WHEN the chat composer is defined THE chat composer SHALL take the text the text area holds as its value WHILE its own value is empty, SHALL take the text area and the button out of what is drawn, out of the focus order, out of what assistive technology reads and out of what the form submits, and SHALL submit under its own name (CCM-36) from then on. |
CCM-38 | WHEN a prop of the chat composer or of a File item changes while it is shown THE chat composer SHALL draw and expose the new value at once, keeping its focus, the caret and the typing area's scroll position, and SHALL announce only what CCM-30 names. WHEN attachments names a value the chat composer does not know THE chat composer SHALL behave as none, and in debug builds report it; WHEN attachments changes between own and given THE chat composer SHALL let go of every file it keeps and remove every File item it drew itself, or stop drawing the product's items and begin with no kept file, emitting and announcing nothing for the change itself; and WHEN it becomes none SHALL let go of every file it keeps and draw no strip. |
CCM-39 | WHEN an on-screen keyboard opens, closes or changes its height THE chat composer SHALL NOT move, resize or scroll itself or the page for it: keeping the composer above the keyboard is the screen's, by the platform's own means, and the product's where it places the composer. THE chat composer SHALL leave the caret kept in view to the typing area (ETI-7) and SHALL NOT close a keyboard a person opened, WHEN a message is sent (CCM-16) or at any other time. |
CCM-40 | THE chat composer SHALL hold the text and the files it keeps for as long as it is shown and no longer: it SHALL NOT write a draft to any store, SHALL NOT restore one, and SHALL NOT read, copy or keep a file's content — the picture of a kept image is the File item's, made and let go of as it says (FIT-7). A product that keeps a draft across a reload or between conversations SHALL be able to do so from input and attachmentsChange and by setting value; WHERE the platform restores a form's controls itself (the web, on going back to a page) THE chat composer SHALL take the restored text and SHALL begin with no file. |
CCM-41 | THE chat composer SHALL draw the capsule as one container behind everything it holds: filled with the container color of its condition (comp.chat-composer.standard.container.color, or comp.chat-composer.error.container.color in error), lifted by the container elevation of its condition (comp.chat-composer.standard.container.elevation, comp.chat-composer.error.container.elevation), with an edge comp.chat-composer.outline.width wide in the outline color of its condition (comp.chat-composer.standard.outline.color, comp.chat-composer.error.outline.color), drawn inside its box so that no state moves what it holds. Its four corners SHALL have one radius: comp.chat-composer.container.shape, and never more than half the height the capsule has WHILE it is laid out inline with one line of text and no attachment — half of comp.chat-composer.container.height, or of more where enlarged text makes that line taller. WHILE that token is the full step a capsule of one line is therefore fully rounded, and a capsule that has grown — by lines, by attachments or by the stacked layout — SHALL keep exactly that radius: it SHALL NOT round further as it grows taller, SHALL NOT take another radius for a grown form, and SHALL clip nothing it holds, since the paddings of CCM-2, CCM-3 and CCM-43 keep the text, the Buttons and the File items inside its corners. THE chat composer SHALL ease the corners by sys.shape.corner-smoothing as a Button eases its own (BTN-10). |
CCM-42 | THE chat composer SHALL answer focus with the capsule's edge, because the embedded text input draws no focus indication and leaves it to what holds it (ETI-11): WHILE the typing area has focus — given by the keyboard, a pointer, touch or the product alike — it SHALL draw the edge from the focused tokens of its condition — the focused width of comp.chat-composer.outline.width, in the focused color of comp.chat-composer.standard.outline.color (of comp.chat-composer.error.outline.color in error) — in place of a focus ring and with the container as at rest (the focused value of comp.chat-composer.standard.container.color), and WHERE sys.behavior.focus-style is glow SHALL also draw the glow around the capsule. WHILE focus is on a Button or inside a File item the capsule SHALL draw its resting edge and that control its own focus indication, so that focus is shown once. WHILE a pointer that can hover is over the capsule and disabled is false THE chat composer SHALL draw the capsule from the hovered tokens of its condition (the hovered value of comp.chat-composer.standard.container.color, in which the default system mixes comp.chat-composer.standard.state-layer.color into the fill, or of comp.chat-composer.error.container.color with comp.chat-composer.error.state-layer.color in error), as every box that is typed in answers hover; a Button or a File item under the pointer answers its own as well. THE capsule SHALL answer no press: it has no pressed tokens, draws no ripple and holds no pressed state, and a press on it only passes focus on (CCM-5). WHILE disabled is true it SHALL be drawn from the disabled tokens of its condition (the disabled values of comp.chat-composer.standard.container.color, comp.chat-composer.standard.outline.color and comp.chat-composer.outline.width, and the group's disabled opacity). WHERE more than one holds, the capsule SHALL be drawn as the first of: disabled, receiving (CCM-23), focused, hovered; the error condition recolors rest, hovered, focused and disabled alike. |
CCM-43 | WHILE it holds at least one File item THE chat composer SHALL lay the items out as a strip at the top of the capsule — comp.chat-composer.container.padding-block and comp.chat-composer.strip.padding-block-start below the capsule's top edge, comp.chat-composer.container.padding-inline from each inline edge, and comp.chat-composer.strip.offset above what follows — in the order given (CCM-21) or kept (CCM-22), from the start edge, comp.chat-composer.strip.gap apart: a File item of the tile form at the width and the height the File item gives it (FIT-3), beside its neighbours and aligned to the top of its line; a File item of the row form across a whole line of the strip (FIT-2), so that its name and its sentence have the strip's width. WHERE a line is full THE chat composer SHALL wrap the next tile onto a further line, comp.chat-composer.strip.gap below; it SHALL NOT scroll the strip sideways, cut or shrink an item, fold items into a count, or hide one — every attachment and its state stays in view, since one that is still on its way or has failed is what holds the message back (CCM-9). The strip SHALL grow the capsule, which keeps its corners (CCM-41); how many attachments one message may carry is the product's, by maxFiles. |
CCM-44 | WHILE attachments is own THE chat composer SHALL give each File item it draws the form attachmentForm names: tile; row; or, for auto, tile for a kept file whose kind the File item reads as image (FIT-5) and row for every other kept file. A file it refused (CCM-22) SHALL be a row whatever attachmentForm says: its sentence is why it is shown, and a row gives the sentence the strip's width. To a tile of an image it SHALL give preview and nameHidden, so that the File item draws the picture from the file on the person's own device and lets go of it itself (FIT-6, FIT-7, FIT-8) and the strip is one of pictures; to every other item it SHALL give neither, and THE chat composer SHALL itself read nothing of any file but what the platform reports. The File items it draws SHALL NOT be pressable and SHALL have the size the File item's own tokens give: a product that opens an attachment where it stands, or sizes its tiles, gives the items itself. WHILE attachments is given every prop of each item is the product's, set on the item, and attachmentForm SHALL NOT be read. WHEN attachmentForm names a value the chat composer does not know THE chat composer SHALL behave as auto, and in debug builds report the unknown value. |
CCM-45 | WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE chat composer SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on its group (CCM-29) — an id reference by the elements it names in the host's tree or a tree around it, a description after its own — except those the chat composer owns (aria-label, aria-labelledby, aria-busy, aria-disabled, aria-invalid, aria-errormessage), which it SHALL move and not expose: its name is label, its busy state is sending and its other states are its props (CCM-29). WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE chat composer SHALL take it off that element. WHEN the product sets an aria- attribute the chat composer owns THE chat composer SHALL report it in debug builds. |
各平台名称对照
对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 组件 | tanrei-chat-composer | TanreiChatComposer | TanreiChatComposer | TanreiChatComposer | ChatComposer |
value · 属性 | value | text | value | controller | value |
label · 属性 | label | label | contentDescription | semanticLabel | accessibilityLabel |
placeholder · 属性 | placeholder | prompt | placeholder | hintText | placeholder |
layout · 属性 | layout | layout | layout | layout | layout |
minRows · 属性 | minRows | lineLimit | minLines | minLines | minRows |
maxRows · 属性 | maxRows | lineLimit | maxLines | maxLines | maxRows |
maxLength · 属性 | maxLength | maxLength | maxLength | maxLength | maxLength |
counter · 属性 | counter | counter | counter | showCounter | counter |
sendKey · 属性 | sendKey | sendKey | sendKey | sendKey | sendKey |
attachments · 属性 | attachments | attachments | attachments | attachments | attachments |
attachmentForm · 属性 | attachmentForm | attachmentForm | attachmentForm | attachmentForm | attachmentForm |
multiple · 属性 | multiple | allowsMultipleSelection | multiple | multiple | multiple |
accept · 属性 | accept | allowedContentTypes | mimeTypes | accept | accept |
maxSize · 属性 | maxSize | maxSize | maxSize | maxSize | maxSize |
maxFiles · 属性 | maxFiles | maxFiles | maxFiles | maxFiles | maxFiles |
sending · 属性 | sending | isSending | sending | sending | sending |
stoppable · 属性 | stoppable | isStoppable | stoppable | stoppable | stoppable |
blocked · 属性 | blocked | isBlocked | blocked | blocked | blocked |
keepAfterSend · 属性 | keepAfterSend | keepsContentAfterSend | keepAfterSend | keepAfterSend | keepAfterSend |
disabled · 属性 | disabled | disabled | enabled | enabled | disabled |
error · 属性 | error | isError | isError | isError | error |
sendLabel · 属性 | sendLabel | sendLabel | sendContentDescription | sendSemanticLabel | sendAccessibilityLabel |
stopLabel · 属性 | stopLabel | stopLabel | stopContentDescription | stopSemanticLabel | stopAccessibilityLabel |
attachLabel · 属性 | attachLabel | attachLabel | attachContentDescription | attachSemanticLabel | attachAccessibilityLabel |
removeLabel · 属性 | removeLabel | removeLabel | removeContentDescription | removeSemanticLabel | removeAccessibilityLabel |
retryLabel · 属性 | retryLabel | retryLabel | retryContentDescription | retrySemanticLabel | retryAccessibilityLabel |
typeErrorText · 属性 | typeErrorText | typeErrorText | typeErrorText | typeErrorText | typeErrorText |
sizeErrorText · 属性 | sizeErrorText | sizeErrorText | sizeErrorText | sizeErrorText | sizeErrorText |
countErrorText · 属性 | countErrorText | countErrorText | countErrorText | countErrorText | countErrorText |
announcement · 属性 | announcement | announcement | announcement | announcement | announcement |
input · 事件 | input | text | onValueChange | onChanged | onChangeText |
send · 事件 | send | onSend | onSend | onSend | onSend |
stop · 事件 | stop | onStop | onStop | onStop | onStop |
sendableChange · 事件 | sendablechange | onSendableChange | onSendableChange | onSendableChanged | onSendableChange |
sendBlocked · 事件 | sendblocked | onSendBlocked | onSendBlocked | onSendBlocked | onSendBlocked |
select · 事件 | select | onSelect | onSelect | onSelect | onSelect |
reject · 事件 | reject | onReject | onReject | onReject | onReject |
attachmentsChange · 事件 | attachmentschange | onAttachmentsChange | onAttachmentsChange | onAttachmentsChanged | onAttachmentsChange |
| 备注 | Planned; built after the embedded text input, the File picker and the File item. The shadow-root form, composed as every composed web element is: its members' own custom elements in its shadow root, beside its own parts. The shadow root holds the capsule — <div part="container">, the chat composer's own — and inside it the strip (part="strip", a wrapping flex row holding a default <slot> for the product's File items and the <tanrei-file-item>s the composer renders itself), <tanrei-file-picker> around its trigger, <tanrei-embedded-text-input multiline>, the counter (part="counter"), <tanrei-button> for send or stop, and the live region. <tanrei-chat-composer label="Message" placeholder="Write a message" send-label="Send" attachments="own" attach-label="Attach files" remove-label="Remove" max-length="2000" counter name="message" files-name="files"><tanrei-button slot="leading" variant="text" label="Mention someone"><span slot="icon">alternate_email</span></tanrei-button></tanrei-chat-composer>. React: ChatComposer from the React package, onSend, onStop, onSendableChange, onSendBlocked, onSelect, onReject, onAttachmentsChange, onInput. Attributes are kebab-case (send-key, max-rows, keep-after-send, attachment-form); accept is a comma-separated list. Slots: leading, trailing, attach, send, stop are named slots and the default slot takes the File items (attachments="given"); all stay in the product's light DOM. The capsule: border-radius: min(var(--…-container-shape), calc(H / 2)) where H is the one-line height — max(var(--…-container-height), 1lh + 2 * input.padding-block + 2 * container.padding-block), with 1lh taken in the embedded text input's text style (the composer reads the member's computed line height, never its token) — so the radius does not depend on the capsule's present height; the edge is an inset box-shadow or an outline with a negative offset, never a border that changes width, so that no state moves the content. The focused edge is a state the composer sets from the embedded text input's own focusin and focusout (ETI-11), not :host(:focus-within) alone, which would also hold while a Button or a File item's action has focus. A pointerdown on the capsule whose target is the capsule itself, the strip's own gaps or the paddings is passed on to the embedded text input by the way it offers what holds it (ETI-12), and its default is prevented so that focus is not taken away again. Tokens reach the members by inheritance and the element writes none of a member's custom properties; a product sets one composer apart with a custom property on the element. Parts: container, strip, counter are its own; the members' documented parts are exported prefixed by the member (input-…, attach-…, send-…). Focus: the shadow root does not delegate focus (the capsule holds File items and a counter a press must not act on); focus() is overridden to go to the typing area. Keys (CCM-12, CCM-13): the composer has no keydown listener that sends. It sets enter-key="action" on the embedded text input while send-key is enter and enter-key="line-break" while it is modifier, and asks to send on the input's action; the composition, the engines that report the committing Enter late, and the guess that stands for an on-screen keyboard ((pointer: coarse) and (hover: none)) are all the embedded text input's (ETI-4, ETI-6), so a tablet with a keyboard attached sends with Ctrl+Enter or Command+Enter and with the send button. Where the message cannot be sent the composer still takes the action (CCM-14) and inserts nothing. Events are CustomEvents that bubble and are composed: send has detail.value and detail.attachments (a File[] while own, a string[] of item values while given) and is cancelable — cancelling it keeps the content for this one message and requests no form submission; sendablechange and sendblocked have detail.sendable and detail.reasons; select, reject as the file picker's; attachmentschange has detail.files. The embedded text input's action, change and rowschange are stopped at the shadow root; its input leaves as it is. Methods: clear(), addFiles(files) (answers the refused files and their reasons), the read-only files. Drop (CCM-23): dragenter, dragover, dragleave and drop on the host, counting enters and leaves, read only while DataTransfer.types holds Files; the files are handed to the file picker to judge (FPK-31). Paste (CCM-24): the paste event from the typing area; files are taken only while clipboardData.files is not empty and clipboardData.getData("text/plain") is. Pictures (CCM-44): the composer makes no object URL; it sets file, preview and name-hidden on the File item of a kept image, and the File item makes and revokes the address itself (FIT-7). The strip is role="list", and each File item is told it is an item of a list by the channel the File upload uses for its own (FIT-19) — the product's slotted items too. A File item of the row form has flex: 1 0 100%; a tile flex: none. Form-associated (CCM-36): the composer has no single value member — it is a feature — so it sets its own form value: setFormValue receives a FormData with the text under name and one entry per kept file under files-name; form.requestSubmit() after send; formResetCallback empties; formStateRestoreCallback restores the text. Without script (CCM-37): <form method="post" action="/messages"><tanrei-chat-composer name="message" label="Message" send-label="Send"><textarea name="message" aria-label="Message" rows="3"></textarea><button type="submit">Send</button></tanrei-chat-composer></form> — until the element is defined these two children are the form, drawn as the page's own styles draw them and with no capsule; once it is, they are not slotted (they are neither a File item nor in a named slot), the text area's text becomes the value, and both are set disabled and hidden so that the form submits the composer alone. A file cannot be attached without script: a page that must take files there adds the platform's own <input type="file" name="files" multiple> beside the text area, which the composer takes out in the same way. The layout of CCM-4 is a container query on the capsule. One level down, by hand — the product draws the box, since the capsule cannot be taken separately: <div class="my-composer" role="group" aria-label="Message"><ul class="strip"><li><tanrei-file-item value="a" name="brief.pdf" size-text="820 KB" remove-label="Remove"></tanrei-file-item></li></ul><div class="row"><tanrei-file-picker multiple><tanrei-button slot="trigger" variant="text" label="Attach files"><span slot="icon">attach_file</span></tanrei-button></tanrei-file-picker><tanrei-embedded-text-input label="Message" multiline max-rows="6" enter-key="action"></tanrei-embedded-text-input><tanrei-button label="Send" disabled><span slot="icon">send</span></tanrei-button></div></div>, with .my-composer:has(tanrei-embedded-text-input:focus-within) drawing the focus indication the embedded text input leaves to its holder, the row a flex row whose items align to its end edge, and a listener on the input's action that sends. | Planned. TanreiChatComposer("Message", text: $text, onSend: { message in … }) { leading } trailing: { … }: a view that draws the capsule itself (a background shape whose corner radius is the lesser of the token and half the one-line height) and holds TanreiEmbeddedTextInput, TanreiFilePicker, TanreiFileItems in a wrapping layout, and TanreiButton. minRows and maxRows are one lineLimit(1...6). attachments is .none, .own or .given([TanreiFileItem.Model]). sendKey sets the embedded text input's enterKey, and its onSubmit is the request to send (ETI-6); marked text (NSTextInputClient, UITextInput.markedTextRange) is the composition of CCM-13. The focused edge follows a @FocusState of the typing area. The drop is dropDestination(for: URL.self) on the whole capsule with its isTargeted binding; the paste of files is onPasteCommand(of:) on macOS and the text view's paste configuration on iOS. A kept image's picture is the File item's to draw, from the file it is given (FIT-7). keepAfterSend with a TanreiChatComposerState whose clear(), add(urls) and files are CCM-15 and CCM-22. Keeping the composer above the keyboard is the screen's: safeAreaInset(edge: .bottom) and the system's keyboard avoidance. No form. | Planned. TanreiChatComposer(value = text, onValueChange = { text = it }, onSend = { message -> … }, leading = { … }, trailing = { … }): a composable that draws the capsule itself (a Surface whose shape's radius is the lesser of the token and half the one-line height) and holds TanreiEmbeddedTextInput, TanreiFilePicker, TanreiFileItems in a FlowRow, and TanreiButton. disabled is the inverse flag enabled. sendKey sets the embedded text input's enterKey, and its onAction is the request to send (ETI-6); TextFieldValue.composition not null is the composition of CCM-13. The focused edge follows the typing area's onFocusChanged. The drop is Modifier.dragAndDropTarget on the whole capsule; the paste of files is the text field's receiveContent. A kept image's picture is the File item's to draw, from the file it is given (FIT-7). rememberTanreiChatComposerState() holds files, add(uris) and clear(). Keeping the composer above the keyboard is the screen's: Modifier.imePadding() where the product places it. No form. | Planned. TanreiChatComposer(controller: controller, semanticLabel: "Message", onSend: (message) { … }, leading: [ … ], trailing: [ … ]): a widget that draws the capsule itself (a DecoratedBox whose radius is the lesser of the token and half the one-line height) and holds TanreiEmbeddedTextInput, TanreiFilePicker, TanreiFileItems in a Wrap, and TanreiButton. value is the TextEditingController; disabled is the inverse flag enabled. sendKey sets the embedded text input's enterKey, and its onAction is the request to send (ETI-6); controller.value.composing valid is the composition of CCM-13. The focused edge follows the typing area's FocusNode. The framework has no picker and no drop target for files: onPick and the controller's dragEntered / dragExited / dropped(files) are the file picker's, as its notes say. A kept image's picture is the File item's to draw, from the file it is given (FIT-7). Keeping the composer above the keyboard is the screen's: a Scaffold that resizes for the inset, or MediaQuery.viewInsetsOf. No form. | Planned. <ChatComposer value={text} onChangeText={setText} accessibilityLabel="Message" onSend={({ value, attachments }) => …} leading={…} trailing={…} />: a component that draws the capsule itself (a View whose borderRadius is the lesser of the token and half the one-line height) and holds EmbeddedTextInput, FilePicker, FileItems in a wrapping row, and Button. sendKey sets the embedded text input's enterKey, and its onAction is the request to send (ETI-6); React Native gives JavaScript no signal that a composition is in progress, so how CCM-13 is held there is to be found when the platform is built, and until then the platform is to send with the send button alone. The focused edge follows the typing area's onFocus and onBlur. onPick is the product's function that opens its picker, as the file picker's notes say; no drop of files on phones. A kept image's picture is the File item's to draw, from the file it is given (FIT-7). A ref holds files, add(files) and clear(). Keeping the composer above the keyboard is the screen's: KeyboardAvoidingView or the product's own. No form. |
令牌
| 令牌 | 引用 | 解析值(浅色) |
|---|---|---|
--tanrei-comp-chat-composer-actions-gap | sys.space.space100 | 8px |
--tanrei-comp-chat-composer-container-gap | sys.space.space100 | 8px |
--tanrei-comp-chat-composer-container-height | — | 56px |
--tanrei-comp-chat-composer-container-padding-block | sys.space.space100 | 8px |
--tanrei-comp-chat-composer-container-padding-inline | sys.space.space200 | 16px |
--tanrei-comp-chat-composer-container-padding-inline-with-button | sys.space.space100 | 8px |
--tanrei-comp-chat-composer-container-row-gap | sys.space.space50 | 4px |
--tanrei-comp-chat-composer-container-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-chat-composer-counter-color | sys.color.on-surface-variant | |
--tanrei-comp-chat-composer-counter-error-color | sys.color.error | |
--tanrei-comp-chat-composer-disabled-opacity | — | 1 |
--tanrei-comp-chat-composer-disabled-outline-width | — | 1px |
--tanrei-comp-chat-composer-error-container-color | sys.color.surface-container-highest | |
--tanrei-comp-chat-composer-error-container-elevation | sys.elevation.level0 | |
--tanrei-comp-chat-composer-error-disabled-container-color | — | |
--tanrei-comp-chat-composer-error-disabled-container-elevation | — | |
--tanrei-comp-chat-composer-error-disabled-outline-color | — | |
--tanrei-comp-chat-composer-error-disabled-state-layer-color | — | |
--tanrei-comp-chat-composer-error-focused-container-color | — | |
--tanrei-comp-chat-composer-error-focused-container-elevation | — | |
--tanrei-comp-chat-composer-error-focused-outline-color | — | |
--tanrei-comp-chat-composer-error-focused-state-layer-color | — | |
--tanrei-comp-chat-composer-error-hovered-container-color | — | |
--tanrei-comp-chat-composer-error-hovered-container-elevation | — | |
--tanrei-comp-chat-composer-error-hovered-outline-color | — | |
--tanrei-comp-chat-composer-error-hovered-state-layer-color | — | |
--tanrei-comp-chat-composer-error-outline-color | sys.color.error | |
--tanrei-comp-chat-composer-error-state-layer-color | sys.color.on-surface | |
--tanrei-comp-chat-composer-focused-opacity | — | 1 |
--tanrei-comp-chat-composer-focused-outline-width | — | 2px |
--tanrei-comp-chat-composer-hovered-opacity | — | 1 |
--tanrei-comp-chat-composer-hovered-outline-width | — | 1px |
--tanrei-comp-chat-composer-input-padding-block | sys.space.space100 | 8px |
--tanrei-comp-chat-composer-layout-inline-min-width | — | 360px |
--tanrei-comp-chat-composer-motion-state-duration | sys.motion.role.state.default | 250ms |
--tanrei-comp-chat-composer-motion-state-easing | sys.motion.easing.state | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-chat-composer-outline-width | sys.border.hairline | 1px |
--tanrei-comp-chat-composer-receiving-container-color | sys.color.secondary-container | |
--tanrei-comp-chat-composer-receiving-outline-color | sys.color.on-secondary-container | |
--tanrei-comp-chat-composer-receiving-outline-width | sys.border.thick | 2px |
--tanrei-comp-chat-composer-send-button-variant | — | 0 |
--tanrei-comp-chat-composer-standard-container-color | sys.color.surface-container-highest | |
--tanrei-comp-chat-composer-standard-container-elevation | sys.elevation.level0 | |
--tanrei-comp-chat-composer-standard-disabled-container-color | — | |
--tanrei-comp-chat-composer-standard-disabled-container-elevation | — | |
--tanrei-comp-chat-composer-standard-disabled-outline-color | — | |
--tanrei-comp-chat-composer-standard-disabled-state-layer-color | — | |
--tanrei-comp-chat-composer-standard-focused-container-color | — | |
--tanrei-comp-chat-composer-standard-focused-container-elevation | — | |
--tanrei-comp-chat-composer-standard-focused-outline-color | — | |
--tanrei-comp-chat-composer-standard-focused-state-layer-color | — | |
--tanrei-comp-chat-composer-standard-hovered-container-color | — | |
--tanrei-comp-chat-composer-standard-hovered-container-elevation | — | |
--tanrei-comp-chat-composer-standard-hovered-outline-color | — | |
--tanrei-comp-chat-composer-standard-hovered-state-layer-color | — | |
--tanrei-comp-chat-composer-standard-outline-color | — | |
--tanrei-comp-chat-composer-standard-state-layer-color | sys.color.on-surface | |
--tanrei-comp-chat-composer-strip-gap | sys.space.space100 | 8px |
--tanrei-comp-chat-composer-strip-offset | sys.space.space50 | 4px |
--tanrei-comp-chat-composer-strip-padding-block-start | sys.space.space50 | 4px |
--tanrei-comp-chat-composer-counter-typescale | sys.typescale.label-small | 500 12px/1.3333 |
测试样例
作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。
inline-empty
label: Message placeholder: Write a message sendLabel: Send stacked-empty
label: Message placeholder: Write a message layout: stacked attachments: own sendLabel: Send attachLabel: Attach files inline-typed
value: Could you send the floor plan before Fri… label: Message placeholder: Write a message sendLabel: Send stacked-typed
value: Could you send the floor plan before Fri… label: Message placeholder: Write a message layout: stacked attachments: own sendLabel: Send attachLabel: Attach files inline-lines-at-maximum
value: Notes from the site visit:
1. The north… label: Message placeholder: Write a message maxRows: 6 sendLabel: Send stacked-lines-at-maximum
value: Notes from the site visit:
1. The north… label: Message placeholder: Write a message layout: stacked maxRows: 6 attachments: own sendLabel: Send attachLabel: Attach files inline-image-tiles
value: Three photographs from the site. label: Message placeholder: Write a message attachments: given sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} retryLabel: Retry {name} items: 3 stacked-image-tiles
value: Three photographs from the site. label: Message placeholder: Write a message layout: stacked attachments: given sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} retryLabel: Retry {name} items: 3 inline-file-rows
value: The brief and the survey are attached. label: Message placeholder: Write a message attachments: own sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} items: 2 stacked-file-rows
value: The brief and the survey are attached. label: Message placeholder: Write a message layout: stacked attachments: own sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} items: 2 inline-uploading
value: The two drawings are attached. label: Message placeholder: Write a message attachments: given sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} retryLabel: Retry {name} items: 2 stacked-uploading
value: The two drawings are attached. label: Message placeholder: Write a message layout: stacked attachments: given sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} retryLabel: Retry {name} items: 2 inline-failed
value: The two drawings are attached. label: Message placeholder: Write a message attachments: given sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} retryLabel: Retry {name} items: 3 stacked-failed
value: The two drawings are attached. label: Message placeholder: Write a message layout: stacked attachments: given sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} retryLabel: Retry {name} items: 3 inline-over-limit
value: Could you send the floor plan, the surve… label: Message placeholder: Write a message maxLength: 80 counter: true sendLabel: Send stacked-over-limit
value: Could you send the floor plan, the surve… label: Message placeholder: Write a message layout: stacked maxLength: 80 counter: true sendLabel: Send inline-disabled
value: The two drawings are attached. label: Message placeholder: Write a message counter: true attachments: given disabled: true sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} items: 1 slots: leading stacked-disabled
value: The two drawings are attached. label: Message placeholder: Write a message layout: stacked counter: true attachments: given disabled: true sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} items: 1 slots: leading inline-sending
value: And one more thing label: Message placeholder: Write a message sending: true sendLabel: Send stacked-sending
value: And one more thing label: Message placeholder: Write a message layout: stacked attachments: own sending: true sendLabel: Send attachLabel: Attach files inline-stoppable
label: Message placeholder: Write a message sending: true stoppable: true sendLabel: Send stopLabel: Stop stacked-stoppable
label: Message placeholder: Write a message layout: stacked attachments: own sending: true stoppable: true sendLabel: Send stopLabel: Stop attachLabel: Attach files inline-rtl
value: Could you send the floor plan before Fri… label: Message placeholder: Write a message maxLength: 280 counter: true attachments: given sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} retryLabel: Retry {name} direction: rtl items: 2 slots: leading, trailing stacked-rtl
value: Could you send the floor plan before Fri… label: Message placeholder: Write a message layout: stacked maxLength: 280 counter: true attachments: given sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} retryLabel: Retry {name} direction: rtl items: 2 slots: leading, trailing inline-actions
label: Message placeholder: Write a message attachments: own sendLabel: Send attachLabel: Attach files slots: leading, trailing stacked-actions
label: Message placeholder: Write a message layout: stacked attachments: own sendLabel: Send attachLabel: Attach files slots: leading, trailing counter
value: Could you send the floor plan before Fri… label: Message placeholder: Write a message maxLength: 280 counter: true sendLabel: Send blocked
value: Could you send the floor plan before Fri… label: Message placeholder: Write a message blocked: true sendLabel: Send error
value: Could you send the floor plan before Fri… label: Message placeholder: Write a message error: true sendLabel: Send own-image-tiles
value: Two photographs and the brief. label: Message placeholder: Write a message attachments: own sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} items: 3 form-tile
label: Message placeholder: Write a message attachments: own attachmentForm: tile sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} items: 3 form-row
label: Message placeholder: Write a message attachments: own attachmentForm: row sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} items: 2 attachments-refused
value: Here is the brief. label: Message placeholder: Write a message attachments: own accept: [".pdf",".png"] sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} typeErrorText: Only PDF and PNG files can be added. items: 2 tiles-wrapped
label: Message placeholder: Write a message layout: stacked attachments: given sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} retryLabel: Retry {name} items: 9 own-buttons
value: Could you send the floor plan before Fri… label: Message placeholder: Write a message layout: stacked attachments: own slots: attach, send long-text
value: Thesupportingstatementforthemunicipalpla… label: Message to everyone on the municipal pla… placeholder: Write what you want every member of the… maxLength: 2000 counter: true attachments: given sendLabel: Send attachLabel: Attach files removeLabel: Remove {name} retryLabel: Retry {name} items: 1 slots: leading, trailing without-words
attachments: given items: 2 生成自 specs/chat-composer.spec.json, tokens/comp/chat-composer.tokens.json, design/tokens/
- 文本在
name之下提交,保留的每个文件在files-name之下提交。位于表单里的输入栏在send之后请求表单提交,并且不清空自己。 - 屏幕键盘和实体键盘无法区分。在主指针是粗略指针、并且不能悬停的地方,Enter 另起一行,发送要靠发送按钮。
- 输入栏自己绘制的文件项上,不画文件的大小。
- 没有脚本时,你写成子元素的
<textarea>和提交用的<button>就是表单。元素完成定义时,它取走文本,并把两者隐藏起来。
聊天输入栏没有自己的文字。请给出 label、placeholder、sendLabel,以及用得到时的 stopLabel、attachLabel、removeLabel、retryLabel 和文件被拒绝时的三个句子。用 setWords() 为整个产品设置一次,或者写在元素上。
在字段里,输入栏以字段的标签作为名称,以字段的帮助文本或错误文本作为输入区的描述,并采用字段的错误和 disabled。
- 用你自己的话说明消息为什么不能发送:写在输入栏旁边,或者使用
announcement。 - 把你自己的操作作为纯图标的 text 按钮,放进
leading和trailing插槽。 - 把
maxFiles设成一条消息可以携带的文件数。 - 回答是逐步到达、并且可以中途打断的地方,使用
stoppable。 - 用平台自己的办法让输入栏保持在屏幕键盘上方。输入栏不会把自己固定在屏幕上。
- 不要通过输入栏来改写键入部分、文件项或按钮的样式。请给成员设置样式。
- 不要把发送按钮或附加按钮换成按钮以外的任何东西。
- 不要在上限处截断文本。
maxLength只标出超长的部分,然后等用户自己处理。 - 不要让拖放成为附加文件的唯一方式。
- 角色
group- 标注
- A group named by label, or by the label of the Field around it. The typing area is a text box of several lines with the same name — the embedded text input has no name of its own and takes it from the chat composer — described by the counter while it is drawn and by the Field's help and, in error, its error text, and exposed as invalid while error is true or the text is over the limit. The send, stop and attach buttons the chat composer brings are named by sendLabel, stopLabel and attachLabel; a Button of the product's keeps its own name. The attachments are one list, each read as an item of that list (FIT-19), its actions named by removeLabel and retryLabel with the file's name in them. One polite live region says the product's announcement and what changed among the attachments; the chat composer has no words of its own.
- 最小目标
- 即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px
键盘操作
| 按键 | 动作 |
|---|---|
| Tab / Shift+Tab | Moves through the chat composer in the order it is drawn: the actions of each File item in the strip's order, then — inline — the attach button, the leading actions, the typing area, the trailing actions and the send or the stop button; stacked, the typing area, the send or the stop button beside it, and then the row beneath: the attach button, the leading actions and the trailing actions. A disabled send button is not a stop, as no disabled Button is. |
| Enter | In the typing area, on a hardware keyboard: sends while sendKey is enter, and begins a new line while it is modifier. Never while an input method is composing: that Enter is the input method's. On an on-screen keyboard the return key begins a new line. |
| Shift+Enter | In the typing area: begins a new line under both values of sendKey. |
| Ctrl+Enter / Command+Enter | In the typing area: sends, under both values of sendKey. |
| Enter / Space | On a Button: what that Button does — sends, stops, opens the platform's file picker, removes or retries an attachment, or the product's own action. |
| The typing area's own keys | Everything else in the typing area — moving the caret, selecting, the input method, undo, Backspace and Delete — is the platform's text element's. No key in the typing area removes an attachment. |
备注
- The capsule is the focus indication of the typing area (CCM-42): the embedded text input draws none (ETI-11), so the chat composer must, and it does so for every focus of the typing area — keyboard, pointer or touch — as a text box does. The focused edge is held to 3:1 against the capsule's fill and against the surface around it, as the search bar's is (WCAG 2.4.7, 1.4.11). A Button or a File item's action inside the capsule shows its own focus ring, and the capsule is then at rest: focus is never shown twice, and the ring is never clipped by the capsule, whose paddings leave it room.
- A press on the capsule where no control is moves focus to the typing area and does nothing else (CCM-5): the capsule is the box of the text, as a text input's container is, and not a pressable control. It answers hover as every box that is typed in does and has no pressed state, and so the chat composer copies no press sentence of the Button for its own name. Every press that acts is on a Button or on a File item's action, and is answered as BTN-8, BTN-9 and BTN-10 and FIT-15 and FIT-16 say.
- The group and the typing area carry the same name: a person who tabs to the typing area hears it once as the group is entered and once on the text box. A second name for the group is left out: it would be one more word for every product to write, for a group whose every other control names itself.
- A send button that cannot send is a disabled Button, out of the focus order (CCM-10). What holds the message back is never said by the button: it is on the counter, whose two numbers say it without color (CCM-19); on the File item that is still on its way or has failed, in the product's words (CCM-20); and, for the product's own reasons, in the product's words beside the composer. A person on the keyboard who asks to send anyway is answered through sendBlocked and announcement (CCM-14).
- Sending is told without a word of Tanrei's: the typing area stands empty and focus is in it (CCM-15, CCM-16). A product that wants a sentence sets announcement ("Message sent") in its send handler; one whose sending can fail says so itself, and with keepAfterSend the text is still there to send again.
- While sending is true the group is exposed as busy; the typing area stays a text box that can be written in. The stop button is a Button with its own name, never a state of the send button.
- An input method's Enter never sends (CCM-13). This is a functional requirement of every language written through composition, not a refinement: a composer that sends on the Enter that chooses a candidate cannot be written in.
- The strip wraps and never scrolls sideways (CCM-43): no attachment is under an edge, every File item's actions are in view when focus reaches them (WCAG 2.4.7, 2.4.11), and at 200 % text in a width of 320 nothing needs scrolling in two directions (WCAG 1.4.10). The actions of the File items are Tab stops before the typing area, in the order drawn (WCAG 1.3.2, 2.4.3); a person who wants the text reaches it at once because focus() on the chat composer, a press on the capsule, sending and attaching all go to the typing area (CCM-5, CCM-16, CCM-25).
- A tile of a picture does not draw the file's name (CCM-44, FIT-8); the name stays the item's name for assistive technology, is drawn wherever no picture is, and an error's sentence is drawn beneath a tile in every case (FIT-4). A file the chat composer refused is still always a row (CCM-44): a sentence under a tile has the tile's width, and a row gives it the strip's.
- Dragging and pasting are never the only way to attach (WCAG 2.5.7): the attach button opens the same picker by keyboard, touch, switch and voice (CCM-23). What is dragged over the composer is the pointer user's own act and is not announced.
- Status is never color alone (WCAG 1.4.1): a File item has its words and a glyph that changes shape; the counter over the limit has its two numbers; the capsule in error has its edge where at rest it has none, and the typing area is exposed as invalid.
- The chat composer moves focus only where a control under the focus has gone or the person's act has ended: to the typing area after sending and after attaching, and to a neighbouring File item's remove action after an item is removed (CCM-25).
- Without script, and until the component is defined, on the web: the product's own text area and submit button inside the element are a working form (CCM-37). The text area needs its own accessible name (aria-label, or a label element), since the composer's label is not yet read. The capsule is not drawn until then.
- A fixture whose attachments is own and that holds file items cannot be drawn from props alone: each file item stands for one file the stage hands over, in order, by the platform's way of CCM-22, before anything is recorded — an image file of the stage's own for a name that ends in an image's extension. A file item of such a fixture sets no form: CCM-44 chooses it. A fixture's string in the attach slot is a glyph in an icon-only Button of the text variant; in the send and the stop slot, a labelled Button of the filled variant.
- The members' sentences are cited by their ids: ETI (the embedded text input), FIT (the File item) and FPK (the File picker). No sentence of the file upload is cited: the chat composer keeps its own files by the rule the file upload keeps its own by, said here in sentences of its own (CCM-22, CCM-38), because the file upload is not held and a sentence is cited only from a member. The holder's contract of the embedded text input (H1 to H7) is met by sentences of this spec: H1 the box, CCM-41; H2 the focus indication, CCM-42; H3 the mark of an error and the disabled look, CCM-27 and CCM-42; H4 the padding and the press passed on, CCM-2, CCM-3 and CCM-5; H5 the name and the description, CCM-19, CCM-28 and CCM-29; H6 what the action is, CCM-12; H7 forced colors, CCM-33.
生成自 specs/chat-composer.spec.json, tokens/sys/measure.tokens.json
- 它是一个以
label命名的组。输入区是一个同名的多行文本框。 - 无论输入区是怎样获得焦点的,胶囊的边缘都是它的焦点指示。按钮或文件项的操作显示它自己的焦点环,这时胶囊不画焦点边缘。
- Tab 按绘制的顺序在其中移动。输入区里没有任何按键会移除附件。
- 不能发送的发送按钮是一个禁用的按钮。是什么拦住了消息,显示在计数器和文件项上。
- 计数器是输入区的描述的第一部分。它不是实时区域。
- 一个礼貌(polite)的实时区域说出你的
announcement,以及附件发生的变化。 - 输入法的 Enter 绝不发送。
- 文本放大到 200%、宽度为 320 时,它按 stacked 布局。没有任何内容被截断,也没有任何内容横向滚动。
- 在强制颜色模式下,胶囊会多出一道边线,获得焦点时这道边线采用高亮颜色。
- 在任何语言里,它都没有自己的文字。
Web 上的已知差距:
- 输入法在 Chromium 中实际操作过,在 Firefox 和 Safari 中则只是用它们的事件模拟的。
- 屏幕阅读器会说什么,是从各浏览器的无障碍树里读出来的,还没有用屏幕阅读器实际听过。
- 给它一个
label,或者把它放进字段。 - 为它带来的按钮命名:
sendLabel、stopLabel、attachLabel、removeLabel、retryLabel。 - 说明消息后来怎样了。一句话有帮助的地方,在你的
send处理函数里设置announcement。 - 给每个失败的文件项一个
errorText,说明它为什么失败,以及该怎么办。