Skip to content

[Dropzone] Expose the state with data-state and toggle the parts with hidden instead of inline styles - #4145

Merged
Kocal merged 1 commit into
symfony:3.xfrom
ker0x:feat/dropzone-data-state
Nov 2, 2026
Merged

Kocal merged 1 commit into
symfony:3.xfrom
ker0x:feat/dropzone-data-state

Conversation

@ker0x

@ker0x ker0x commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor
Q A
Bug fix? no
New feature? yes
Deprecations? no
Documentation? yes
Issues #4142
License MIT

Third step of the RFC in #4142.

The controller shows and hides the input, the placeholder and the preview with element.style.display, and the form theme renders style="display: none". Inline styles win over the utility classes an app sets (flex, grid, hidden), the style attributes need style-src 'unsafe-inline' under a CSP, and an app has no way to style the drop zone while files are dragged over it.

The controller now uses the hidden attribute on these parts, and the container gets a data-state attribute: empty, filled, dragging or processing (the last one wins). The theme renders hidden instead of style="display: none", and the bundle stylesheet adds .dropzone-container [hidden] { display: none !important; }, so the default look does not change. !important keeps a part hidden when an app rule with the same specificity, loaded later, sets display on it, as the inline style did. The dragging state is also set in multiple mode.

In single mode, the visible parts are now computed from two flags (a file is shown, files are dragged over) instead of being set by each event handler. This replaces the deferred check in onDrop() for Chrome, which fires no change when the very same file is dropped again: the drop now only ends the drag, one tick later, so the input stays shown until the browser has put the dropped files into it. A controller that reconnects also ends a drag it missed the end of. It also fixes a drag that leaves an empty drop zone, which used to hide the placeholder and show an empty preview.

Case Before After
Visibility of the parts inline style.display hidden attribute
State of the container none data-state
Drag leaving an empty drop zone placeholder hidden, empty preview shown placeholder shown
Theme copied with style="display: none" works works: the controller removes that inline style on connect
Default stylesheet disabled, own display rule on these parts parts hidden parts stay visible until the app adds the [hidden] rule (see UPGRADE.md)

The last row is the behavior change: a rule of the app's own CSS that sets display on these classes wins over the browser's [hidden] style. The new src/Dropzone/UPGRADE.md explains what to add, and what to change in a theme that overrides dropzone_widget.

Tests: unit tests for each state and the visibility in both modes, the image preview, a drag leaving an empty or a filled drop zone, the same file dropped again, the input staying shown while the drop is dispatched, a reconnect during a drag, and a theme with inline styles. Playwright tests for the states and the visibility through the e2e app, and one that loads only the bundle stylesheet, next to an app rule that sets display on the parts: the e2e app loads Bootstrap, whose [hidden] { display: none !important } would hide the parts even without the bundle rule. All of them pass on Chrome lowest, Chrome latest and Firefox latest, and the new ones fail on #4143.

@carsonbot carsonbot added the Status: Needs Review Needs to be reviewed label Oct 26, 2026
@ker0x ker0x changed the title feat/dropzone data state [Dropzone] Expose the state with data-state and toggle the parts with hidden instead of inline styles Oct 26, 2026
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

📊 Packages dist files size difference

Thanks for the PR! Here is the difference in size of the packages dist files between the base branch and the PR.
Please review the changes and make sure they are expected.

FileBefore (Size / Gzip)After (Size / Gzip)
Dropzone
controller.d.ts 1.29 kB / 522 B 1.34 kB+5% 📈 / 544 B+4% 📈
controller.js 7.59 kB / 2.06 kB 7.61 kB0% / 2.18 kB+6% 📈
style.min.css 1.52 kB / 590 B 1.57 kB+3% 📈 / 613 B+4% 📈

@ker0x
ker0x force-pushed the feat/dropzone-data-state branch 4 times, most recently from 19d0150 to a21e141 Compare October 26, 2026 12:19
ker0x added a commit to ker0x/symfony-ux that referenced this pull request Oct 26, 2026
| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | yes
| Deprecations?  | no
| Documentation? | yes
| Issues         | Fix symfony#4142
| License        | MIT

Sixth and last step of the RFC in symfony#4142. Based on symfony#4143, symfony#4145, symfony#4144, symfony#4146, symfony#4148 and symfony#4149, replayed in that order: only the last commit belongs to this PR.

The two modes of the controller had grown into two code paths. Single-file mode showed one file with the preview of the form theme, cleared the input when the controller connected, and dispatched `dropzone:clear`; `multiple` mode kept a list, re-added the files already in the input on connect, and never dispatched `dropzone:clear`. Each feature of the RFC had to be written twice, and the modes differed in ways nobody chose: a prevented or rejected selection emptied the drop zone in single-file mode but kept the files in `multiple` mode, and the preview template only worked in `multiple` mode.

Single-file mode is now the stored list of files capped at one, and both modes go through the same steps for each selection: the input is put back to the stored files right away (also while `dropzone:pre-add` listeners are processing), the dropped files are checked against `accept`, the `dropzone:pre-add` listeners run, then the files are added (single-file mode: the new file replaces the previous one, and the files over the first one are rejected with `max_files`), the input is updated, the preview is rendered and `dropzone:change` is dispatched.

| Case                                              | Before                                         | After
| ------------------------------------------------- | ---------------------------------------------- | ---
| Single-file mode, controller connects with a file | input emptied, `dropzone:clear`                | file kept and shown
| `multiple` mode, last file removed                | no `dropzone:clear`                            | `dropzone:clear`
| Form reset                                        | drop zone keeps showing the files              | cleared, `dropzone:clear`
| Single-file mode, selection prevented by a `dropzone:pre-add` listener | drop zone emptied     | previous file kept, as in `multiple` mode
| Single-file mode, preview template                | not used                                       | rendered in the preview, its remove slot clears
| `multiple` mode, selection of files already in the list | `dropzone:change`          | no event
| Drag of text or a link over the drop zone         | `dragging`                                     | ignored
| Drag over a child of the drop zone in Safari      | `dragging` ends (`relatedTarget` is `null`)    | `dragging` kept
| Image previews                                    | read again on every render, an outdated read can show the image of a replaced file | read once per file, never shown for another file

`dropzone:change` keeps its payload (a `File` in single-file mode, a `FileList` in `multiple` mode), to stay compatible: changing it is left for 4.0. The form theme now renders the `dropzone_preview_item_template` block and the `remove_label` value in both modes. The drag state keeps the elements a drag has entered, from `dragenter` and `dragleave`, instead of reading `relatedTarget`, which WebKit does not set, and forgets the elements removed during the drag. `onMultipleChange()` is merged into `onInputChange()`. `src/Dropzone/UPGRADE.md` explains the changes for code listening to `dropzone:clear` on connect or to `dropzone:change`, and for controllers that extend this one.

Tests: unit tests for a file kept on connect without `dropzone:clear`, a prevented selection and a pending one in single-file mode, a selection of several files in single-file mode, an outdated image read, a form reset, a prevented form reset, a disconnect during a form reset, the clear button after a failed connect, the preview template in single-file mode, `dropzone:clear` after the last removal in `multiple` mode, a selection that adds no file in `multiple` mode, one read per image, an image read again after a failed read, a drag without files, a drag over a child without `relatedTarget`, and a drag over an element removed during the drag; PHPUnit tests for the template block and the `remove_label` value in single-file mode; Playwright tests for a form reset and the last removal in `multiple` mode. The new tests fail on the base of this PR. Two tests of the base changed: the single-file mode `file chosen` test now picks the file with a writable `files` property like the other tests, as `@testing-library/user-event` makes it read-only while the controller now writes the input on every selection; and the test of a prevented selection after a pending one now expects the previous file to be kept.
@Kocal Kocal self-assigned this Nov 2, 2026
…th `hidden` instead of inline styles

| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | yes
| Deprecations?  | no
| Documentation? | yes
| Issues         | symfony#4142
| License        | MIT

Third step of the RFC in symfony#4142. Based on symfony#4143, which adds the `processing` state: only the last commit belongs to this PR.

The controller shows and hides the input, the placeholder and the preview with `element.style.display`, and the form theme renders `style="display: none"`. Inline styles win over the utility classes an app sets (`flex`, `grid`, `hidden`), the `style` attributes need `style-src 'unsafe-inline'` under a CSP, and an app has no way to style the drop zone while files are dragged over it.

The controller now uses the `hidden` attribute on these parts, and the container gets a `data-state` attribute: `empty`, `filled`, `dragging` or `processing` (the last one wins). The theme renders `hidden` instead of `style="display: none"`, and the bundle stylesheet adds `.dropzone-container [hidden] { display: none !important; }`, so the default look does not change. `!important` keeps a part hidden when an app rule with the same specificity, loaded later, sets `display` on it, as the inline style did. The `dragging` state is also set in `multiple` mode.

In single mode, the visible parts are now computed from two flags (a file is shown, files are dragged over) instead of being set by each event handler. This replaces the deferred check in `onDrop()` for Chrome, which fires no `change` when the very same file is dropped again: the drop now only ends the drag, one tick later, so the input stays shown until the browser has put the dropped files into it. A controller that reconnects also ends a drag it missed the end of. It also fixes a drag that leaves an empty drop zone, which used to hide the placeholder and show an empty preview.

| Case                                        | Before                                   | After
| ------------------------------------------- | ---------------------------------------- | ---
| Visibility of the parts                     | inline `style.display`                   | `hidden` attribute
| State of the container                      | none                                     | `data-state`
| Drag leaving an empty drop zone             | placeholder hidden, empty preview shown  | placeholder shown
| Theme copied with `style="display: none"`   | works                                    | works: the controller removes that inline style on connect
| Default stylesheet disabled, own `display` rule on these parts | parts hidden | parts stay visible until the app adds the `[hidden]` rule (see `UPGRADE.md`)

The last row is the behavior change: a rule of the app's own CSS that sets `display` on these classes wins over the browser's `[hidden]` style. The new `src/Dropzone/UPGRADE.md` explains what to add, and what to change in a theme that overrides `dropzone_widget`.

Tests: unit tests for each state and the visibility in both modes, the image preview, a drag leaving an empty or a filled drop zone, the same file dropped again, the input staying shown while the drop is dispatched, a reconnect during a drag, and a theme with inline styles. Playwright tests for the states and the visibility through the e2e app, and one that loads only the bundle stylesheet, next to an app rule that sets `display` on the parts: the e2e app loads Bootstrap, whose `[hidden] { display: none !important }` would hide the parts even without the bundle rule. All of them pass on Chrome lowest, Chrome latest and Firefox latest, and the new ones fail on symfony#4143.
@Kocal Kocal added Dropzone Feature New Feature Documentation Improvements or additions to documentation labels Nov 2, 2026
@Kocal
Kocal force-pushed the feat/dropzone-data-state branch from a21e141 to 40c37f0 Compare November 2, 2026 18:23
@carsonbot carsonbot added Status: Reviewed Has been reviewed by a maintainer and removed Status: Needs Review Needs to be reviewed labels Nov 2, 2026
@Kocal

Kocal commented Nov 2, 2026

Copy link
Copy Markdown
Member

Thank you @ker0x.

@Kocal
Kocal merged commit 95331c6 into symfony:3.x Nov 2, 2026
38 checks passed
@ker0x
ker0x deleted the feat/dropzone-data-state branch November 2, 2026 21:35
Kocal pushed a commit to ker0x/symfony-ux that referenced this pull request Nov 2, 2026
| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | yes
| Deprecations?  | no
| Documentation? | yes
| Issues         | Fix symfony#4142
| License        | MIT

Sixth and last step of the RFC in symfony#4142. Based on symfony#4143, symfony#4145, symfony#4144, symfony#4146, symfony#4148 and symfony#4149, replayed in that order: only the last commit belongs to this PR.

The two modes of the controller had grown into two code paths. Single-file mode showed one file with the preview of the form theme, cleared the input when the controller connected, and dispatched `dropzone:clear`; `multiple` mode kept a list, re-added the files already in the input on connect, and never dispatched `dropzone:clear`. Each feature of the RFC had to be written twice, and the modes differed in ways nobody chose: a prevented or rejected selection emptied the drop zone in single-file mode but kept the files in `multiple` mode, and the preview template only worked in `multiple` mode.

Single-file mode is now the stored list of files capped at one, and both modes go through the same steps for each selection: the input is put back to the stored files right away (also while `dropzone:pre-add` listeners are processing), the dropped files are checked against `accept`, the `dropzone:pre-add` listeners run, then the files are added (single-file mode: the new file replaces the previous one, and the files over the first one are rejected with `max_files`), the input is updated, the preview is rendered and `dropzone:change` is dispatched.

| Case                                              | Before                                         | After
| ------------------------------------------------- | ---------------------------------------------- | ---
| Single-file mode, controller connects with a file | input emptied, `dropzone:clear`                | file kept and shown
| `multiple` mode, last file removed                | no `dropzone:clear`                            | `dropzone:clear`
| Form reset                                        | drop zone keeps showing the files              | cleared, `dropzone:clear`
| Single-file mode, selection prevented by a `dropzone:pre-add` listener | drop zone emptied     | previous file kept, as in `multiple` mode
| Single-file mode, preview template                | not used                                       | rendered in the preview, its remove slot clears
| `multiple` mode, selection of files already in the list | `dropzone:change`          | no event
| Drag of text or a link over the drop zone         | `dragging`                                     | ignored
| Drag over a child of the drop zone in Safari      | `dragging` ends (`relatedTarget` is `null`)    | `dragging` kept
| Image previews                                    | read again on every render, an outdated read can show the image of a replaced file | read once per file, never shown for another file

`dropzone:change` keeps its payload (a `File` in single-file mode, a `FileList` in `multiple` mode), to stay compatible: changing it is left for 4.0. The form theme now renders the `dropzone_preview_item_template` block and the `remove_label` value in both modes. The drag state keeps the elements a drag has entered, from `dragenter` and `dragleave`, instead of reading `relatedTarget`, which WebKit does not set, and forgets the elements removed during the drag. `onMultipleChange()` is merged into `onInputChange()`. `src/Dropzone/UPGRADE.md` explains the changes for code listening to `dropzone:clear` on connect or to `dropzone:change`, and for controllers that extend this one.

Tests: unit tests for a file kept on connect without `dropzone:clear`, a prevented selection and a pending one in single-file mode, a selection of several files in single-file mode, an outdated image read, a form reset, a prevented form reset, a disconnect during a form reset, the clear button after a failed connect, the preview template in single-file mode, `dropzone:clear` after the last removal in `multiple` mode, a selection that adds no file in `multiple` mode, one read per image, an image read again after a failed read, a drag without files, a drag over a child without `relatedTarget`, and a drag over an element removed during the drag; PHPUnit tests for the template block and the `remove_label` value in single-file mode; Playwright tests for a form reset and the last removal in `multiple` mode. The new tests fail on the base of this PR. Two tests of the base changed: the single-file mode `file chosen` test now picks the file with a writable `files` property like the other tests, as `@testing-library/user-event` makes it read-only while the controller now writes the input on every selection; and the test of a prevented selection after a pending one now expects the previous file to be kept.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Documentation Improvements or additions to documentation Dropzone Feature New Feature Status: Reviewed Has been reviewed by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants