@georapbox/files-dropzone-element 2.1.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.editorconfig +15 -0
  2. package/.nvmrc +1 -0
  3. package/.prettierignore +6 -0
  4. package/.prettierrc +11 -0
  5. package/.vscode/extensions.json +8 -0
  6. package/.vscode/settings.json +8 -0
  7. package/.zed/settings.json +9 -0
  8. package/CHANGELOG.md +144 -0
  9. package/README.md +119 -103
  10. package/custom-elements-manifest.config.mjs +48 -0
  11. package/dist/custom-elements.json +509 -0
  12. package/dist/files-dropzone-defined.d.ts +1 -1
  13. package/dist/files-dropzone-defined.d.ts.map +1 -1
  14. package/dist/files-dropzone-defined.js +5 -5
  15. package/dist/files-dropzone-defined.js.map +4 -4
  16. package/dist/files-dropzone.d.ts +55 -68
  17. package/dist/files-dropzone.d.ts.map +1 -1
  18. package/dist/files-dropzone.js +5 -5
  19. package/dist/files-dropzone.js.map +4 -4
  20. package/dist/files-selector.d.ts +9 -0
  21. package/dist/files-selector.d.ts.map +1 -0
  22. package/dist/{utils/is-valid-file.d.ts → is-valid-file.d.ts} +1 -1
  23. package/dist/is-valid-file.d.ts.map +1 -0
  24. package/docs/css/main.css +190 -0
  25. package/docs/css/water.css +674 -0
  26. package/docs/favicon-32x32.png +0 -0
  27. package/docs/index.html +144 -0
  28. package/docs/js/main.js +154 -0
  29. package/docs/lib/files-dropzone.js +93 -0
  30. package/docs/lib/highlight/highlight-github.css +160 -0
  31. package/docs/lib/highlight/highlight.min.js +1160 -0
  32. package/eslint.config.mjs +42 -0
  33. package/package.json +52 -30
  34. package/scripts/build.mjs +32 -0
  35. package/scripts/copy-build-to-docs.mjs +17 -0
  36. package/scripts/dev.mjs +24 -0
  37. package/src/files-dropzone-defined.js +1 -1
  38. package/src/files-dropzone.js +70 -89
  39. package/test/files-dropzone-upgrade.test.js +39 -0
  40. package/test/files-dropzone.test.js +397 -0
  41. package/test/files-selector.test.js +96 -0
  42. package/test/is-valid-file.test.js +78 -0
  43. package/tsconfig.json +15 -0
  44. package/web-test-runner.config.js +13 -0
  45. package/dist/utils/files-selector.d.ts +0 -2
  46. package/dist/utils/files-selector.d.ts.map +0 -1
  47. package/dist/utils/is-valid-file.d.ts.map +0 -1
  48. /package/src/{utils/files-selector.js → files-selector.js} +0 -0
  49. /package/src/{utils/is-valid-file.js → is-valid-file.js} +0 -0
@@ -0,0 +1,42 @@
1
+ import globals from 'globals';
2
+ import pluginJs from '@eslint/js';
3
+ import { configs as pluginWc } from 'eslint-plugin-wc';
4
+
5
+ export default [
6
+ {
7
+ languageOptions: {
8
+ globals: {
9
+ ...globals.browser,
10
+ ...globals.node,
11
+ ...globals.chai,
12
+ ...globals.mocha
13
+ }
14
+ }
15
+ },
16
+ pluginJs.configs.recommended,
17
+ pluginWc['flat/recommended'],
18
+ pluginWc['flat/best-practice'],
19
+ {
20
+ rules: {
21
+ 'no-use-before-define': [
22
+ 'error',
23
+ {
24
+ functions: false
25
+ }
26
+ ],
27
+ curly: ['warn'],
28
+ eqeqeq: [
29
+ 'error',
30
+ 'always',
31
+ {
32
+ null: 'ignore'
33
+ }
34
+ ],
35
+ 'wc/no-child-traversal-in-connectedcallback': 'off',
36
+ 'wc/no-child-traversal-in-attributechangedcallback': 'off'
37
+ }
38
+ },
39
+ {
40
+ ignores: ['dist', 'docs/lib', 'coverage']
41
+ }
42
+ ];
package/package.json CHANGED
@@ -1,31 +1,41 @@
1
1
  {
2
2
  "name": "@georapbox/files-dropzone-element",
3
- "version": "2.1.1",
3
+ "version": "3.0.0",
4
4
  "description": "A custom element that creates a drag and drop zone for files",
5
+ "type": "module",
5
6
  "main": "dist/files-dropzone.js",
6
- "module": "dist/files-dropzone.js",
7
7
  "unpkg": "dist/files-dropzone.js",
8
- "type": "module",
9
8
  "types": "dist/files-dropzone.d.ts",
10
- "files": [
11
- "/src",
12
- "/dist"
13
- ],
9
+ "customElements": "dist/custom-elements.json",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/files-dropzone.d.ts",
13
+ "import": "./dist/files-dropzone.js"
14
+ },
15
+ "./define": {
16
+ "types": "./dist/files-dropzone.d.ts",
17
+ "import": "./dist/files-dropzone-defined.js"
18
+ },
19
+ "./custom-elements.json": "./dist/custom-elements.json"
20
+ },
14
21
  "scripts": {
15
- "lint": "eslint",
16
- "format": "prettier --ignore-unknown --write .",
22
+ "start": "npm-run-all --parallel \"serve:dev -- {1}\" dev",
23
+ "dev": "npm-run-all clean dev:esbuild",
24
+ "dev:esbuild": "node ./scripts/dev.mjs",
25
+ "serve:dev": "web-dev-server --node-resolve --open /docs/ --watch",
26
+ "build": "npm-run-all clean build:esbuild",
27
+ "build:esbuild": "node ./scripts/build.mjs",
28
+ "build:docs": "npm-run-all build build:docs:copy",
29
+ "build:docs:copy": "node ./scripts/copy-build-to-docs.mjs",
17
30
  "test": "web-test-runner",
18
31
  "test:watch": "npm run test -- --watch",
19
32
  "test:coverage": "npm run test -- --coverage",
33
+ "lint": "eslint",
34
+ "format": "prettier --ignore-unknown --write .",
20
35
  "types": "tsc --project tsconfig.json",
21
- "dev:esbuild": "node ./scripts/dev.mjs",
22
- "dev": "npm-run-all clean dev:esbuild",
23
- "serve:dev": "web-dev-server --node-resolve --open /docs/ --watch",
24
- "start": "npm-run-all --parallel \"serve:dev -- {1}\" dev",
25
- "build:esbuild": "node ./scripts/build.mjs",
26
- "build": "npm-run-all clean build:esbuild",
36
+ "analyze": "custom-elements-manifest analyze",
27
37
  "clean": "rimraf dist",
28
- "prepare": "npm-run-all clean lint test build types"
38
+ "prepublishOnly": "npm-run-all clean lint test build types analyze"
29
39
  },
30
40
  "keywords": [
31
41
  "custom element",
@@ -46,23 +56,35 @@
46
56
  },
47
57
  "homepage": "https://github.com/georapbox/files-dropzone-element#readme",
48
58
  "devDependencies": {
49
- "@eslint/js": "~9.34.0",
50
- "@esm-bundle/chai": "~4.3.4",
51
- "@open-wc/testing": "~4.0.0",
52
- "@web/dev-server": "~0.4.6",
53
- "@web/test-runner": "~0.20.2",
54
- "@web/test-runner-playwright": "~0.11.0",
55
- "esbuild": "~0.25.9",
56
- "eslint": "~9.34.0",
57
- "eslint-plugin-wc": "~3.0.1",
58
- "globals": "~16.3.0",
59
+ "@custom-elements-manifest/analyzer": "~0.11.0",
60
+ "@eslint/js": "~10.0.1",
61
+ "@esm-bundle/chai": "~4.3.4-fix.0",
62
+ "@open-wc/testing": "~5.0.0",
63
+ "@types/node": "~26.6.5",
64
+ "@web/dev-server": "~1.0.0",
65
+ "@web/test-runner": "~1.0.0",
66
+ "@web/test-runner-playwright": "~1.0.0",
67
+ "esbuild": "~0.28.2",
68
+ "eslint": "~10.14.0",
69
+ "eslint-plugin-wc": "~3.1.0",
70
+ "globals": "~17.13.0",
59
71
  "npm-run-all": "~4.1.5",
60
- "prettier": "~3.6.2",
61
- "rimraf": "~6.0.1",
62
- "sinon": "~21.0.0",
63
- "typescript": "~5.9.2"
72
+ "playwright": "~1.64.1",
73
+ "prettier": "~3.9.10",
74
+ "rimraf": "~6.1.3",
75
+ "sinon": "~22.1.0",
76
+ "typescript": "~7.0.2"
64
77
  },
65
78
  "publishConfig": {
66
79
  "access": "public"
80
+ },
81
+ "engines": {
82
+ "node": ">=24",
83
+ "npm": ">=11.17.0"
84
+ },
85
+ "allowScripts": {
86
+ "esbuild@0.28.2": true,
87
+ "fsevents@2.3.2": true,
88
+ "rs-module-lexer@2.6.0": true
67
89
  }
68
90
  }
@@ -0,0 +1,32 @@
1
+ import fs from 'fs/promises';
2
+ import * as esbuild from 'esbuild';
3
+
4
+ const PACKAGE_NAME = 'files-dropzone';
5
+
6
+ const readPkg = async () => {
7
+ return JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url)));
8
+ };
9
+
10
+ const pkg = await readPkg();
11
+
12
+ const makeBanner = pkg => `/*!
13
+ * ${pkg.name}
14
+ * ${pkg.description}
15
+ *
16
+ * @version ${pkg.version}
17
+ * @homepage ${pkg.homepage}
18
+ * @author ${pkg.author.name} <${pkg.author.email}>
19
+ * @license ${pkg.license}
20
+ */`;
21
+
22
+ await esbuild.build({
23
+ entryPoints: [`src/${PACKAGE_NAME}.js`, `src/${PACKAGE_NAME}-defined.js`],
24
+ bundle: true,
25
+ minify: true,
26
+ sourcemap: true,
27
+ format: 'esm',
28
+ outdir: 'dist',
29
+ banner: {
30
+ js: makeBanner(pkg)
31
+ }
32
+ });
@@ -0,0 +1,17 @@
1
+ import { copyFile, mkdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const PACKAGE_NAME = 'files-dropzone';
5
+ const files = [`${PACKAGE_NAME}.js`];
6
+ const sourceDir = path.resolve('dist');
7
+ const destinationDir = path.resolve('docs/lib');
8
+
9
+ await mkdir(destinationDir, { recursive: true });
10
+
11
+ const copyPromises = files.map(file => {
12
+ const source = path.join(sourceDir, file);
13
+ const destination = path.join(destinationDir, file);
14
+ return copyFile(source, destination);
15
+ });
16
+
17
+ await Promise.all(copyPromises);
@@ -0,0 +1,24 @@
1
+ import * as esbuild from 'esbuild';
2
+
3
+ const PACKAGE_NAME = 'files-dropzone';
4
+
5
+ const ctx = await esbuild.context({
6
+ entryPoints: [`src/${PACKAGE_NAME}.js`, `src/${PACKAGE_NAME}-defined.js`],
7
+ bundle: true,
8
+ sourcemap: true,
9
+ format: 'esm',
10
+ outdir: 'dist',
11
+ metafile: true,
12
+ plugins: [
13
+ {
14
+ name: 'rebuild-notify',
15
+ setup(build) {
16
+ build.onEnd(result => {
17
+ console.log(`Build ended with ${result.errors.length} errors`);
18
+ });
19
+ }
20
+ }
21
+ ]
22
+ });
23
+
24
+ await ctx.watch();
@@ -1,5 +1,5 @@
1
1
  import { FilesDropzone } from './files-dropzone.js';
2
2
 
3
- FilesDropzone.defineCustomElement();
3
+ FilesDropzone.define();
4
4
 
5
5
  export { FilesDropzone };
@@ -1,20 +1,13 @@
1
1
  // @ts-check
2
2
 
3
3
  /**
4
- * Represents a value that may be of type T, or null.
5
- *
6
- * @template T
7
- * @typedef {T | null} Nullable
8
- */
9
-
10
- /**
11
- * Stable identifier for error codes used in the `files-dropzone-error` event.
4
+ * Stable identifier for error codes used in the `dropzone-error` event.
12
5
  *
13
6
  * @typedef {typeof FilesDropzone.ERROR_CODES[keyof typeof FilesDropzone.ERROR_CODES]} FilesDropzoneErrorCode
14
7
  */
15
8
 
16
9
  /**
17
- * The detail payload for the `files-dropzone-error` event.
10
+ * The detail payload for the `dropzone-error` event.
18
11
  *
19
12
  * @typedef {Object} FilesDropzoneErrorDetail
20
13
  * @property {FilesDropzoneErrorCode} code - Stable identifier for the type of error that occurred.
@@ -27,12 +20,11 @@
27
20
  * @typedef {typeof FilesDropzone.REJECTION_CODES[keyof typeof FilesDropzone.REJECTION_CODES]} FilesDropzoneRejectionCode
28
21
  */
29
22
 
30
- import { isValidFile } from './utils/is-valid-file.js';
31
- import { getFilesFromEvent } from './utils/files-selector.js';
23
+ import { isValidFile } from './is-valid-file.js';
24
+ import { getFilesFromEvent } from './files-selector.js';
32
25
 
33
26
  const css = String.raw;
34
27
  const html = String.raw;
35
- const COMPONENT_NAME = 'files-dropzone';
36
28
  const template = document.createElement('template');
37
29
 
38
30
  const styles = css`
@@ -126,7 +118,7 @@ template.innerHTML = html`
126
118
  * @summary A custom element that allows users to drag and drop files into it.
127
119
  * @documentation https://github.com/georapbox/files-dropzone-element
128
120
  *
129
- * @tagname files-dropzone - This is the default tag name, unless overridden by the `defineCustomElement` method.
121
+ * @tagname files-dropzone - This is the default tag name, unless overridden by the `define` method.
130
122
  *
131
123
  * @property {string} accept - A comma-separated list of unique file type specifiers describing file types to allow.
132
124
  * @property {boolean} disabled - Determines whether the dropzone is disabled.
@@ -137,14 +129,14 @@ template.innerHTML = html`
137
129
  * @property {boolean} autoFocus - Automatically focuses the dropzone when it's connected to the DOM.
138
130
  * @property {boolean} noStyle - Prevents the dropzone from applying any styling.
139
131
  *
140
- * @attribute {string} accept - Reflects the accept property.
141
- * @attribute {boolean} disabled - Reflects the disabled property.
142
- * @attribute {number} max-files - Reflects the maxFiles property.
143
- * @attribute {number} max-size - Reflects the maxSize property.
144
- * @attribute {number} min-size - Reflects the minSize property.
145
- * @attribute {boolean} multiple - Reflects the multiple property.
146
- * @attribute {boolean} auto-focus - Reflects the autoFocus property.
147
- * @attribute {boolean} no-style - Reflects the noStyle property.
132
+ * @attribute {string} accept - A comma-separated list of unique file type specifiers describing file types to allow.
133
+ * @attribute {boolean} disabled - Determines whether the dropzone is disabled.
134
+ * @attribute {number} max-files - The maximum number of files allowed to be dropped.
135
+ * @attribute {number} max-size - The maximum file size allowed in bytes.
136
+ * @attribute {number} min-size - The minimum file size allowed in bytes.
137
+ * @attribute {boolean} multiple - Allows multiple files to be dropped.
138
+ * @attribute {boolean} auto-focus - Automatically focuses the dropzone when it's connected to the DOM.
139
+ * @attribute {boolean} no-style - Prevents the dropzone from applying any styling.
148
140
  *
149
141
  * @slot - The default slot content of the dropzone.
150
142
  *
@@ -167,15 +159,15 @@ template.innerHTML = html`
167
159
  * @cssproperty --dropzone-focus-box-shadow - The box shadow of the dropzone when focused.
168
160
  * @cssproperty --dropzone-transition-duration - The transition's duration for the dropzone area.
169
161
  *
170
- * @event files-dropzone-drop - Fired when files are dropped.
171
- * @event files-dropzone-drop-accepted - Fired when files dropped files are accepted.
172
- * @event files-dropzone-drop-rejected - Fired when files dropped files are rejected.
173
- * @event files-dropzone-dragenter - Fired when files are dragged into the dropzone.
174
- * @event files-dropzone-dragover - Fired when files are dragged over the dropzone.
175
- * @event files-dropzone-dragleave - Fired when files are dragged out of the dropzone.
176
- * @event files-dropzone-error - Fired when there is any error in the process of reading dropped files or directories.
162
+ * @event dropzone-drop - Dispatched when files are dropped.
163
+ * @event dropzone-drop-accepted - Dispatched when files dropped files are accepted.
164
+ * @event dropzone-drop-rejected - Dispatched when files dropped files are rejected.
165
+ * @event dropzone-dragenter - Dispatched when files are dragged into the dropzone.
166
+ * @event dropzone-dragover - Dispatched when files are dragged over the dropzone.
167
+ * @event dropzone-dragleave - Dispatched when files are dragged out of the dropzone.
168
+ * @event dropzone-error - Dispatched when there is any error in the process of reading dropped files or directories.
177
169
  *
178
- * @method defineCustomElement - Static method. Defines a custom element with the given name.
170
+ * @method define - Static method. Registers the custom element with the browser's CustomElementRegistry unless it has already been defined.
179
171
  * @method openFileDialog - Instance method. Opens the file dialog programmatically.
180
172
  */
181
173
  class FilesDropzone extends HTMLElement {
@@ -201,10 +193,10 @@ class FilesDropzone extends HTMLElement {
201
193
  INVALID_MIME_TYPE: 'INVALID_MIME_TYPE'
202
194
  });
203
195
 
204
- /** @type {Nullable<HTMLInputElement>} */
196
+ /** @type {HTMLInputElement | null} */
205
197
  #fileInput = null;
206
198
 
207
- /** @type {Nullable<HTMLElement>} */
199
+ /** @type {HTMLElement | null} */
208
200
  #dropzoneEl = null;
209
201
 
210
202
  constructor() {
@@ -216,7 +208,7 @@ class FilesDropzone extends HTMLElement {
216
208
  }
217
209
 
218
210
  if (this.shadowRoot) {
219
- this.#fileInput = /** @type {Nullable<HTMLInputElement>} */ (this.shadowRoot.getElementById('file-input'));
211
+ this.#fileInput = /** @type {HTMLInputElement | null} */ (this.shadowRoot.getElementById('file-input'));
220
212
  this.#dropzoneEl = this.shadowRoot.getElementById('dropzone');
221
213
  }
222
214
  }
@@ -293,7 +285,8 @@ class FilesDropzone extends HTMLElement {
293
285
 
294
286
  /**
295
287
  * @type {string} - A comma-separated list of unique file type specifiers describing file types to allow.
296
- * @attribute accept - Reflects the accept property.
288
+ * @attribute accept
289
+ * @default ''
297
290
  */
298
291
  get accept() {
299
292
  return this.getAttribute('accept') || '';
@@ -305,8 +298,8 @@ class FilesDropzone extends HTMLElement {
305
298
 
306
299
  /**
307
300
  * @type {boolean} - Determines whether the dropzone is disabled.
301
+ * @attribute disabled
308
302
  * @default false
309
- * @attribute disabled - Reflects the disabled property.
310
303
  */
311
304
  get disabled() {
312
305
  return this.hasAttribute('disabled');
@@ -318,8 +311,8 @@ class FilesDropzone extends HTMLElement {
318
311
 
319
312
  /**
320
313
  * @type {number} - The maximum number of files allowed to be dropped.
314
+ * @attribute max-files
321
315
  * @default Infinity
322
- * @attribute max-files - Reflects the maxFiles property.
323
316
  */
324
317
  get maxFiles() {
325
318
  const num = Number(this.getAttribute('max-files')) || 0;
@@ -337,8 +330,8 @@ class FilesDropzone extends HTMLElement {
337
330
 
338
331
  /**
339
332
  * @type {number} - The maximum file size allowed in bytes.
333
+ * @attribute max-size
340
334
  * @default Infinity
341
- * @attribute max-size - Reflects the maxSize property.
342
335
  */
343
336
  get maxSize() {
344
337
  const value = this.getAttribute('max-size');
@@ -358,8 +351,8 @@ class FilesDropzone extends HTMLElement {
358
351
 
359
352
  /**
360
353
  * @type {number} - The minimum file size allowed in bytes.
354
+ * @attribute min-size
361
355
  * @default 0
362
- * @attribute min-size - Reflects the minSize property.
363
356
  */
364
357
  get minSize() {
365
358
  const value = this.getAttribute('min-size');
@@ -379,8 +372,8 @@ class FilesDropzone extends HTMLElement {
379
372
 
380
373
  /**
381
374
  * @type {boolean} - Allows multiple files to be dropped.
375
+ * @attribute multiple
382
376
  * @default false
383
- * @attribute multiple - Reflects the multiple property.
384
377
  */
385
378
  get multiple() {
386
379
  return this.hasAttribute('multiple');
@@ -392,8 +385,8 @@ class FilesDropzone extends HTMLElement {
392
385
 
393
386
  /**
394
387
  * @type {boolean} - Automatically focuses the dropzone when it's connected to the DOM.
388
+ * @attribute auto-focus
395
389
  * @default false
396
- * @attribute auto-focus - Reflects the autoFocus property.
397
390
  */
398
391
  get autoFocus() {
399
392
  return this.hasAttribute('auto-focus');
@@ -405,8 +398,8 @@ class FilesDropzone extends HTMLElement {
405
398
 
406
399
  /**
407
400
  * @type {boolean} - Prevents the dropzone from applying any styling.
401
+ * @attribute no-style
408
402
  * @default false
409
- * @attribute no-style - Reflects the noStyle property.
410
403
  */
411
404
  get noStyle() {
412
405
  return this.hasAttribute('no-style');
@@ -417,37 +410,19 @@ class FilesDropzone extends HTMLElement {
417
410
  }
418
411
 
419
412
  /**
420
- * Emit a custom event with the given name and detail.
421
- *
422
- * @template D
423
- * @param {string} name - The name of the event.
424
- * @param {D} [detail] - The detail payload of the event.
425
- * @param {CustomEventInit<D>} [init] - Override the default event initialization options.
426
- * @returns {boolean} - Returns false if at least one event listener called `preventDefault()`, otherwise true.
427
- */
428
- #emitEvent(name, detail, init) {
429
- const options = {
430
- bubbles: true,
431
- composed: true,
432
- cancelable: false,
433
- ...init,
434
- detail
435
- };
436
- const evt = new CustomEvent(`${COMPONENT_NAME}-${name}`, options);
437
- return this.dispatchEvent(evt);
438
- }
439
-
440
- /**
441
- * Emit a standardized error event with typed detail.
413
+ * Dispatches an event, using CustomEvent when detail is provided.
442
414
  *
443
- * @param {FilesDropzoneErrorCode} code - Stable identifier for the type of error that occurred.
444
- * @param {unknown} error - The underlying error that triggered the event.
415
+ * @template T
416
+ * @param {string} type - The event type.
417
+ * @param {T} [detail] - Optional event detail.
418
+ * @param {{ cancelable?: boolean }} [options] - Event options.
419
+ * @returns {boolean} Whether the event was not canceled.
445
420
  */
446
- #emitErrorEvent(code, error) {
447
- /** @type {FilesDropzoneErrorDetail} */
448
- const detail = { code, error };
421
+ #emit(type, detail, { cancelable = false } = {}) {
422
+ const options = { bubbles: true, composed: true, cancelable };
423
+ const event = detail === undefined ? new Event(type, options) : new CustomEvent(type, { ...options, detail });
449
424
 
450
- this.#emitEvent('error', detail);
425
+ return this.dispatchEvent(event);
451
426
  }
452
427
 
453
428
  /**
@@ -459,7 +434,10 @@ class FilesDropzone extends HTMLElement {
459
434
  try {
460
435
  this.#handleFilesSelect(await getFilesFromEvent(evt));
461
436
  } catch (err) {
462
- this.#emitErrorEvent(FilesDropzone.ERROR_CODES.FILE_INPUT_CHANGE_FAILED, err);
437
+ this.#emit('dropzone-error', {
438
+ code: FilesDropzone.ERROR_CODES.FILE_INPUT_CHANGE_FAILED,
439
+ error: err
440
+ });
463
441
  }
464
442
  };
465
443
 
@@ -471,7 +449,7 @@ class FilesDropzone extends HTMLElement {
471
449
  return;
472
450
  }
473
451
 
474
- this.#emitEvent('dragenter');
452
+ this.#emit('dropzone-dragenter');
475
453
  };
476
454
 
477
455
  /**
@@ -494,7 +472,7 @@ class FilesDropzone extends HTMLElement {
494
472
  this.#dropzoneEl.part.add('dropzone--dragover');
495
473
  }
496
474
 
497
- this.#emitEvent('dragover');
475
+ this.#emit('dropzone-dragover');
498
476
  };
499
477
 
500
478
  /**
@@ -510,7 +488,7 @@ class FilesDropzone extends HTMLElement {
510
488
  this.#dropzoneEl.part.remove('dropzone--dragover');
511
489
  }
512
490
 
513
- this.#emitEvent('dragleave');
491
+ this.#emit('dropzone-dragleave');
514
492
  };
515
493
 
516
494
  /**
@@ -533,7 +511,10 @@ class FilesDropzone extends HTMLElement {
533
511
  try {
534
512
  this.#handleFilesSelect(await getFilesFromEvent(evt));
535
513
  } catch (err) {
536
- this.#emitErrorEvent(FilesDropzone.ERROR_CODES.DROP_EVENT_PROCESSING_FAILED, err);
514
+ this.#emit('dropzone-error', {
515
+ code: FilesDropzone.ERROR_CODES.DROP_EVENT_PROCESSING_FAILED,
516
+ error: err
517
+ });
537
518
  }
538
519
  };
539
520
 
@@ -643,14 +624,14 @@ class FilesDropzone extends HTMLElement {
643
624
  }
644
625
  }
645
626
 
646
- this.#emitEvent('drop', { acceptedFiles, rejectedFiles });
627
+ this.#emit('dropzone-drop', { acceptedFiles, rejectedFiles });
647
628
 
648
629
  if (acceptedFiles.length > 0) {
649
- this.#emitEvent('drop-accepted', { acceptedFiles });
630
+ this.#emit('dropzone-drop-accepted', { acceptedFiles });
650
631
  }
651
632
 
652
633
  if (rejectedFiles.length > 0) {
653
- this.#emitEvent('drop-rejected', { rejectedFiles });
634
+ this.#emit('dropzone-drop-rejected', { rejectedFiles });
654
635
  }
655
636
 
656
637
  if (this.#fileInput) {
@@ -673,7 +654,10 @@ class FilesDropzone extends HTMLElement {
673
654
  try {
674
655
  this.#fileInput.showPicker();
675
656
  } catch (err) {
676
- this.#emitErrorEvent(FilesDropzone.ERROR_CODES.FILE_DIALOG_OPEN_FAILED, err);
657
+ this.#emit('dropzone-error', {
658
+ code: FilesDropzone.ERROR_CODES.FILE_DIALOG_OPEN_FAILED,
659
+ error: err
660
+ });
677
661
  }
678
662
  return;
679
663
  }
@@ -695,10 +679,9 @@ class FilesDropzone extends HTMLElement {
695
679
  * @param {'accept' | 'disabled' | 'maxFiles' | 'maxSize' | 'minSize' | 'multiple' | 'autoFocus' | 'noStyle'} prop - The property name to upgrade.
696
680
  */
697
681
  #upgradeProperty(prop) {
698
- /** @type {any} */
699
- const instance = this;
682
+ const instance = /** @type {HTMLElement & Record<string, unknown>} */ (this);
700
683
 
701
- if (Object.prototype.hasOwnProperty.call(instance, prop)) {
684
+ if (Object.hasOwn(instance, prop)) {
702
685
  const value = instance[prop];
703
686
  delete instance[prop];
704
687
  instance[prop] = value;
@@ -706,18 +689,16 @@ class FilesDropzone extends HTMLElement {
706
689
  }
707
690
 
708
691
  /**
709
- * Defines a custom element with the given name.
710
- * The name must contain a dash (-).
711
- *
712
- * @param {string} [elementName='files-dropzone'] - The name of the custom element.
713
- * @example
692
+ * Registers the custom element with the browser's CustomElementRegistry unless
693
+ * it has already been defined.
714
694
  *
715
- * FilesDropzone.defineCustomElement('my-dropzone');
695
+ * @param {string} [tagName='files-dropzone'] - The tag name to use for the custom element.
716
696
  */
717
- static defineCustomElement(elementName = COMPONENT_NAME) {
718
- if (typeof window !== 'undefined' && !window.customElements.get(elementName)) {
719
- window.customElements.define(elementName, FilesDropzone);
697
+ static define(tagName = 'files-dropzone') {
698
+ if (typeof window === 'undefined' || window.customElements.get(tagName)) {
699
+ return;
720
700
  }
701
+ window.customElements.define(tagName, FilesDropzone);
721
702
  }
722
703
  }
723
704
 
@@ -0,0 +1,39 @@
1
+ import { expect, fixture, fixtureCleanup, html } from '@open-wc/testing';
2
+ import { FilesDropzone } from '../src/files-dropzone.js';
3
+
4
+ describe('files-dropzone upgrading', () => {
5
+ afterEach(() => {
6
+ fixtureCleanup();
7
+ });
8
+
9
+ it('should preserve properties set before the custom element is defined', async () => {
10
+ const container = await fixture(html`<div></div>`);
11
+ const el = document.createElement('files-dropzone');
12
+
13
+ // Set properties before upgrading
14
+ el.accept = 'image/*';
15
+ el.disabled = true;
16
+ el.maxFiles = 5;
17
+ el.maxSize = 1024 * 1024;
18
+ el.minSize = 1024;
19
+ el.multiple = true;
20
+ el.autoFocus = true;
21
+ el.noStyle = true;
22
+
23
+ // Define and explicitly upgrade the custom element
24
+ FilesDropzone.define();
25
+ customElements.upgrade(el);
26
+
27
+ // Connect the element so connectedCallback runs
28
+ container.appendChild(el);
29
+
30
+ expect(el.getAttribute('accept')).to.equal('image/*');
31
+ expect(el.getAttribute('disabled')).to.equal('');
32
+ expect(el.getAttribute('max-files')).to.equal('5');
33
+ expect(el.getAttribute('max-size')).to.equal('1048576');
34
+ expect(el.getAttribute('min-size')).to.equal('1024');
35
+ expect(el.getAttribute('multiple')).to.equal('');
36
+ expect(el.getAttribute('auto-focus')).to.equal('');
37
+ expect(el.getAttribute('no-style')).to.equal('');
38
+ });
39
+ });