From 6a036914eae46748baed12983b51e63bf1906837 Mon Sep 17 00:00:00 2001 From: Matias Simon Date: Wed, 16 Sep 2026 18:52:06 +0200 Subject: [PATCH 1/6] Type clickable and hiddenInputContainer as what they accept The option types are derived from `defaultOptions`, so each one is inferred from its default value. `clickable: true` came out as `boolean` and `hiddenInputContainer: "body"` as `string`, neither of which matches what the option documents two lines above it or what Dropzone has always accepted: `clickable` is passed to `getElements`, which takes an element, a CSS selector or an array of those, and `hiddenInputContainer` to `getElement`, which takes an element or a selector. Widen both the way `previewsContainer` already is. This is a declaration fix only -- the unit tests already cover `clickable` as `true`, as a CSS selector, as an element and as an array, and they are untouched. Fixes #2373 --- .changeset/clickable-option-type.md | 9 +++++++++ packages/dropzone/src/options.ts | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/clickable-option-type.md diff --git a/.changeset/clickable-option-type.md b/.changeset/clickable-option-type.md new file mode 100644 index 000000000..db1d55f1e --- /dev/null +++ b/.changeset/clickable-option-type.md @@ -0,0 +1,9 @@ +--- +"dropzone": patch +--- + +Fix the declared type of `clickable` and `hiddenInputContainer`. + +Both options are derived from their default values, so `clickable: true` was inferred as `boolean` and `hiddenInputContainer: "body"` as `string`. That contradicted what each one documents and what both have always accepted at runtime, and TypeScript rejected the documented forms. + +`clickable` is now `boolean | string | HTMLElement | (string | HTMLElement)[]` and `hiddenInputContainer` is `string | HTMLElement`. Nothing changes at runtime. diff --git a/packages/dropzone/src/options.ts b/packages/dropzone/src/options.ts index 84df83729..a6e714b04 100644 --- a/packages/dropzone/src/options.ts +++ b/packages/dropzone/src/options.ts @@ -203,7 +203,7 @@ let defaultOptions = { * or an array of those. In that case, all of those elements will trigger an * upload when clicked. */ - clickable: true, + clickable: true as boolean | string | HTMLElement | (string | HTMLElement)[], /** * Whether hidden files in directories should be ignored. @@ -274,7 +274,7 @@ let defaultOptions = { * * Can be a selector string, or an element directly. */ - hiddenInputContainer: "body", + hiddenInputContainer: "body" as string | HTMLElement, /** * If null, no capture type will be specified From c8da4322746038f08bb7889161e1b627ca95972b Mon Sep 17 00:00:00 2001 From: Matias Simon Date: Wed, 16 Sep 2026 18:52:11 +0200 Subject: [PATCH 2/6] Check the public types as part of typecheck `DropzoneOptions` is derived from `defaultOptions`, so every option is typed by whatever its default happens to be. Where a default is narrower than what the option accepts -- `clickable: true` standing in for an element, a selector or an array of those -- the `as` widening in options.ts is the only thing keeping the declaration honest, and nothing in the suite would notice it being dropped. The unit tests cannot: they are JavaScript, and this is a compile-time property. Add test/types, checked by tsc and never emitted. It needs its own tsconfig because the build one is `emitDeclarationOnly` over src, so assertions living under src would ship to dist as declarations of their own; `pnpm build:types` still emits only the four src declarations. Reverting the widening in options.ts now fails `pnpm typecheck` with the four errors from #2373. The `@ts-expect-error` lines assert in the other direction: each one fails the build if it stops being an error, so they also pin the types against being widened to `any`. --- packages/dropzone/package.json | 2 +- .../dropzone/test/types/options.test-d.ts | 44 +++++++++++++++++++ packages/dropzone/tsconfig.typecheck.json | 13 ++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 packages/dropzone/test/types/options.test-d.ts create mode 100644 packages/dropzone/tsconfig.typecheck.json diff --git a/packages/dropzone/package.json b/packages/dropzone/package.json index 2926f97a0..b8df8532b 100644 --- a/packages/dropzone/package.json +++ b/packages/dropzone/package.json @@ -52,7 +52,7 @@ "build": "vite build && vite build --config vite.config.global.mjs && pnpm run css && pnpm run build:types", "css": "lightningcss --minify --sourcemap --browserslist src/dropzone.css -o dist/dropzone.css && lightningcss --minify --sourcemap --browserslist src/basic.css -o dist/basic.css", "build:types": "tsc", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit -p tsconfig.typecheck.json", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest", diff --git a/packages/dropzone/test/types/options.test-d.ts b/packages/dropzone/test/types/options.test-d.ts new file mode 100644 index 000000000..a3b744dde --- /dev/null +++ b/packages/dropzone/test/types/options.test-d.ts @@ -0,0 +1,44 @@ +// Assertions about the option types, checked by `pnpm typecheck` and never +// emitted -- see tsconfig.typecheck.json. +// +// `DropzoneOptions` is derived from the default values, so an option is typed +// by whatever its default happens to be. Where a default is narrower than what +// the option accepts, the widening in options.ts is the only thing keeping the +// declaration honest, and nothing else would notice it being dropped. Hence +// these. A `@ts-expect-error` is itself an assertion: it fails the build if the +// line it marks stops being an error, so it also pins the type against being +// widened all the way to `any`. +import type { DropzoneOptions } from "../../src/options"; + +let element: HTMLElement = document.createElement("div"); + +// clickable: true for the dropzone element, false for nothing, or an element, +// a CSS selector, or an array of those. See #2373. +export const clickableTrue: DropzoneOptions = { clickable: true }; +export const clickableFalse: DropzoneOptions = { clickable: false }; +export const clickableElement: DropzoneOptions = { clickable: element }; +export const clickableSelector: DropzoneOptions = { clickable: ".pick-files" }; +export const clickableList: DropzoneOptions = { clickable: [element, ".pick-files"] }; + +// @ts-expect-error clickable is not a number +export const clickableNumber: DropzoneOptions = { clickable: 42 }; + +// hiddenInputContainer: a selector string or an element. +export const hiddenInputSelector: DropzoneOptions = { hiddenInputContainer: "body" }; +export const hiddenInputElement: DropzoneOptions = { hiddenInputContainer: element }; + +// @ts-expect-error hiddenInputContainer is not a number +export const hiddenInputNumber: DropzoneOptions = { hiddenInputContainer: 42 }; + +// previewsContainer: a selector, an element, or false to opt out. +export const previewsSelector: DropzoneOptions = { previewsContainer: ".previews" }; +export const previewsElement: DropzoneOptions = { previewsContainer: element }; +export const previewsFalse: DropzoneOptions = { previewsContainer: false }; + +// @ts-expect-error previewsContainer is not a number +export const previewsNumber: DropzoneOptions = { previewsContainer: 42 }; + +// The lack of an index signature is deliberate: a misspelled option has to be +// an error rather than an unused custom key. See the note on DropzoneOptions. +// @ts-expect-error maxFileSize is the wrong capitalisation of maxFilesize +export const misspelled: DropzoneOptions = { maxFileSize: 4 }; diff --git a/packages/dropzone/tsconfig.typecheck.json b/packages/dropzone/tsconfig.typecheck.json new file mode 100644 index 000000000..5ad41af08 --- /dev/null +++ b/packages/dropzone/tsconfig.typecheck.json @@ -0,0 +1,13 @@ +{ + // Type checking only, so it can reach files the build must not emit: + // test/types holds assertions about the public types, which would otherwise + // land in dist/ as declarations of their own. + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "declaration": false, + "emitDeclarationOnly": false, + "rootDir": "." + }, + "include": ["src/**/*.ts", "src/types.d.ts", "test/types/**/*.ts"] +} From bb1235ec3a8c227fffba47a485fe1edc18cd3cc7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:57:50 +0000 Subject: [PATCH 3/6] Version packages --- .changeset/clickable-option-type.md | 9 --------- packages/dropzone/CHANGELOG.md | 10 ++++++++++ packages/dropzone/package.json | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) delete mode 100644 .changeset/clickable-option-type.md diff --git a/.changeset/clickable-option-type.md b/.changeset/clickable-option-type.md deleted file mode 100644 index db1d55f1e..000000000 --- a/.changeset/clickable-option-type.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"dropzone": patch ---- - -Fix the declared type of `clickable` and `hiddenInputContainer`. - -Both options are derived from their default values, so `clickable: true` was inferred as `boolean` and `hiddenInputContainer: "body"` as `string`. That contradicted what each one documents and what both have always accepted at runtime, and TypeScript rejected the documented forms. - -`clickable` is now `boolean | string | HTMLElement | (string | HTMLElement)[]` and `hiddenInputContainer` is `string | HTMLElement`. Nothing changes at runtime. diff --git a/packages/dropzone/CHANGELOG.md b/packages/dropzone/CHANGELOG.md index aca6e2051..4aa6007e9 100644 --- a/packages/dropzone/CHANGELOG.md +++ b/packages/dropzone/CHANGELOG.md @@ -1,3 +1,13 @@ +## 6.3.3 + +### Patch Changes + +- [#2375](https://github.com/enyo/dropzone/pull/2375) [`6a03691`](https://github.com/enyo/dropzone/commit/6a036914eae46748baed12983b51e63bf1906837) - Fix the declared type of `clickable` and `hiddenInputContainer`. + + Both options are derived from their default values, so `clickable: true` was inferred as `boolean` and `hiddenInputContainer: "body"` as `string`. That contradicted what each one documents and what both have always accepted at runtime, and TypeScript rejected the documented forms. + + `clickable` is now `boolean | string | HTMLElement | (string | HTMLElement)[]` and `hiddenInputContainer` is `string | HTMLElement`. Nothing changes at runtime. + ## 6.3.2 ### Patch Changes diff --git a/packages/dropzone/package.json b/packages/dropzone/package.json index b8df8532b..abfbee39d 100644 --- a/packages/dropzone/package.json +++ b/packages/dropzone/package.json @@ -1,6 +1,6 @@ { "name": "dropzone", - "version": "6.3.2", + "version": "6.3.3", "description": "Handles drag and drop of files for you.", "keywords": [ "drag and drop", From 0b302eee30dd709ea00f58ad43d33e3a48502cbb Mon Sep 17 00:00:00 2001 From: Matias Simon Date: Wed, 16 Sep 2026 19:44:16 +0200 Subject: [PATCH 4/6] Don't let a second destroy() evict another instance `splice(indexOf(this), 1)` removes the last element whenever `indexOf` returns -1, so destroying an instance that was already unregistered -- calling `destroy()` twice is enough -- dropped an unrelated, live Dropzone out of `Dropzone.instances`. Refs #2367 --- .changeset/destroy-instances.md | 5 +++++ packages/dropzone/src/dropzone.ts | 6 +++++- packages/dropzone/test/unit-tests/all.js | 12 ++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .changeset/destroy-instances.md diff --git a/.changeset/destroy-instances.md b/.changeset/destroy-instances.md new file mode 100644 index 000000000..102182c0b --- /dev/null +++ b/.changeset/destroy-instances.md @@ -0,0 +1,5 @@ +--- +"dropzone": patch +--- + +Stop `destroy()` removing a different instance. `Dropzone.instances.splice(indexOf(this), 1)` dropped the last entry whenever `indexOf` returned -1 — calling `destroy()` twice was enough — evicting an unrelated live Dropzone from the registry. diff --git a/packages/dropzone/src/dropzone.ts b/packages/dropzone/src/dropzone.ts index 89f8a13aa..1c4a431ab 100644 --- a/packages/dropzone/src/dropzone.ts +++ b/packages/dropzone/src/dropzone.ts @@ -547,7 +547,11 @@ export default class Dropzone extends Emitter { this.hiddenFileInput = null; } delete this.element.dropzone; - return Dropzone.instances.splice(Dropzone.instances.indexOf(this), 1); + // `indexOf` returns -1 for an instance that is no longer registered -- + // destroying twice is enough -- and `splice(-1, 1)` would then quietly + // drop the last entry, which is a different, live Dropzone. + let index = Dropzone.instances.indexOf(this); + return index === -1 ? [] : Dropzone.instances.splice(index, 1); } updateTotalUploadProgress() { diff --git a/packages/dropzone/test/unit-tests/all.js b/packages/dropzone/test/unit-tests/all.js index c2220ab09..779086464 100644 --- a/packages/dropzone/test/unit-tests/all.js +++ b/packages/dropzone/test/unit-tests/all.js @@ -807,6 +807,18 @@ describe("Dropzone", function () { dropzone.destroy(); return expect(Dropzone.instances.indexOf(dropzone) === -1).toBeTruthy(); }); + + it("should leave other instances alone when destroyed twice", function () { + let other = new Dropzone(Dropzone.createElement("
"), { url: "url" }); + + dropzone.destroy(); + // The second call finds nothing to remove. `splice(-1, 1)` used to + // take the last entry regardless, evicting an unrelated live instance. + dropzone.destroy(); + + expect(Dropzone.instances.indexOf(other) !== -1).toBeTruthy(); + other.destroy(); + }); }); describe(".filesize()", function () { From dc5d7bfb1dd450baa467d4682d9fb3d12be30758 Mon Sep 17 00:00:00 2001 From: Matias Simon Date: Wed, 16 Sep 2026 19:44:23 +0200 Subject: [PATCH 5/6] Compare tagName against FORM, not form `element.tagName` is upper case on HTML elements, so `init()`'s check never matched and `enctype="multipart/form-data"` was never set on a form Dropzone was attached to. `_addFormElementData` spells the same check `"FORM"` and does work. Refs #2367 --- .changeset/form-enctype.md | 5 +++++ packages/dropzone/src/dropzone.ts | 5 +++-- packages/dropzone/test/unit-tests/all.js | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 .changeset/form-enctype.md diff --git a/.changeset/form-enctype.md b/.changeset/form-enctype.md new file mode 100644 index 000000000..f9867a3f2 --- /dev/null +++ b/.changeset/form-enctype.md @@ -0,0 +1,5 @@ +--- +"dropzone": patch +--- + +Set `enctype="multipart/form-data"` on a form again. `init()` compared `tagName` against lower-case `"form"`, which never matches, so the attribute was never set. Only affects forms that are also submitted natively; the XHR upload is unchanged. diff --git a/packages/dropzone/src/dropzone.ts b/packages/dropzone/src/dropzone.ts index 1c4a431ab..89131f7ec 100644 --- a/packages/dropzone/src/dropzone.ts +++ b/packages/dropzone/src/dropzone.ts @@ -334,8 +334,9 @@ export default class Dropzone extends Emitter { // The function that gets called when Dropzone is initialized. You // can (and should) setup event listeners inside this function. init() { - // In case it isn't set already - if (this.element.tagName === "form") { + // In case it isn't set already. `tagName` is upper case on HTML elements, + // so this never matched and the attribute was never set. + if (this.element.tagName === "FORM") { this.element.setAttribute("enctype", "multipart/form-data"); } diff --git a/packages/dropzone/test/unit-tests/all.js b/packages/dropzone/test/unit-tests/all.js index 779086464..7d00c6452 100644 --- a/packages/dropzone/test/unit-tests/all.js +++ b/packages/dropzone/test/unit-tests/all.js @@ -764,6 +764,24 @@ describe("Dropzone", function () { }, 10); }))); + describe("init()", function () { + it("should set the enctype on a form element", function () { + let form = Dropzone.createElement("
"); + let formDropzone = new Dropzone(form, { url: "url" }); + + expect(form.getAttribute("enctype")).toBe("multipart/form-data"); + formDropzone.destroy(); + }); + + it("should leave an existing enctype alone on a non-form element", function () { + let div = Dropzone.createElement("
"); + let divDropzone = new Dropzone(div, { url: "url" }); + + expect(div.getAttribute("enctype")).toBe(null); + divDropzone.destroy(); + }); + }); + describe(".destroy()", function () { it("should properly cancel all pending uploads and remove all file references", () => new Promise((done) => { From 552cce2868bc03ee6f2e313965dc2cadbe66449d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:50:06 +0000 Subject: [PATCH 6/6] Version packages --- .changeset/destroy-instances.md | 5 ----- .changeset/form-enctype.md | 5 ----- packages/dropzone/CHANGELOG.md | 8 ++++++++ packages/dropzone/package.json | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) delete mode 100644 .changeset/destroy-instances.md delete mode 100644 .changeset/form-enctype.md diff --git a/.changeset/destroy-instances.md b/.changeset/destroy-instances.md deleted file mode 100644 index 102182c0b..000000000 --- a/.changeset/destroy-instances.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"dropzone": patch ---- - -Stop `destroy()` removing a different instance. `Dropzone.instances.splice(indexOf(this), 1)` dropped the last entry whenever `indexOf` returned -1 — calling `destroy()` twice was enough — evicting an unrelated live Dropzone from the registry. diff --git a/.changeset/form-enctype.md b/.changeset/form-enctype.md deleted file mode 100644 index f9867a3f2..000000000 --- a/.changeset/form-enctype.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"dropzone": patch ---- - -Set `enctype="multipart/form-data"` on a form again. `init()` compared `tagName` against lower-case `"form"`, which never matches, so the attribute was never set. Only affects forms that are also submitted natively; the XHR upload is unchanged. diff --git a/packages/dropzone/CHANGELOG.md b/packages/dropzone/CHANGELOG.md index 4aa6007e9..8aa2f1838 100644 --- a/packages/dropzone/CHANGELOG.md +++ b/packages/dropzone/CHANGELOG.md @@ -1,3 +1,11 @@ +## 6.3.4 + +### Patch Changes + +- [#2377](https://github.com/enyo/dropzone/pull/2377) [`0b302ee`](https://github.com/enyo/dropzone/commit/0b302eee30dd709ea00f58ad43d33e3a48502cbb) - Stop `destroy()` removing a different instance. `Dropzone.instances.splice(indexOf(this), 1)` dropped the last entry whenever `indexOf` returned -1 — calling `destroy()` twice was enough — evicting an unrelated live Dropzone from the registry. + +- [#2377](https://github.com/enyo/dropzone/pull/2377) [`dc5d7bf`](https://github.com/enyo/dropzone/commit/dc5d7bfb1dd450baa467d4682d9fb3d12be30758) - Set `enctype="multipart/form-data"` on a form again. `init()` compared `tagName` against lower-case `"form"`, which never matches, so the attribute was never set. Only affects forms that are also submitted natively; the XHR upload is unchanged. + ## 6.3.3 ### Patch Changes diff --git a/packages/dropzone/package.json b/packages/dropzone/package.json index abfbee39d..02744eec5 100644 --- a/packages/dropzone/package.json +++ b/packages/dropzone/package.json @@ -1,6 +1,6 @@ { "name": "dropzone", - "version": "6.3.3", + "version": "6.3.4", "description": "Handles drag and drop of files for you.", "keywords": [ "drag and drop",