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/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/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 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"] +}