入力中インジケータ
stimeo--typing-indicator
Action Cable で届く「X が入力中…」— ほぼゼロ JS のリアルタイム表示。
stimeo--typing-indicator は server-bound(サーバ連動)カテゴリの第 1 号です。状態はクライアントのメモリではなく、サーバの Action Cable ストリーム上に住みます。要素配下のどこかで入力すると、スロットリング(throttle ms に 1 回・先頭エッジ)付きで自分の表示名を載せた typing 信号を broadcast します。他クライアントから受けた信号は入力者ごとに追跡され、 status ライブリージョンに「X が入力中…」と表示、timeout ms 沈黙した入力者は自動で消えます。自分のエコーは name の比較で抑制します。サーバ側は受けた信号を部屋へ rebroadcast するだけの薄いチャンネル 1 枚(このカタログの TypingChannel は仕様のサンプルに公開デプロイ用の固定 room allowlist を足したもの)で、状態もタイマーも持ちません。表示文言は data-one / data-many テンプレートでローカライズでき、data-typing フックが見た目の切替を担います。opt-in サブパス stimeo-ui/cable に同梱(@rails/actioncable は optional peer。コアはゼロ依存のまま)で、既存アプリの consumer は setCableConsumer() で共有できます。認証とメッセージ本文の送受信はスコープ外です。
このページをもう 1 つのタブで開いて並べ、片方のコンポーザで入力してください。もう片方のタブにインジケータが表示されます(自分のタブには出ません — 自分のエコーは設計どおり抑制されます)。入力を止めると約 3 秒で消えます。
あなたは ゲスト ce12 として他の人に表示されます。
<%# 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 status live region 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. %>
<% guest = t("components.typing_indicator.demo.guest_name", token: SecureRandom.hex(2)) %>
<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">
<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" role="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);
}
.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
|
入力中の相手を表示するライブリージョン。role="status"(または aria-live)は作者が付与。data-one / data-many テンプレート(%{name} / %{names} / %{count})でローカライズ。 |
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 |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
入力中の相手の集合が変わるたび(追加・自動クリア)に { names } と共に発火。 |
stimeo--typing-indicator:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-typing |
コントローラ要素 | 他クライアントが入力中なら "true"、全員止まると "false"(初期は無し)。利用側 CSS の表示切替フック。 |