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
2 changes: 1 addition & 1 deletion packages/file_picker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The iOS and macOS native implementations live under the shared Darwin source tre
2. **Removed Parameters Deprecated Since v12**:
* `allowMultiple`, `withData`, `withReadStream`, and `readSequential` on `pickFiles()`: use `pickFile()` for single-file selection, and `PlatformFile.readAsBytes()`/`readAsByteStream()` to read file data.
* `lockParentWindow` on `pickFiles()`/`pickFile()`/`getDirectoryPath()`/`saveFile()`: use `WindowsOptions.lockParentWindow` or `LinuxOptions.lockParentWindow`.
* `cancelUploadOnWindowBlur` on `pickFiles()`/`pickFile()`: use `WebOptions.cancelUploadOnWindowBlur`.
* `cancelUploadOnWindowBlur` on `pickFiles()`/`pickFile()`: pass `webOptions: FilePickerWebOptions(cancelUploadOnWindowBlur: false)`. `FilePickerWebOptions` lives in `package:file_picker_web`, so add it as a direct dependency to import it.
* `androidSafOptions` on `pickFiles()`/`pickFile()`/`getDirectoryPath()`: use `androidOptions`.

## Migrating to v12
Expand Down
5 changes: 5 additions & 0 deletions packages/file_picker_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 4.0.1

- Fixed picking files on Safari (macOS and iOS) never returning the selection. [#2222](https://github.com/vicajilau/flutter_file_picker/issues/2222)
- Fixed a selection being reported as cancelled when the window regained focus before the `change` event.

## 4.0.0

- **BREAKING CHANGE**: `PlatformFile.length()` now returns `Future<int?>` instead of `Future<int>`. Returns `null` when the length could not be determined (e.g. a failed disk read), instead of `0`, matching `lengthSync()`. [#2197](https://github.com/vicajilau/flutter_file_picker/issues/2197)
Expand Down
2 changes: 1 addition & 1 deletion packages/file_picker_web/lib/src/file_picker_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class FilePickerWeb extends FilePickerPlatform {
'flt-file-picker-inputs',
)..id = id;

document.querySelector('body')!.children.add(targetElement);
document.body!.appendChild(targetElement);
target = targetElement;
}
return target;
Expand Down
49 changes: 35 additions & 14 deletions packages/file_picker_web/lib/src/web_file_input_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ class WebFileInputSession {
final Completer<List<PlatformFile>?> _completer = Completer();
bool _eventTriggered = false;

// Kept attached to [target] (and referenced here) for the whole session.
// WebKit does not deliver the `change` event to a file input that is not in
// the document, which left the returned future hanging on Safari.
HTMLInputElement? _input;

// Cached once and reused for both addEventListener and removeEventListener.
// Function.toJS creates a new JS function object on every call, so passing
// freshly-created ones to removeEventListener would never actually match
Expand All @@ -76,10 +81,10 @@ class WebFileInputSession {
window.addEventListener('focus', _onCancelListener);
}

_input = uploadInput;
_clearTargetChildren();
target.children.add(uploadInput);
target.appendChild(uploadInput);
uploadInput.click();
_clearTargetChildren();

return _completer.future;
}
Expand All @@ -90,8 +95,29 @@ class WebFileInputSession {

final targetInput = e.target as HTMLInputElement?;
_cleanupListeners(targetInput);
await _complete(targetInput?.files);
}

void _onCancel(Event _) {
_cleanupListeners(null);

Future.delayed(const Duration(milliseconds: 500)).then((_) async {
if (_eventTriggered) return;
_eventTriggered = true;

// Safari can fire the window `focus` event well before `change` once
// the dialog closes. If the input already holds a selection, use it
// instead of reporting a cancellation.
final input = _input;
final files = input?.files;
_cleanupListeners(input);
await _complete(files != null && files.length > 0 ? files : null);
});
}

Future<void> _complete(FileList? files) async {
_removeInput();

final files = targetInput?.files;
if (files == null) {
if (!_completer.isCompleted) {
_completer.complete(null);
Expand All @@ -107,17 +133,12 @@ class WebFileInputSession {
}
}

void _onCancel(Event _) {
_cleanupListeners(null);

Future.delayed(const Duration(milliseconds: 500)).then((_) {
if (!_eventTriggered) {
_eventTriggered = true;
if (!_completer.isCompleted) {
_completer.complete(null);
}
}
});
void _removeInput() {
final input = _input;
_input = null;
if (input != null && input.parentNode == target) {
target.removeChild(input);
}
}

void _cleanupListeners(HTMLInputElement? input) {
Expand Down
2 changes: 1 addition & 1 deletion packages/file_picker_web/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: file_picker_web
description: Web platform implementation of the file_picker plugin, providing browser file selection, file streaming, and file loading.
version: 4.0.0
version: 4.0.1
homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_web
repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_web
topics:
Expand Down
108 changes: 108 additions & 0 deletions packages/file_picker_web/test/web_file_input_session_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
@TestOn('browser')
library;

import 'dart:js_interop';

import 'package:file_picker_web/file_picker_web.dart';
import 'package:file_picker_web/src/web_file_input_session.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:web/web.dart';

void main() {
late HTMLDivElement target;

setUp(() {
target = HTMLDivElement();
document.body!.append(target);
});

tearDown(() => target.remove());

WebFileInputSession createSession({
FilePickerWebOptions webOptions = const FilePickerWebOptions(),
}) {
return WebFileInputSession(
target: target,
accept: '',
allowMultiple: false,
webOptions: webOptions,
onFileLoading: null,
processFiles: (files, _) async => [
for (var i = 0; i < files.length; i++)
WebPlatformFile(
name: files.item(i)!.name,
uri: Uri.parse('blob:test'),
),
],
);
}

HTMLInputElement attachedInput() => target.firstChild! as HTMLInputElement;

void selectFile(HTMLInputElement input, String name) {
final transfer = DataTransfer();
transfer.items.add(File(<JSAny>[].toJS, name));
input.files = transfer.files;
}

test('keeps the input attached while the dialog is open', () async {
final result = createSession().start();

expect(target.childNodes.length, 1);
expect(target.firstChild, isA<HTMLInputElement>());

attachedInput().dispatchEvent(Event('cancel'));
expect(await result, isNull);
});

test(
'completes with the selected file on change and detaches the input',
() async {
final result = createSession().start();
final input = attachedInput();

selectFile(input, 'a.pdf');
input.dispatchEvent(Event('change'));

final files = await result;
expect(files?.single.name, 'a.pdf');
expect(target.childNodes.length, 0);
},
);

test(
'completes with null when the window regains focus without a selection',
() async {
final result = createSession().start();

window.dispatchEvent(Event('focus'));

expect(await result, isNull);
expect(target.childNodes.length, 0);
},
);

test(
'uses the selection when focus returns before the change event',
() async {
final result = createSession().start();

selectFile(attachedInput(), 'b.png');
window.dispatchEvent(Event('focus'));

final files = await result;
expect(files?.single.name, 'b.png');
expect(target.childNodes.length, 0);
},
);

test('completes with null on the input cancel event', () async {
final result = createSession(
webOptions: const FilePickerWebOptions(cancelUploadOnWindowBlur: false),
).start();

attachedInput().dispatchEvent(Event('cancel'));

expect(await result, isNull);
});
}
Loading