ファイルドロップゾーン
stimeo--file-dropzone
クリック / キーボード / ドラッグ&ドロップでファイルを選び、画像をプレビューする。
stimeo--file-dropzone コントローラは、ネイティブの file input とドロップ領域を橋渡しする。trigger ボタンはネイティブのファイル選択ダイアログを開き(キーボード操作可)、領域へファイルをドラッグすると追加され、data-dragover フラグとライブリージョンの文言でドラッグ状態を示す(色だけに依存しない)。各ファイルは accept・maxSize・枚数(maxFiles、 input が multiple でなければ 1)で検証し、拒否時は stimeo--file-dropzone:reject を発火して data-…-invalid を付与する。受理ファイルはテンプレートから Remove {name} ボタンと画像サムネイル(objectURL)付きで生成し、変化のたびに stimeo--file-dropzone:change を現在の File 配列付きで発火する。削除時は objectURL を解放してフォーカスを隣(無ければ trigger)へ移し、disconnect で残りの objectURL をすべて解放する。アップロード通信は利用側の責務。
キーボード操作
| キー | 動作 |
|---|---|
| Enter / Space | trigger ボタンからネイティブのファイル選択ダイアログを開く。 |
<%# Markup for the file-dropzone (file drag & drop / image preview) demo.
Click or keyboard launches the native file input, and drag & drop also adds files.
It validates accept / maxSize / count and shows image thumbnails via objectURL.
The library handles the drop bridge, validation, preview create/revoke, focus
handoff on removal, and live announcements. Drag & drop is only an aid — selection
is always possible via click / keyboard. %>
<div class="file-dropzone" data-controller="stimeo--file-dropzone"
data-stimeo--file-dropzone-max-size-value="5242880"
data-stimeo--file-dropzone-max-files-value="4"
data-stimeo--file-dropzone-drag-label-value="<%= t(
'components.file_dropzone.demo.drag_label'
) %>">
<div
class="file-dropzone__zone"
data-stimeo--file-dropzone-target="zone"
data-action="dragover->stimeo--file-dropzone#onDragOver
dragleave->stimeo--file-dropzone#onDragLeave
drop->stimeo--file-dropzone#onDrop">
<button
type="button"
class="file-dropzone__trigger"
data-stimeo--file-dropzone-target="trigger"
data-action="click->stimeo--file-dropzone#openDialog">
<%= t("components.file_dropzone.demo.trigger") %>
</button>
<input
type="file"
accept="image/*"
multiple
aria-label="<%= t('components.file_dropzone.demo.input_label') %>"
class="file-dropzone__input visually-hidden"
data-stimeo--file-dropzone-target="input"
data-action="change->stimeo--file-dropzone#onChange" />
</div>
<ul
class="file-dropzone__list"
aria-label="<%= t('components.file_dropzone.demo.list_label') %>"
data-stimeo--file-dropzone-target="list"></ul>
<%# Demo-only: the library accepts only images within the limits (5 MB / 4 files) and
fires stimeo--file-dropzone:reject for the rest — accepted images get a thumbnail
preview. Surface the reason visibly so a rejected file (e.g. an over-size photo) is
not mistaken for a broken preview. demo.js fills this from the reject event; the
reason copy is localized here. %>
<p class="file-dropzone__error"
data-file-dropzone-error
hidden
data-reason-type="<%= t('components.file_dropzone.demo.reject_type') %>"
data-reason-size="<%= t('components.file_dropzone.demo.reject_size') %>"
data-reason-count="<%= t('components.file_dropzone.demo.reject_count') %>"></p>
<span
role="status"
aria-live="polite"
class="file-dropzone__status visually-hidden"
data-stimeo--file-dropzone-target="status"></span>
<template data-stimeo--file-dropzone-target="itemTemplate">
<li class="file-dropzone__item" data-stimeo--file-dropzone-target="item">
<img class="file-dropzone__thumb" data-file-dropzone-slot="thumb" alt="" hidden />
<span class="file-dropzone__name" data-file-dropzone-slot="name"></span>
<%# Removal is handled by a delegated listener on the list container, so it
works instantly without waiting on Stimulus wiring a data-action onto the
dynamically-added button. %>
<button
type="button"
class="file-dropzone__remove">
<%= t("components.file_dropzone.demo.remove") %>
</button>
</li>
</template>
</div>
/*
* Presentation-only styles for the file-dropzone demo.
* The library toggles the zone's data-dragover for drag state and
* data-stimeo--file-dropzone-invalid for a rejected validation (announced in a
* live region too, never via color alone). Creating/revoking previews and moving
* focus on removal are also the library's responsibility.
*/
.file-dropzone {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 30rem;
}
.file-dropzone__zone {
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
border: 2px dashed var(--border-strong);
border-radius: 0.625rem;
background: var(--surface-subtle);
text-align: center;
}
.file-dropzone__zone[data-dragover] {
border-color: var(--accent, var(--color-primary));
background: var(--color-primary-soft);
}
.file-dropzone__zone[data-stimeo--file-dropzone-invalid] {
border-color: var(--danger-500);
background: var(--danger-50);
}
.file-dropzone__trigger {
padding: 0.5rem 0.9rem;
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
background: var(--surface, var(--surface-card));
color: var(--fg, var(--color-text));
font: inherit;
font-weight: 600;
cursor: pointer;
}
.file-dropzone__list {
display: flex;
flex-direction: column;
gap: 0.4rem;
margin: 0;
padding: 0;
list-style: none;
}
.file-dropzone__item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.4rem 0.6rem;
border: 1px solid var(--border-default);
border-radius: 0.375rem;
}
.file-dropzone__thumb {
width: 2.5rem;
height: 2.5rem;
object-fit: cover;
border-radius: 0.25rem;
}
.file-dropzone__name {
flex: 1;
font-size: 0.875rem;
color: var(--fg, var(--color-text));
word-break: break-all;
}
.file-dropzone__remove {
border: 0;
background: transparent;
color: var(--danger-500);
font: inherit;
cursor: pointer;
}
.file-dropzone__remove:focus-visible {
outline: 2px solid var(--accent, var(--color-primary));
outline-offset: 2px;
border-radius: 0.25rem;
}
/* Visible reason for a rejected file (filled by demo.js from the reject event). */
.file-dropzone__error {
margin: 0;
font-size: 0.875rem;
color: var(--danger-500);
}
// Demo: the library validates every file against accept / maxSize / maxFiles and fires
// stimeo--file-dropzone:reject for the ones it turns away — accepted images get a
// thumbnail preview. Surface the reason both visibly and in the live status region, so a
// rejected file (e.g. an image over the 5 MB limit) is not mistaken for a broken preview.
// The reason copy is localized in the view; reason is one of type / size / count and is
// read straight off data-reason-<reason> (no dataset camelCase round-trip).
document.querySelectorAll('[data-controller~="stimeo--file-dropzone"]').forEach((root) => {
const error = root.querySelector("[data-file-dropzone-error]");
const status = root.querySelector('[data-stimeo--file-dropzone-target="status"]');
if (!error) return;
root.addEventListener("stimeo--file-dropzone:reject", (event) => {
const { file, reason } = event.detail;
const detail = error.getAttribute(`data-reason-${reason}`);
if (!detail) return;
const message = `${file.name}: ${detail}`;
error.textContent = message;
error.hidden = false;
// Mirror the reason into the live region so screen-reader users hear *why* a file was
// rejected, not just the file name the library announces there.
if (status) status.textContent = message;
});
// A successful add clears the last rejection notice.
root.addEventListener("stimeo--file-dropzone:change", () => {
error.hidden = true;
error.textContent = "";
});
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--file-dropzone"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
zone
|
ドロップ対象領域。ドラッグ中は data-dragover、拒否時は data-…-invalid が付く。 |
data-stimeo--file-dropzone-target="zone" |
trigger
必須
|
ネイティブのファイルダイアログを開くボタン。削除後のフォーカス戻り先にもなる。 | data-stimeo--file-dropzone-target="trigger" |
input
必須
|
ネイティブの <input type=file>。キーボード操作可能な主要アップロード経路。 |
data-stimeo--file-dropzone-target="input" |
list
|
受理済みファイルのプレビュー項目を描画するコンテナ。 | data-stimeo--file-dropzone-target="list" |
item
|
受理された 1 ファイルのプレビュー項目。 | data-stimeo--file-dropzone-target="item" |
itemTemplate
|
各ファイル項目(名前・サムネイル・削除ボタン)を生成する複製元の <template>。 |
data-stimeo--file-dropzone-target="itemTemplate" |
status
|
ドラッグのヒントや追加・削除を読み上げる live 領域。 | data-stimeo--file-dropzone-target="status" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
maxSize
|
許可する最大ファイルサイズ(バイト、0 = 無制限、既定 0)。 | data-stimeo--file-dropzone-max-size-value |
maxFiles
|
最大ファイル数(0 = input の multiple/単一の規則に従う、既定 0)。 | data-stimeo--file-dropzone-max-files-value |
dragLabel
|
ゾーン上をドラッグ中に読み上げるヒント(既定 "Drop files to add them")。 | data-stimeo--file-dropzone-drag-label-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
onChange
|
ネイティブダイアログで選ばれたファイルを追加する。 | stimeo--file-dropzone#onChange |
onDragLeave
|
ポインタがゾーンを離れたら drag-over フラグを解除する。 | stimeo--file-dropzone#onDragLeave |
onDragOver
|
ゾーンをドロップ対象として示し、操作可能であることを言葉で読み上げる。 | stimeo--file-dropzone#onDragOver |
onDrop
|
ドロップされたファイルを受理し、drag-over 状態を解除する。 | stimeo--file-dropzone#onDrop |
openDialog
|
ネイティブのファイルダイアログを開く。 | stimeo--file-dropzone#openDialog |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
受理済みファイル集合が変化したときに発火。detail は { files }(現在の File[])。 |
stimeo--file-dropzone:change |
reject
|
ファイルが拒否されたときに発火。detail は { file, reason }(type/size/count)。 |
stimeo--file-dropzone:reject |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-dragover |
ゾーン | ドラッグオーバー中に付与。 |
data-stimeo--file-dropzone-invalid |
ゾーン | 検証拒否の後に付与。 |
hidden |
アイテムのサムネイル | 画像以外のファイルでは付与(プレビューなし)。 |
テキスト |
ステータス | ドラッグ状態 / 検証結果 / 追加・削除のライブリージョン通知。 |