入力中インジケータ
stimeo--typing-indicator
「〇〇さんが入力中…」を、ほとんど自前の JavaScript なしで出します。
「〇〇さんが入力中」の表示を、相手の画面に出します。状態はブラウザの中ではなくサーバ側の通信路にあります。この要素の中で入力すると、自分の表示名を添えた合図が送られます。合図は間引かれるので、打鍵のたびに飛ぶことはありません。ほかの人から届いた合図は入力者ごとに追いかけ、しばらく止まった人は自動で消えます。自分の合図は自分の画面には出しません。サーバ側は届いた合図をその部屋へ流し直すだけで、状態もタイマーも持ちません。表示の文言は言語ごとに用意できます。
このページをもう 1 つのタブで開いて並べ、片方で入力してください。もう片方に表示が出ます。自分のタブには出ません。入力を止めると数秒で消えます。
あなたは ゲスト d9ec として他の人に表示されます。
キーボード操作
このコンポーネント自体はキーボード操作を持ちません。
<%# typing-indicator: the state lives on the server stream, not in client memory.
Typing in the composer broadcasts a throttled signal through TypingChannel;
every OTHER client shows "X is typing…" in the visible status slot and clears
it automatically after silence. Own echoes are suppressed by design, so open
this page in a second tab to see the indicator. The guest name is randomized
per render so each tab is a distinct "user" (a real app passes
current_user.name). The data-one/data-many templates keep the announcement
localizable; %{name}/%{names} are substituted by the controller. The status slot is
plain visible copy — assistive tech is reached through the page's shared announcer
instead, so the announce-one/announce-many values carry the spoken wording ({name}
syntax, not %{}). Leaving them off shows the indicator without announcing it. %>
<% guest = t("components.typing_indicator.demo.guest_name", token: SecureRandom.hex(2)) %>
<% announce_one = t("components.typing_indicator.demo.announce_one") %>
<% announce_many = t("components.typing_indicator.demo.announce_many") %>
<div class="typing-demo"
data-controller="stimeo--typing-indicator"
data-stimeo--typing-indicator-channel-value="TypingChannel"
data-stimeo--typing-indicator-params-value='{"room":"<%= TypingChannel::ROOM %>"}'
data-stimeo--typing-indicator-name-value="<%= guest %>"
data-stimeo--typing-indicator-timeout-value="3000"
data-stimeo--typing-indicator-throttle-value="1000"
data-stimeo--typing-indicator-announce-one-text-value="<%= announce_one %>"
data-stimeo--typing-indicator-announce-many-text-value="<%= announce_many %>">
<p class="typing-demo__hint"><%= t("components.typing_indicator.demo.hint") %></p>
<p class="typing-demo__you"><%= t("components.typing_indicator.demo.you_are", name: guest) %></p>
<label class="typing-demo__label" for="typing-demo-input">
<%= t("components.typing_indicator.demo.input_label") %>
</label>
<textarea id="typing-demo-input" class="demo-input typing-demo__input" rows="3"
data-stimeo--typing-indicator-target="input"></textarea>
<p class="typing-demo__status"
data-stimeo--typing-indicator-target="status"
data-one="<%= t("components.typing_indicator.demo.one") %>"
data-many="<%= t("components.typing_indicator.demo.many") %>"></p>
</div>
/*
* Presentation-only styles for the typing-indicator demo. The library fills the
* status live-region text and flips data-typing on the root; this CSS lays out
* the composer and makes the "someone is typing" state visible (animated dots
* are decorative — the announcement itself is the status text, per WCAG 4.1.3).
*/
.typing-demo {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 28rem;
}
.typing-demo__hint,
.typing-demo__you {
margin: 0;
font-size: 0.9rem;
color: var(--muted);
}
.typing-demo__label {
font-size: 0.9rem;
}
.typing-demo__input {
resize: vertical;
}
.typing-demo__status {
margin: 0;
min-height: 1.25rem;
font-size: 0.9rem;
color: var(--muted);
}
/* While others are typing (data-typing="true" on the root), accent the status
line and append decorative pulsing dots after the announced text. */
.typing-demo[data-typing="true"] .typing-demo__status {
color: var(--accent-700);
}
.typing-demo[data-typing="true"] .typing-demo__status::after {
content: "…";
animation: typing-demo-pulse 1s ease-in-out infinite;
}
@keyframes typing-demo-pulse {
0%,
100% {
opacity: 0.2;
}
50% {
opacity: 1;
}
}
/* Respect reduced-motion preferences: keep the dots static. */
@media (prefers-reduced-motion: reduce) {
.typing-demo[data-typing="true"] .typing-demo__status::after {
animation: none;
}
}
このデモに固有の消費側 JS はありません(挙動はコントローラが担います)。
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--typing-indicator"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
input
|
コンポーザの任意マーカー。入力は要素への委譲リスナで拾うため、この target が無くても配下のどの入力でも発火する。 | data-stimeo--typing-indicator-target="input" |
status
|
入力中の相手を表示する可視スロット(ライブリージョンではない)。data-one / data-many テンプレート(%{name} / %{names} / %{count})でローカライズ。読み上げは共有 Announcer が担う。 |
data-stimeo--typing-indicator-target="status" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
channel
|
購読する Action Cable チャンネル名。空(既定)なら購読も送信もしない。 | data-stimeo--typing-indicator-channel-value |
params
|
購読識別子に混ぜる追加パラメータ(例 {"room":"chat_42"})。 |
data-stimeo--typing-indicator-params-value |
name
|
このクライアントの表示名。信号に載り、同名の受信はエコーとして無視される。 | data-stimeo--typing-indicator-name-value |
timeout
|
最後の信号からこの ms 沈黙した入力者を自動で消す(既定 3000)。信号のたびにタイマーは再スタート。 | data-stimeo--typing-indicator-timeout-value |
throttle
|
送信の最小間隔 ms(先頭エッジ。既定 2000)。 | data-stimeo--typing-indicator-throttle-value |
announceOneText
|
1 人が入力中のときに共有 Announcer へ送る文言({name})。空(既定)なら告知しない。 |
data-stimeo--typing-indicator-announce-one-text-value |
announceManyText
|
複数人が入力中のときに共有 Announcer へ送る文言({names} / {count})。空(既定)なら告知しない。 |
data-stimeo--typing-indicator-announce-many-text-value |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
入力中の相手の集合が変わるたび(追加・自動クリア)に { names } と共に発火。 |
stimeo--typing-indicator:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-typing |
コントローラ要素 | 他クライアントが入力中なら "true"、全員止まると "false"(初期は無し)。利用側 CSS の表示切替フック。 |