続きを読む
stimeo--read-more
長い本文を数行に畳み、続きを開けるようにします。収まっているときはボタンを出しません。
長い本文を数行に畳んで、「続きを読む」で開けるようにします。何行で畳むかは利用側の CSS です。この部品は、本文が本当にはみ出しているかどうかを測ります。はみ出していなければボタンを出さないので、開いても何も変わらないボタンを押させません。幅が変わればもう一度測り直します。畳んでいる間も本文はそのまま残っているので、読み上げには全文が届きます。
Stimeo は Ruby on Rails 向けのヘッドレス Stimulus UI フレームワークです。提供するのは挙動のみ——ロール・キーボード操作・ライブリージョン通知を備えたアクセシブルで属性駆動のコンポーネントを提供し、見た目はすべて利用側に委ねます。同梱 CSS もランタイム依存もありません。マークアップと見た目はあなたが用意し、Stimeo は正しくテストされたアクセシビリティの意味論をその下で提供します。
キーボード操作
| キー | 動作 |
|---|---|
| Enter / Space | 省略表示と全文表示を切り替える(ネイティブボタン)。 |
<%# Markup for the read-more demo.
The body is visually truncated with a CSS line clamp, and a toggle switches to the
full text. The toggle carries both wordings: the library shows the half that belongs
to the current state and hides the other. It also syncs aria-expanded / data-state and
hides the whole toggle when there is no overflow (so no needless button appears).
The clamp itself lives in demo.css. %>
<div class="read-more" data-controller="stimeo--read-more">
<p
class="read-more__content"
id="read-more-demo-content"
data-state="collapsed"
data-stimeo--read-more-target="content">
<%= t("components.read_more.demo.body") %>
</p>
<button
class="read-more__trigger"
type="button"
aria-expanded="false"
aria-controls="read-more-demo-content"
hidden
data-stimeo--read-more-target="trigger"
data-action="click->stimeo--read-more#toggle">
<span data-stimeo--read-more-target="collapsedLabel">
<%= t("components.read_more.demo.trigger") %>
</span>
<span data-stimeo--read-more-target="expandedLabel" hidden>
<%= t("components.read_more.demo.trigger_expanded") %>
</span>
</button>
</div>
<%# Readout: what the component just reported. Purely visual — the component
already announces what a screen reader needs, so this line is aria-hidden to
keep it from being read twice. The value is a code fragment (API identifiers
and values), so only the label is translated. %>
<p class="demo-readout" aria-hidden="true">
<span><%= t("catalog.readout.events") %></span>
<span class="demo-readout__value" data-readout>—</span>
</p>
/*
* Presentation-only styles for the read-more demo.
* The visual truncation (line clamp) is applied only when data-state="collapsed".
* The library toggles the trigger's visibility (hidden) based on overflow detection, and
* swaps the two label halves inside it, so neither the trigger's wording nor which half
* shows is styled here.
*/
.read-more {
max-width: 32rem;
}
.read-more__content {
margin: 0 0 0.5rem;
line-height: 1.6;
}
/* Clamp to 3 lines while collapsed. Line-based, so it follows text zoom (WCAG 1.4.4). */
.read-more__content[data-state="collapsed"] {
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.read-more__content[data-state="expanded"] {
-webkit-line-clamp: none;
line-clamp: none;
}
.read-more__trigger {
padding: 0.25rem 0;
background: transparent;
border: 0;
font: inherit;
font-weight: 600;
color: var(--accent-700);
cursor: pointer;
}
// Every move of the open state is reported, with a `reason` saying what drove it
// — a trigger, Escape, a click outside, or a call from your own code. A real app
// subscribes to mirror the state elsewhere or to send an analytics event; this
// demo writes the event name and its reason into the readout so both are visible
// while you interact. `reason: "api"` is the one to watch for: a close you asked
// for comes back to your own listener.
// The page chrome uses some of these controllers too (the nav, the tab strip),
// so everything below is scoped to the demo itself.
const demoStage = document.querySelector(".demo-stage") ?? document;
const stateReadout = demoStage.querySelector("[data-readout]");
const stateRoot = demoStage.querySelector("[data-controller~='stimeo--read-more']");
for (const name of ["open", "close"]) {
stateRoot?.addEventListener(`stimeo--read-more:${name}`, (event) => {
const reason = event.detail?.reason;
if (stateReadout) {
stateReadout.textContent = reason
? `stimeo--read-more:${name} · reason: ${reason}`
: `stimeo--read-more:${name}`;
}
});
}
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--read-more"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
content
必須
|
クランプされるテキスト要素。data-state(collapsed/expanded)が利用側の視覚的クランプを切り替える。 |
data-stimeo--read-more-target="content" |
trigger
必須
|
「続きを読む」ボタン。テキストが溢れない限り非表示。aria-expanded を持つ。 |
data-stimeo--read-more-target="trigger" |
expandedLabel
|
展開中に出るトリガーの側。hidden は部品が持つ。同じトリガーの中の collapsedLabel と対になる。 |
data-stimeo--read-more-target="expandedLabel" |
collapsedLabel
|
折りたたみ中に出るトリガーの側。hidden は部品が持つ。同じトリガーの中の expandedLabel と対になる。 |
data-stimeo--read-more-target="collapsedLabel" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
collapsed
|
connect 時に反映する初期の折りたたみ(クランプ)状態(既定 true)。 | data-stimeo--read-more-collapsed-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
toggle
|
折りたたみ(クランプ)と展開を切り替え、オーバーフローを再判定する。 | stimeo--read-more#toggle |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
close
|
畳んだ直後に発火。detail の reason に、何がそうさせたかを載せる。 | stimeo--read-more:close |
open
|
展開した直後に発火。detail の reason に、何がそうさせたかを載せる。 | stimeo--read-more:open |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-expanded |
トリガー | 展開で "true"、折りたたみで "false"。 |
data-state |
中身 | "collapsed" / "expanded"。CSS の行クランプを制御。 |
hidden |
トリガー | 本文が収まっている(オーバーフロー無し)とき付与し、トグルを隠す。 |