/** * ファイルブラウザのツールバー部品群(スペース / タスク共有)。 * いずれも状態を持たない純プレゼンテーショナル。drag 状態だけ FileDropzone が内部に持つ。 * * - FileActions … 「追加」ボタン + 隠し input + 再読み込み * - FileSelectionBar … すべて選択 + 選択件数 + 削除(複数選択) * - FileDropzone … 子をラップし、ドラッグ&ドロップでアップロードを受ける */ import { useEffect, useRef, useState } from 'react'; export { filesToBase64 } from '../../lib/fileBase64'; const ICON_BTN = 'w-8 h-8 flex items-center justify-center rounded-md border border-hairline bg-canvas text-slate-500 hover:text-slate-900 hover:bg-surface transition-colors'; export function FileActions({ idPrefix, canUpload, onUploadFiles, onRefresh, isRefreshing, isUploading, }: { idPrefix: string; canUpload: boolean; onUploadFiles: (files: File[]) => void; onRefresh: () => void; isRefreshing?: boolean; isUploading?: boolean; }) { const inputRef = useRef(null); return (
{canUpload && ( <> { const files = Array.from(e.target.files ?? []); e.target.value = ''; onUploadFiles(files); }} /> )}
); } export function FileSelectionBar({ idPrefix, allSelected, onToggleSelectAll, selectedCount, onDeleteSelected, onDownloadSelected, isDeleting, isDownloading, }: { idPrefix: string; allSelected: boolean; onToggleSelectAll: () => void; selectedCount: number; onDeleteSelected: () => void; onDownloadSelected: () => void; isDeleting?: boolean; isDownloading?: boolean; }) { return (
{selectedCount} 件選択中 {selectedCount > 0 && (
)}
); } export function FileDropzone({ idPrefix, enabled, isUploading, onDropFiles, onRejectFolder, children, }: { idPrefix: string; /** false のときドロップを受け付けない(オーバーレイも出さない)。 */ enabled: boolean; isUploading?: boolean; onDropFiles: (files: File[]) => void; /** フォルダがドロップされたときのコールバック(未対応メッセージ表示用)。 */ onRejectFolder?: () => void; children: React.ReactNode; }) { const [dragDepth, setDragDepth] = useState(0); // ウィンドウ外への drag 離脱や drop 取りこぼしで dragDepth が戻らずオーバーレイが // 残るのを防ぐ。window レベルの dragend/drop で必ず 0 に戻す。 useEffect(() => { const reset = () => setDragDepth(0); window.addEventListener('dragend', reset); window.addEventListener('drop', reset); return () => { window.removeEventListener('dragend', reset); window.removeEventListener('drop', reset); }; }, []); // testid と要素は常に出し、enabled=false(read-only)のときは DnD ハンドラと // オーバーレイだけ無効化する(既存 e2e の dropzone testid を温存する)。 return (
{ e.preventDefault(); setDragDepth(d => d + 1); }) : undefined} onDragOver={enabled ? (e => { e.preventDefault(); }) : undefined} onDragLeave={enabled ? (e => { e.preventDefault(); if (!e.relatedTarget) { setDragDepth(0); } else { setDragDepth(d => Math.max(0, d - 1)); } }) : undefined} onDrop={enabled ? (e => { e.preventDefault(); setDragDepth(0); const hasDirectory = Array.from(e.dataTransfer.items ?? []).some( it => it.webkitGetAsEntry?.()?.isDirectory, ); const files = Array.from(e.dataTransfer.files); if (hasDirectory) { onRejectFolder?.(); return; } if (files.length === 0) return; onDropFiles(files); }) : undefined} > {enabled && dragDepth > 0 && (
ここにドロップして追加
)} {enabled && isUploading && (
アップロード中…
)} {children}
); }