Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Pages
on:
push:
branches:
- main
workflow_dispatch:

# Allow the deploy job to publish to GitHub Pages via OIDC.
permissions:
contents: read
pages: write
id-token: write

# Only one concurrent deployment; don't cancel an in-progress one.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
name: Build docs (Vocs)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24

- name: Install dependencies
run: npm ci

- name: Build docs
run: npm run docs:build

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
# Vocs' full-static output; served under /file-selector (see basePath in vocs.config.ts).
path: site/public

deploy:
name: Deploy to Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@ Icon
## Dist
coverage
build
docs
dist
out-tsc
tmp

## Playwright
test-results
playwright-report

## Vocs (docs build output, cache, generated route types)
.vocs
site
docs/pages.gen.ts
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

> A small package for converting a [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent) or [file input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file) to a list of File objects.

📖 **[Documentation & live playground →](https://react-dropzone.github.io/file-selector/)**

[![npm](https://img.shields.io/npm/v/file-selector.svg?style=flat-square)](https://www.npmjs.com/package/file-selector)
![Tests](https://img.shields.io/github/actions/workflow/status/react-dropzone/file-selector/test.yml?branch=main&style=flat-square&label=tests)
[![coverage](https://img.shields.io/coveralls/github/react-dropzone/file-selector/main?style=flat-square)](https://coveralls.io/github/react-dropzone/file-selector?branch=main)
Expand Down Expand Up @@ -141,6 +143,9 @@ The project is built with [tsdown](https://tsdown.dev) (Rolldown), type-checked
| `npm run format:check` | Check formatting without writing. |
| `npm run build` | Build the outputs into `dist/`. |
| `npm run dev` | Build in watch mode. |
| `npm run docs:dev` | Run the [Vocs](https://vocs.dev) docs site locally. |
| `npm run docs:build` | Build the docs site into `site/`. |
| `npm run docs:preview` | Preview the built docs site. |

The build emits into `dist/`:
- `dist/index.js` — ES module
Expand Down
175 changes: 175 additions & 0 deletions docs/components/Playground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
'use client';

import {useCallback, useRef, useState} from 'react';
import {fromEvent} from '../../src';

interface Row {
path: string;
type: string;
size: number;
}

function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
const units = ['KB', 'MB', 'GB'];
let value = bytes / 1024;
let i = 0;
while (value >= 1024 && i < units.length - 1) {
value /= 1024;
i++;
}
return `${value.toFixed(1)} ${units[i]}`;
}

export function Playground() {
const [rows, setRows] = useState<Row[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [active, setActive] = useState(false);
const filesInput = useRef<HTMLInputElement>(null);
const dirInput = useRef<HTMLInputElement>(null);

const parse = useCallback(async (evt: Event | React.SyntheticEvent) => {
try {
setError(null);
const result = await fromEvent(evt as Event);
const files = result.filter((f): f is File => f instanceof File);
setRows(
files.map(f => ({
path: (f as {path?: string}).path ?? f.name,
type: f.type || '—',
size: f.size,
})),
);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
setRows(null);
}
}, []);

const clear = useCallback(() => {
if (filesInput.current) filesInput.current.value = '';
if (dirInput.current) dirInput.current.value = '';
setRows(null);
setError(null);
}, []);

return (
<div className="fsp">
<style>{styles}</style>

<div
className={active ? 'fsp-drop fsp-drop--active' : 'fsp-drop'}
onDragEnter={e => {
e.preventDefault();
setActive(true);
}}
onDragOver={e => {
e.preventDefault();
setActive(true);
}}
onDragLeave={() => setActive(false)}
onDrop={e => {
e.preventDefault();
setActive(false);
parse(e);
}}
>
<p className="fsp-hint">Drag &amp; drop here</p>
<p className="fsp-sub">Files or an entire folder — nothing is uploaded.</p>
<div className="fsp-buttons">
<button type="button" onClick={() => filesInput.current?.click()}>
Select files
</button>
<button type="button" onClick={() => dirInput.current?.click()}>
Select folder
</button>
</div>
</div>

<input ref={filesInput} type="file" multiple hidden onChange={parse} />
{/* `webkitdirectory` is not in React's typings; set it on the DOM node directly. */}
<input
ref={el => {
if (el) el.setAttribute('webkitdirectory', '');
(dirInput as {current: HTMLInputElement | null}).current = el;
}}
type="file"
hidden
onChange={parse}
/>

<div className="fsp-results">
<div className="fsp-results-head">
<strong>Parsed files</strong>
{rows && rows.length > 0 && (
<>
<span className="fsp-count">
{rows.length} File object{rows.length === 1 ? '' : 's'}
</span>
<button type="button" className="fsp-clear" onClick={clear}>
Clear
</button>
</>
)}
</div>

{error ? (
<p className="fsp-error">{error}</p>
) : rows === null ? (
<p className="fsp-empty">No files yet. Drop something above or use the buttons.</p>
) : rows.length === 0 ? (
<p className="fsp-empty">No File objects were produced.</p>
) : (
<div className="fsp-table-wrap">
<table className="fsp-table">
<thead>
<tr>
<th>path</th>
<th>type</th>
<th>size</th>
</tr>
</thead>
<tbody>
{rows.map((r, i) => (
<tr key={`${r.path}-${i}`}>
<td className="fsp-path">{r.path}</td>
<td>
<span className="fsp-badge">{r.type}</span>
</td>
<td className="fsp-size">{formatSize(r.size)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}

const styles = `
.fsp { --fsp-border: rgba(128,128,128,0.3); --fsp-accent: #4d8dff; --fsp-accent-soft: rgba(77,141,255,0.1); --fsp-muted: rgba(128,128,128,0.9); margin: 1.5rem 0; }
.fsp-drop { border: 2px dashed var(--fsp-border); border-radius: 14px; padding: 2.5rem 1.5rem; text-align: center; transition: border-color .15s, background .15s; }
.fsp-drop--active { border-color: var(--fsp-accent); background: var(--fsp-accent-soft); }
.fsp-hint { font-size: 1.1rem; font-weight: 600; margin: 0 0 .25rem; }
.fsp-sub { color: var(--fsp-muted); font-size: .9rem; margin: 0 0 1.25rem; }
.fsp-buttons { display: flex; gap: .625rem; justify-content: center; flex-wrap: wrap; }
.fsp button { font: inherit; font-size: .9rem; font-weight: 500; color: inherit; background: transparent; border: 1px solid var(--fsp-border); border-radius: 8px; padding: .5rem 1rem; cursor: pointer; transition: border-color .15s, background .15s; }
.fsp button:hover { border-color: var(--fsp-accent); background: var(--fsp-accent-soft); }
.fsp-results { margin-top: 1.75rem; }
.fsp-results-head { display: flex; align-items: baseline; gap: .75rem; margin-bottom: .75rem; }
.fsp-count { font-size: .85rem; color: var(--fsp-muted); }
.fsp-clear { margin-left: auto; font-size: .8rem !important; padding: .2rem .625rem !important; }
.fsp-empty, .fsp-error { font-size: .9rem; padding: .5rem 0; }
.fsp-empty { color: var(--fsp-muted); }
.fsp-error { color: #ff6b6b; }
.fsp-table-wrap { overflow-x: auto; }
.fsp-table { width: 100%; border-collapse: collapse; font-size: .85rem; }
.fsp-table th { text-align: left; color: var(--fsp-muted); font-weight: 600; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; padding: .375rem .625rem; border-bottom: 1px solid var(--fsp-border); }
.fsp-table td { padding: .5rem .625rem; border-bottom: 1px solid var(--fsp-border); vertical-align: top; }
.fsp-table tbody tr:last-child td { border-bottom: none; }
.fsp-path { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
.fsp-size { color: var(--fsp-muted); white-space: nowrap; }
.fsp-badge { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .75rem; background: rgba(128,128,128,0.15); border-radius: 5px; padding: .05rem .375rem; }
`;
61 changes: 61 additions & 0 deletions docs/pages/api.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: API Reference
description: The fromEvent function and the FileWithPath type.
showAskAi: false
---

# API Reference

The public surface is intentionally tiny: one function and one type.

## `fromEvent`

```ts
function fromEvent(evt: Event | any): Promise<(FileWithPath | DataTransferItem)[]>
```

Converts an event (or a list of file-system handles) into a flat list of files.

**Accepts one of:**

| Input | Source |
| --- | --- |
| A [`DragEvent`](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent) | A `drop` (or other drag) event with a `dataTransfer` |
| A [change `Event`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event) | An `<input type="file">` (optionally `webkitdirectory`) |
| An array of [`FileSystemFileHandle`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle) | `window.showOpenFilePicker()` |

**Returns** `Promise<(FileWithPath | DataTransferItem)[]>`:

- For a `drop` event and for file inputs, entries are [`FileWithPath`](#filewithpath) objects.
Dropped directories are traversed recursively and flattened into the same list.
- For non-`drop` drag events (e.g. `dragenter`/`dragover`), only `DataTransferItem`s are
returned, since file data is not yet accessible per the HTML drag-and-drop spec.
- Anything unrecognized resolves to an empty array (`[]`) rather than throwing.

## `FileWithPath`

Every parsed file is a native [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File)
augmented with its location:

```ts
interface FileWithPath extends File {
/** Full path of the file, e.g. `./photos/cat.png`. On Electron this is the absolute path. */
readonly path: string;
/** Path relative to the dropped/selected root, always populated. */
readonly relativePath: string;
}
```

Because it extends `File`, the standard members (`name`, `size`, `type`, `lastModified`,
`arrayBuffer()`, `text()`, `stream()`, …) are all available.

```ts
import {fromEvent} from 'file-selector';

const files = await fromEvent(evt);
for (const file of files) {
if (file instanceof File) {
console.log(file.path, file.name, file.type, file.size);
}
}
```
31 changes: 31 additions & 0 deletions docs/pages/browser-support.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: Browser Support
description: Which browser APIs file-selector relies on, and how well they are supported.
showAskAi: false
---

# Browser Support

Most browsers support basic file selection with drag & drop or a file input:

- [File API](https://developer.mozilla.org/en-US/docs/Web/API/File#Browser_compatibility)
- [Drag Event](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent#Browser_compatibility)
- [DataTransfer](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer#Browser_compatibility)
- [`<input type="file">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Browser_compatibility)

## Folder drop

For folder drop we use the [File System API](https://developer.mozilla.org/en-US/docs/Web/API/FileSystem),
which has more limited support:

- [DataTransferItem.getAsFile()](https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/getAsFile#Browser_compatibility)
- [DataTransferItem.webkitGetAsEntry()](https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry#Browser_compatibility)
- [FileSystemEntry](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemEntry#Browser_compatibility)
- [FileSystemFileEntry.file()](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileEntry/file#Browser_compatibility)
- [FileSystemDirectoryEntry.createReader()](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryEntry/createReader#Browser_compatibility)
- [FileSystemDirectoryReader.readEntries()](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader/readEntries#Browser_compatibility)

Where it's available, directory traversal prefers the newer
[File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API),
which avoids a class of failures in the legacy entries API (for example, dragging a folder from
Windows Explorer into a Chromium browser).
Loading