フレーム読込状態
stimeo--frame-loading
turbo-frame の読込中に skeleton 表示+aria-busy を制御し、操作抑止とフォーカス退避を行う。
stimeo--frame-loading コントローラは <turbo-frame> の読込状態を管理します。自フレームで Turbo のフェッチライフサイクルを購読し、turbo:before-fetch-request(フレーム内のリンク/フォーム、またはフレーム自身からバブルする分)で読込開始、turbo:frame-load で完了、turbo:fetch-request-error を安全網とします。読込中は aria-busy / data-frame-loading を付与し、任意の skeleton / overlay ターゲットの hidden を切替、content ターゲットを inert 化して二重操作を抑止し、古くなった content からフォーカスを退避します(restoreFocus 時は完了後に復帰)。minDuration は skeleton の最小表示時間を担保しチラつきを防ぎます。挙動のみで skeleton のマークアップや装飾は持たず(Skeleton / CSS と組む)、読込状態は aria-busy / data-frame-loading とターゲットの hidden にのみ保持します。リスナと最小表示タイマは disconnect(Turbo 遷移含む)で解除し、キャッシュされたフレームが busy のまま残らないようフックも後始末します。
読み込み済みの内容。
<%# Frame-loading demo: the controller subscribes to a turbo-frame's fetch lifecycle
and toggles aria-busy / data-frame-loading, the skeleton, content inert, and focus.
This catalog has no Turbo backend, so demo.js fires the turbo:before-fetch-request →
turbo:frame-load pair to reproduce a frame fetch. The library only toggles state and
hidden; demo.css owns the skeleton bars and the dimmed content. %>
<div class="frame-demo">
<button type="button" class="demo-trigger" data-frame-demo-reload>
<%= t("components.frame_loading.demo.reload") %>
</button>
<div
class="frame-demo__frame"
data-controller="stimeo--frame-loading"
data-stimeo--frame-loading-min-duration-value="600">
<div class="frame-demo__skeleton" data-stimeo--frame-loading-target="skeleton" hidden>
<span class="visually-hidden"><%= t("components.frame_loading.demo.loading") %></span>
<span class="frame-demo__bar"></span>
<span class="frame-demo__bar"></span>
<span class="frame-demo__bar"></span>
</div>
<div class="frame-demo__content" data-stimeo--frame-loading-target="content">
<p><%= t("components.frame_loading.demo.content") %></p>
<button
type="button"
class="demo-trigger"
data-frame-demo-action
data-result-template="<%= t("components.frame_loading.demo.action_result") %>">
<%= t("components.frame_loading.demo.action") %>
</button>
<span class="frame-demo__result" data-frame-demo-result role="status"></span>
</div>
</div>
</div>
/*
* Presentation-only styles for the frame-loading demo. The library toggles aria-busy /
* data-frame-loading, the skeleton's hidden, and inert on the content; this CSS owns the
* skeleton bars and dims the (inert) content while loading.
*/
.frame-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 28rem;
align-items: flex-start;
}
.frame-demo__frame {
position: relative;
width: 100%;
padding: 1rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
}
.frame-demo__skeleton {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.frame-demo__bar {
height: 0.75rem;
border-radius: 0.25rem;
background: linear-gradient(
90deg,
var(--surface-subtle) 25%,
var(--surface-subtle) 50%,
var(--surface-subtle) 75%
);
background-size: 200% 100%;
animation: frame-demo-shimmer 1.2s ease-in-out infinite;
}
.frame-demo__bar:nth-child(3) {
width: 80%;
}
.frame-demo__bar:nth-child(4) {
width: 60%;
}
/* While loading, dim the stale (inert) content so the skeleton reads as the live state. */
.frame-demo__frame[data-frame-loading] .frame-demo__content {
opacity: 0.4;
}
.frame-demo__result {
margin-left: 0.5rem;
font-size: 0.85rem;
color: var(--color-text-muted);
}
@keyframes frame-demo-shimmer {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.frame-demo__bar {
animation: none;
}
}
// Frame-loading demo (consumer-side JS).
//
// The catalog has no Turbo backend, so the reload button fires the same fetch events a
// real <turbo-frame> would (turbo:before-fetch-request to start, turbo:frame-load to
// finish ~1.2s later). The controller reacts exactly as it would in production: it sets
// aria-busy, shows the skeleton, inerts the content, and retreats/restores focus.
document.querySelectorAll(".frame-demo").forEach((root) => {
const frame = root.querySelector('[data-controller="stimeo--frame-loading"]');
const reload = root.querySelector("[data-frame-demo-reload]");
if (!frame || !reload) return;
// Idempotent: Turbo can re-run this inline module on navigation; wire each root once.
if (root.dataset.demoWired) return;
root.dataset.demoWired = "1";
reload.addEventListener("click", () => {
frame.dispatchEvent(new Event("turbo:before-fetch-request", { bubbles: true }));
setTimeout(() => {
frame.dispatchEvent(new Event("turbo:frame-load", { bubbles: true }));
}, 1200);
});
// The in-content action button gives visible feedback so its inert-while-loading
// state is observable: it responds when the frame is idle, but not while loading
// (the controller marks the content inert, which swallows the click).
const action = root.querySelector("[data-frame-demo-action]");
const result = root.querySelector("[data-frame-demo-result]");
if (action && result) {
const template = action.dataset.resultTemplate ?? "Ran the action ({n}×)";
let runs = 0;
action.addEventListener("click", () => {
runs += 1;
result.textContent = template.replace("{n}", String(runs));
});
}
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--frame-loading"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
content
|
現在の内容。読込中は inert 化し、フォーカス復帰の起点にもなる。 |
data-stimeo--frame-loading-target="content" |
skeleton
|
読込中に表示する任意のプレースホルダ(hidden を切替)。 |
data-stimeo--frame-loading-target="skeleton" |
overlay
|
読込中に表示する任意のオーバーレイ(hidden を切替)。 |
data-stimeo--frame-loading-target="overlay" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
minDuration
|
skeleton を表示し続ける最小ミリ秒(チラつき防止。既定 0)。 | data-stimeo--frame-loading-min-duration-value |
restoreFocus
|
読込完了後に元の位置へフォーカスを戻すか(既定 true)。 |
data-stimeo--frame-loading-restore-focus-value |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
start
|
読込開始時に発火。 | stimeo--frame-loading:start |
end
|
読込完了時に発火。 | stimeo--frame-loading:end |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-busy |
フレーム(ルート) | 読込中 true。 |
data-frame-loading |
フレーム(ルート) | 読込中 true。CSS で skeleton / overlay を制御するため。 |