diff --git a/README.md b/README.md
index e12d6bc..18b881c 100644
--- a/README.md
+++ b/README.md
@@ -14,9 +14,12 @@ spawns the BEAM release; in development an existing BEAM can launch the host wit
| Windows | WebView2 | Done — [docs/status/windows.md](docs/status/windows.md) |
| Linux | WebKitGTK | Usable — [docs/status/linux.md](docs/status/linux.md) |
-HTML `` support is required on all three hosts. The shared E2E
-checks the fixture's DOM contract; native picker selection remains a manual check.
-It is separate from the `dialog.choose_file` RPC. See [docs/protocol.md](docs/protocol.md).
+HTML `` support and file-manager drag-and-drop are required on
+all three hosts. Finder, Explorer, and Linux file-manager drops must reach the
+page as normal web drag events with a usable `DataTransfer.files` list. The
+shared E2E checks the fixture's DOM contract; native picker and file-drop checks
+remain manual. These features are separate from the `dialog.choose_file` RPC.
+See [docs/protocol.md](docs/protocol.md).
## Quick start (development)
diff --git a/docs/porting.md b/docs/porting.md
index 250163b..6565817 100644
--- a/docs/porting.md
+++ b/docs/porting.md
@@ -44,21 +44,23 @@ Do **not** copy macOS UI code into other platforms — share only the protocol.
10. **Packaged BEAM spawn** + **CI artifact** on tag draft releases
11. **Test RPC** behind `--edw-test-rpc`; run shared E2E
-## HTML file chooser
+## HTML file inputs and file-manager drag-and-drop
-`` is required on every platform. It is separate from
-`dialog.choose_file`, which is an explicit Elixir RPC. Follow the semantics in
-[protocol.md](protocol.md) and use the platform hook below.
+`` and file-manager drag-and-drop are required on every
+platform. They are separate from `dialog.choose_file`, which is an explicit
+Elixir RPC. Follow the semantics in [protocol.md](protocol.md) and use the
+platform hooks below.
-| Platform | Hook | Required integration |
+| Platform | Hooks | Required integration |
|----------|------|----------------------|
-| macOS | `WKUIDelegate.webView(_:runOpenPanelWith:initiatedByFrame:completionHandler:)` | Map `WKOpenPanelParameters` to the native panel. Pass selected URLs to the completion handler, or `nil` on cancel. |
-| Windows | WebView2's built-in file picker | Keep the WebView2 UI thread and message loop active. WebView2 has no native file-chooser event for this input; do not replace it with `dialog.choose_file` or CDP file injection. |
-| Linux | WebKitGTK `run-file-chooser` default handler | Keep WebKitGTK's asynchronous default handler enabled, or provide an equivalent handler that completes the request with selected paths or cancellation. |
+| macOS | `WKUIDelegate.webView(_:runOpenPanelWith:initiatedByFrame:completionHandler:)` and `NSDraggingDestination` | Map `WKOpenPanelParameters` to the native panel. Register file URLs and file promises on the webview. Forward accepted drag methods to WebKit so the page receives its `FileList`. |
+| Windows | WebView2's built-in file picker and drag handling | Keep the WebView2 UI thread and message loop active. Do not replace the picker or file drops with `dialog.choose_file`, CDP injection, or a host-only drop handler. |
+| Linux | WebKitGTK `run-file-chooser` default handler and drag handling | Keep the asynchronous chooser and normal WebKitGTK drag handling enabled. Provide equivalent handlers only when they preserve the page `drop` event and `dataTransfer.files`. |
The shared E2E checks the fixture's DOM contract only. It cannot drive a native
picker or inject a `FileList`; selection, cancellation, multiple files, and
-directory selection need manual checks until a supported platform test hook exists.
+directory selection, and file-manager drops need manual checks until a supported
+platform test hook exists.
## Toolchain expectations
diff --git a/docs/protocol.md b/docs/protocol.md
index bd6589d..eae584f 100644
--- a/docs/protocol.md
+++ b/docs/protocol.md
@@ -154,6 +154,24 @@ The shared E2E may inspect the fixture's DOM, but it cannot select files or
populate a `FileList` through JavaScript. Native picker selection and cancellation
remain manual checks until a supported platform test hook exists.
+### File-manager drag-and-drop
+
+File-manager drag-and-drop is also a required webview feature on macOS, Windows,
+and Linux. A drop from Finder, Explorer, or a Linux file manager onto a page
+must produce the normal web `drop` event. Its `dataTransfer.files` must contain
+the dropped local files.
+
+Hosts MUST:
+
+- accept local file URLs and file promises from the platform file manager;
+- preserve the browser's normal drop handling and `FileList` semantics;
+- support multiple dropped files when the page accepts them; and
+- avoid converting a page drop into `dialog.choose_file` or another RPC.
+
+The webview host may inspect the drag to decide whether to accept it, but it must
+forward accepted drops to the engine. Applications still decide which files to
+accept through their page and upload logic.
+
### Menus and tray
- The macOS host installs a default `Edit` submenu on the main menu (Undo, Redo,
diff --git a/docs/status/linux.md b/docs/status/linux.md
index 887323f..a94cf33 100644
--- a/docs/status/linux.md
+++ b/docs/status/linux.md
@@ -32,7 +32,7 @@ Host: GTK 4 + WebKitGTK 6 (`native/linux/`). Binary delivery via GitHub Releases
| Permission policy hybrid | done | |
| Microphone in webview | done | E2E via test RPC + fixture |
| Camera in webview | done | E2E via test RPC + fixture |
-| HTML `` | partial | WebKitGTK's asynchronous `run-file-chooser` default handler; native selection and cancellation checks pending |
+| HTML `` and file-manager drag-and-drop | partial | WebKitGTK default chooser and drag handling; native picker and file-manager checks pending |
| Test RPC channel | done | `--edw-test-rpc` |
| Release artifact download | todo | |
| CI build | done | ubuntu-latest + xvfb |
diff --git a/docs/status/macos.md b/docs/status/macos.md
index 864a5d1..a1c08a1 100644
--- a/docs/status/macos.md
+++ b/docs/status/macos.md
@@ -33,7 +33,7 @@ manual-only with justification).
| Microphone in webview | done | E2E via test RPC + fixture |
| Camera in webview | done | E2E via test RPC + fixture |
| Native dialogs (`dialog.choose_file/dir`) | done | `NSOpenPanel` (manual; blocks RPC) |
-| HTML `` | partial | `WKUIDelegate` open-panel hook; native selection and cancellation checks pending |
+| HTML `` and Finder drag-and-drop | partial | `WKUIDelegate` open-panel hook and `FileDropWebView` forwarding; native picker and Finder checks pending |
| Dialog prompt | done | `NSAlert` + text field (manual) |
| EventBridge Env/Window/Menu | done | Elixir unit coverage |
| Test RPC channel | done | `--edw-test-rpc` |
diff --git a/docs/status/windows.md b/docs/status/windows.md
index c8783bc..24121dc 100644
--- a/docs/status/windows.md
+++ b/docs/status/windows.md
@@ -33,7 +33,7 @@ Release asset: `DesktopWebView-windows-x64.exe` (GitHub Releases; not Hex `priv/
| Microphone in webview | done | Permission RPC + WebView2 kinds |
| Camera in webview | done | Permission RPC + WebView2 kinds |
| Native dialogs (`dialog.choose_file/dir`) | done | IFileOpenDialog + Win32 prompt |
-| HTML `` | partial | WebView2's built-in picker; native selection and cancellation checks pending |
+| HTML `` and Explorer drag-and-drop | partial | WebView2 built-in picker and drag handling; native picker and Explorer checks pending |
| Host-driven BEAM restart | done | `restart_beam` ini + process wait |
| Test RPC channel | done | E2E |
| Release artifact download | todo | Elixir fetch/cache still pending |
diff --git a/native/linux/README.md b/native/linux/README.md
index 174a1b9..54a50b2 100644
--- a/native/linux/README.md
+++ b/native/linux/README.md
@@ -46,17 +46,21 @@ Release artifact name: `DesktopWebView-linux-x86_64` (see [packaging.md](../../d
Headless CI needs a display (`xvfb-run` or a pre-set `DISPLAY`).
-## HTML file inputs
+## HTML file inputs and file-manager drops
WebKitGTK's asynchronous `run-file-chooser` default handler serves
``. It handles single files, multiple files, directories,
and cancellation without using the `dialog.choose_file` RPC.
+File-manager drops are also required. Keep WebKitGTK's normal drag handling
+enabled so file URLs reach the page as a `drop` event with
+`dataTransfer.files`. Do not replace the page drop with `dialog.choose_file`.
+
The shared E2E checks the fixture DOM, but it cannot drive a native picker.
Manually verify selection and cancellation with
`test/fixtures/file_input.html`. The chooser uses the GTK desktop portal when
-the desktop session provides one, so the host needs an interactive display for
-manual checks.
+the desktop session provides one. File-manager drop checks also need an
+interactive display.
## Structure
diff --git a/native/macos/README.md b/native/macos/README.md
index 4cea905..3c37c70 100644
--- a/native/macos/README.md
+++ b/native/macos/README.md
@@ -21,15 +21,17 @@ Output is copied to `priv/native/macos/DesktopWebView` (universal when both arch
# prints: listening
```
-## HTML file inputs
+## HTML file inputs and Finder drops
`` uses the `WKUIDelegate` open-panel callback in
`WebWindow.swift`. The callback maps single, multiple, directory, and cancel
-actions to WebKit's `FileList`.
+actions to WebKit's `FileList`. `FileDropWebView.swift` registers file URLs and
+file promises, accepts Finder drops, and forwards the drop methods to WebKit.
-The shared E2E checks the fixture DOM, but it cannot drive the macOS picker.
-Manually verify selection and cancellation with
-`test/fixtures/file_input.html`, including after `webview.rebuild`.
+The shared E2E checks the fixture DOM, but it cannot drive the macOS picker or
+Finder. Manually verify selection, cancellation, multiple files, directory
+selection, and Finder drops with `test/fixtures/file_input.html`, including
+after `webview.rebuild`.
## Structure
diff --git a/native/macos/Sources/DesktopWebView/FileDropWebView.swift b/native/macos/Sources/DesktopWebView/FileDropWebView.swift
new file mode 100644
index 0000000..20b433b
--- /dev/null
+++ b/native/macos/Sources/DesktopWebView/FileDropWebView.swift
@@ -0,0 +1,23 @@
+import AppKit
+import WebKit
+
+final class FileDropWebView: WKWebView {
+ private static let fileDragTypes =
+ [
+ NSPasteboard.PasteboardType.fileURL,
+ NSPasteboard.PasteboardType("NSFilenamesPboardType")
+ ] +
+ NSFilePromiseReceiver.readableDraggedTypes.map {
+ NSPasteboard.PasteboardType($0)
+ }
+
+ override init(frame: NSRect, configuration: WKWebViewConfiguration) {
+ super.init(frame: frame, configuration: configuration)
+ registerForDraggedTypes(Self.fileDragTypes)
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ registerForDraggedTypes(Self.fileDragTypes)
+ }
+}
diff --git a/native/macos/Sources/DesktopWebView/WebWindow.swift b/native/macos/Sources/DesktopWebView/WebWindow.swift
index f2b433b..a020730 100644
--- a/native/macos/Sources/DesktopWebView/WebWindow.swift
+++ b/native/macos/Sources/DesktopWebView/WebWindow.swift
@@ -38,7 +38,7 @@ final class WebWindowController: NSObject, NSWindowDelegate, WKUIDelegate, WKNav
if #available(macOS 14.0, *) {
// media capture handled via UI delegate
}
- let wv = WKWebView(frame: rect, configuration: config)
+ let wv = FileDropWebView(frame: rect, configuration: config)
wv.allowsBackForwardNavigationGestures = true
self.webView = wv
super.init()
@@ -53,7 +53,7 @@ final class WebWindowController: NSObject, NSWindowDelegate, WKUIDelegate, WKNav
let config = WKWebViewConfiguration()
config.mediaTypesRequiringUserActionForPlayback = []
let frame = webView.frame
- let wv = WKWebView(frame: frame, configuration: config)
+ let wv = FileDropWebView(frame: frame, configuration: config)
wv.uiDelegate = self
wv.navigationDelegate = self
window.contentView = wv
diff --git a/native/windows/README.md b/native/windows/README.md
index 940bfa0..280fcee 100644
--- a/native/windows/README.md
+++ b/native/windows/README.md
@@ -33,7 +33,7 @@ $env:DESKTOP_WEBVIEW_BINARY = "$PWD\priv\native\windows\DesktopWebView.exe"
mix test.e2e
```
-## HTML file inputs
+## HTML file inputs and Explorer drops
Normal HTML file inputs use WebView2's built-in Windows file picker:
@@ -42,11 +42,16 @@ Normal HTML file inputs use WebView2's built-in Windows file picker:
- If the user cancels, the input receives no new files and the page receives no new selection.
- No host C++ change, JSON-RPC call, CDP interception, or custom picker is needed. The page reads selected files through the normal HTML `input.files` API.
+File-manager drops are also required. Preserve WebView2's normal drag handling
+so Explorer file drops reach the page as a `drop` event with
+`dataTransfer.files`. Do not replace page drops with `dialog.choose_file` or a
+host-only drop handler.
+
The picker needs an interactive Windows desktop and a working Microsoft Edge
WebView2 Evergreen Runtime. It cannot show from a service, a headless run, or a
non-interactive session. Runtime or Windows security restrictions can also
prevent native UI from appearing, so test with a current runtime in a normal
-desktop session.
+desktop session. Test Explorer drops in the same session.
This browser feature is separate from the [`dialog.choose_file` JSON-RPC
method](../../docs/protocol.md#dialog). That method is an explicit host dialog