ダイレクトアップロードの進捗
stimeo--direct-upload
アップロードの進み具合をファイルごとに出し、終了と失敗を読み上げます。
Rails のダイレクトアップロードの進み具合を、ファイルごとの行にして表示します。行はテンプレートから複製されるので、見た目は自由に作れます。進み具合はパーセントの文字と CSS の値の両方で出るので、バーの塗りも書けます。スクリーンリーダーからも今どこまで進んだかを確かめられます。ファイル名はイベントから受け取って行に入ります。失敗した行は、あとから届く終了の知らせで成功に上書きされません。全体の進み具合も別に持ち、すべて終われば消えます。終了と失敗はページ共通の窓口から読み上げられ、文言は利用側が決められます。途中の進み具合を変わるたびに読み上げることはしないので、騒がしくなりません。
キーボード操作
このコンポーネント自体はキーボード操作を持ちません。
<%# Direct upload demo: there is no server, so demo.js fires the ActiveStorage
direct-upload:* events to drive the rows — including one upload that fails
(error followed by end, the order ActiveStorage really uses). The library
clones the row template per file, updates aria-valuenow / aria-valuetext /
data-upload-state / the --stimeo--upload-progress var, and hands completion
and failure announcements to the shared stimeo--announcer your app seats once,
in its layout (completion and failure are the transitions worth reading;
per-tick progress stays on each row's aria-valuenow). This demo styles the
bars. %>
<% done_text = t("components.direct_upload.demo.done") %>
<% error_text = t("components.direct_upload.demo.error") %>
<div class="direct-upload-demo">
<button type="button" class="demo-trigger" data-direct-upload-start>
<%= t("components.direct_upload.demo.start") %>
</button>
<div
class="direct-upload"
data-controller="stimeo--direct-upload"
data-stimeo--direct-upload-announce-done-text-value="<%= done_text %>"
data-stimeo--direct-upload-announce-error-text-value="<%= error_text %>">
<div class="direct-upload__list" data-stimeo--direct-upload-target="list"></div>
<template data-stimeo--direct-upload-target="row">
<div class="direct-upload__row" role="progressbar" aria-valuemin="0" aria-valuemax="100">
<span class="direct-upload__name" data-field="name"></span>
<span class="direct-upload__track"><span class="direct-upload__bar"></span></span>
<span class="direct-upload__percent" data-field="percent"></span>
</div>
</template>
</div>
</div>
/*
* Presentation-only styles for the direct-upload demo.
* The library sets aria-valuenow, data-upload-state, and the
* --stimeo--upload-progress custom property; this CSS draws the bar from that var
* and colors the done / error states.
*/
.direct-upload-demo {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 30rem;
}
.direct-upload__list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.direct-upload__row {
display: grid;
grid-template-columns: 8rem 1fr 3rem;
align-items: center;
gap: 0.5rem;
}
.direct-upload__name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.85rem;
}
.direct-upload__track {
height: 0.5rem;
border-radius: 999px;
background: var(--border);
overflow: hidden;
}
.direct-upload__bar {
display: block;
height: 100%;
width: var(--stimeo--upload-progress, 0%);
background: var(--accent);
transition: width 0.2s ease;
}
.direct-upload__row[data-upload-state="done"] .direct-upload__bar {
background: var(--leaf-500);
}
.direct-upload__row[data-upload-state="error"] .direct-upload__bar {
background: var(--danger-500);
}
.direct-upload__percent {
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
text-align: right;
}
/* Non-color state cues (WCAG 1.4.1); progressbar children are presentational,
* so the glyphs stay out of the accessibility tree. */
.direct-upload__row[data-upload-state="done"] .direct-upload__percent::after {
content: " ✓";
color: var(--leaf-500);
}
.direct-upload__row[data-upload-state="error"] .direct-upload__percent::after {
content: " ⚠";
color: var(--danger-500);
}
// Direct upload demo (consumer-side JS).
//
// There is no server here, so this fires the ActiveStorage direct-upload:* events
// the controller subscribes to. Real apps get these from @rails/activestorage.
// Two uploads complete (initialize, progress to 100, end); the third fails midway
// with error followed by end — the order ActiveStorage really dispatches — so the
// errored row demonstrably survives the trailing end event.
document.querySelectorAll(".direct-upload-demo").forEach((root) => {
const startButton = root.querySelector("[data-direct-upload-start]");
if (!startButton) return;
const fire = (type, detail) => {
// cancelable mirrors ActiveStorage; the library cancels a rendered
// direct-upload:error to suppress the native alert().
document.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, cancelable: true }));
};
let run = 0;
startButton.addEventListener("click", () => {
run += 1;
const uploads = [
{ name: `photo-${run}.jpg`, failAt: null },
{ name: `notes-${run}.pdf`, failAt: null },
{ name: `report-${run}.docx`, failAt: 60 },
];
uploads.forEach(({ name, failAt }, index) => {
const id = `${run}-${index}`;
const file = { name };
fire("direct-upload:initialize", { id, file });
let percent = 0;
const timer = window.setInterval(() => {
percent += 20;
if (failAt !== null && percent >= failAt) {
window.clearInterval(timer);
fire("direct-upload:error", { id, file, error: "Network error" });
fire("direct-upload:end", { id, file });
return;
}
fire("direct-upload:progress", { id, file, progress: percent });
if (percent >= 100) {
window.clearInterval(timer);
fire("direct-upload:end", { id, file });
}
}, 300);
});
});
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--direct-upload"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
list
必須
|
進捗行の挿入先。 | data-stimeo--direct-upload-target="list" |
row
必須
|
ファイルごとに複製する <template>。 |
data-stimeo--direct-upload-target="row" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
removeOnDone
|
完了行を一定後に消すか(既定 false)。 |
data-stimeo--direct-upload-remove-on-done-value |
announceDoneText
|
完了時に共有 Announcer へ送る文言。{name} をファイル名へ展開。空なら通知しない。 |
data-stimeo--direct-upload-announce-done-text-value |
announceErrorText
|
失敗時に共有 Announcer へ送る文言。{name} をファイル名へ展開。空なら通知しない。 |
data-stimeo--direct-upload-announce-error-text-value |
scope
|
所有フォーム/ルートのセレクタ。その配下の input からのイベントのみ処理する。解釈できないセレクタは既定値(全イベント)へ落ちる。 |
data-stimeo--direct-upload-scope-value |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
progress
|
進捗更新ごとに発火。detail.id / detail.percent(クランプ後)を伴う。 |
stimeo--direct-upload:progress |
done
|
1 ファイルの成功完了時に発火。detail.id を伴う。失敗した行には発火しない。 |
stimeo--direct-upload:done |
error
|
失敗時に発火。detail.id / detail.error を伴う。行として描画できた失敗は ActiveStorage の alert() を抑止し、描画できなければ alert() が安全網として残る。 |
stimeo--direct-upload:error |
reconcile
|
Turbo キャッシュの巻き戻しが進行中のアップロード行を捨てたとき発火。detail に捨てた ids。 |
stimeo--direct-upload:reconcile |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-valuenow / aria-valuetext |
各行 | 行の進捗(0–100 に丸め・クランプ / "42%"。生成時は 0 / "0%")。 |
aria-label |
各行 | アクセシブル名=ファイル名(テンプレート authored が優先)。 |
data-upload-state |
各行 | "uploading" / "done" / "error"。端末状態(done / error)は後続イベントで変わらない。 |
data-upload-progress |
コントローラ要素 | 全行の集計進捗(0–100)。行の追加・更新・削除で再計算し、0 件で撤去。 |
--stimeo--upload-progress |
各行・要素 | バー描画用の進捗率。全体側は行が 0 件になったら撤去。 |