Direct Upload の進捗
stimeo--direct-upload
ActiveStorage Direct Upload の進捗をファイル別に表示し、aria と status で通知する。
stimeo--direct-upload コントローラは ActiveStorage の direct-upload:* イベント(initialize / progress / error / end。document までバブリングする)を購読し、row テンプレートを複製してファイル別の進捗行を描画します。aria-valuenow / aria-valuetext、[data-field="percent"] のテキスト、--stimeo-upload-progress カスタムプロパティを更新し、data-upload-state を done / error に切り替え、全体進捗を data-upload-progress に反映します。完了・失敗は任意の status ライブリージョンへ利用側の doneLabel / errorLabel(%{name} をファイル名へ置換)で通知し、毎ティックの進捗は progressbar の aria-valuenow で伝えて読み上げ氾濫を避けます。ライブラリは挙動のみで、バーは描画せず、アップロード自体は @rails/activestorage が担います。リスナは disconnect で解除します。
<%# Direct upload demo: there is no server, so demo.js fires the ActiveStorage
direct-upload:* events to drive the rows. The library clones the row template per
file, updates aria-valuenow / data-upload-state / the --stimeo-upload-progress
var, and announces completion into the status live region. This demo styles the
bars; the sr-only status carries the announcement. %>
<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-done-label-value="<%= t('components.direct_upload.demo.done') %>"
data-stimeo--direct-upload-error-label-value="<%= t('components.direct_upload.demo.error') %>">
<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>
<span class="visually-hidden" data-stimeo--direct-upload-target="status"
aria-live="polite"></span>
</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;
}
// Direct upload demo (consumer-side JS).
//
// There is no server here, so this fires the ActiveStorage direct-upload:* events
// the controller subscribes to — initialize, progress (stepping to 100), then end —
// to drive the progress rows. Real apps get these from @rails/activestorage.
document.querySelectorAll(".direct-upload-demo").forEach((root) => {
const startButton = root.querySelector("[data-direct-upload-start]");
if (!startButton) return;
const fire = (type, detail) => {
document.dispatchEvent(new CustomEvent(type, { detail, bubbles: true }));
};
let run = 0;
startButton.addEventListener("click", () => {
run += 1;
const files = [`photo-${run}.jpg`, `notes-${run}.pdf`];
files.forEach((name, index) => {
const id = `${run}-${index}`;
fire("direct-upload:initialize", { id, file: { name } });
let percent = 0;
const timer = window.setInterval(() => {
percent += 20;
fire("direct-upload:progress", { id, progress: percent });
if (percent >= 100) {
window.clearInterval(timer);
fire("direct-upload:end", { id });
}
}, 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" |
status
|
完了/失敗を通知する任意の aria-live 領域。 |
data-stimeo--direct-upload-target="status" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
announce
|
完了/失敗を status へ通知するか(既定 true)。 |
data-stimeo--direct-upload-announce-value |
removeOnDone
|
完了行を一定後に消すか(既定 false)。 |
data-stimeo--direct-upload-remove-on-done-value |
doneLabel
|
完了時の文言。%{name} をファイル名へ置換。空なら通知しない。 |
data-stimeo--direct-upload-done-label-value |
errorLabel
|
失敗時の文言。%{name} をファイル名へ置換。空なら通知しない。 |
data-stimeo--direct-upload-error-label-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 を伴う。 |
stimeo--direct-upload:error |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-valuenow / aria-valuetext |
各行 | 行の進捗(0–100 / "42%")。 |
data-upload-state |
各行 | "uploading" / "done" / "error"。 |
data-upload-progress |
コントローラ要素 | 全行の集計進捗(0–100)。 |
--stimeo-upload-progress |
各行・要素 | バー描画用の進捗率。 |