Skip to content

Commit 94ac3a6

Browse files
authored
chore(dropzone): establish file structure (#6436)
* chore(dropzone): establish file structure * chore(dropzone): address feedback
1 parent b8d0a57 commit 94ac3a6

10 files changed

Lines changed: 564 additions & 24 deletions

File tree

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/**
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import { property } from 'lit/decorators.js';
14+
15+
import { SpectrumElement } from '@spectrum-web-components/core/element/index.js';
16+
17+
import {
18+
DROP_EFFECTS,
19+
type DropEffect,
20+
SWC_DROPZONE_DRAGLEAVE_EVENT,
21+
SWC_DROPZONE_DRAGOVER_EVENT,
22+
SWC_DROPZONE_DROP_EVENT,
23+
SWC_DROPZONE_SHOULD_ACCEPT_EVENT,
24+
} from './Dropzone.types.js';
25+
26+
/**
27+
* Base class for the `<swc-dropzone>` drop zone component.
28+
*
29+
* Encapsulates all drag-and-drop event handling, state management, and
30+
* validation logic. Rendering, ARIA, and CSS are provided by the concrete
31+
* SWC subclass.
32+
*
33+
* @slot - Slot for the illustrated message, browse control, and any uploaded content.
34+
*
35+
* @fires swc-dropzone-should-accept - Cancelable event fired on `dragover`. Cancel to
36+
* reject the dragged payload and set the cursor to `none`.
37+
* @fires swc-dropzone-dragover - Fired when dragged files are over the zone and accepted.
38+
* @fires swc-dropzone-dragleave - Fired when dragged files leave the zone after a 100 ms
39+
* debounce. `event.detail.dataTransfer` may be `null` if the browser has already ended
40+
* the drag session by the time the event fires.
41+
* @fires swc-dropzone-drop - Fired when files are dropped on the zone. `element.dragged`
42+
* is still `true` when this event fires; it transitions to `false` after dispatch.
43+
*/
44+
export abstract class DropzoneBase extends SpectrumElement {
45+
// ──────────────────────────
46+
// SHARED API
47+
// ──────────────────────────
48+
49+
/**
50+
* Whether files are currently being dragged over the drop zone.
51+
* Set automatically by the component; also settable to reflect programmatic state.
52+
*
53+
* @attr dragged
54+
* @default false
55+
*/
56+
@property({ type: Boolean, reflect: true })
57+
public dragged = false;
58+
59+
/**
60+
* Whether the drop zone has received a file and is in the filled state.
61+
* Set by consuming code after a successful drop or browse-file selection to
62+
* switch the zone to its filled visual.
63+
*
64+
* @attr filled
65+
* @default false
66+
*/
67+
@property({ type: Boolean, reflect: true })
68+
public filled = false;
69+
70+
/**
71+
* The OS drag-cursor feedback shown while a file is held over the zone.
72+
* Maps directly to `DataTransfer.dropEffect`. Does not reflect as an
73+
* attribute because it controls browser chrome, not component state.
74+
*
75+
* @type {'copy' | 'move' | 'link' | 'none'}
76+
* @default 'copy'
77+
*/
78+
public get dropEffect(): DropEffect {
79+
return this._dropEffect;
80+
}
81+
82+
public set dropEffect(value: DropEffect) {
83+
if ((DROP_EFFECTS as readonly string[]).includes(value)) {
84+
this._dropEffect = value;
85+
} else if (window.__swc?.DEBUG) {
86+
window.__swc?.warn(
87+
this,
88+
`<${this.localName}> "dropEffect" received an invalid value: "${value}". Must be one of: ${DROP_EFFECTS.join(', ')}.`,
89+
'https://opensource.adobe.com/spectrum-web-components/?path=/docs/dropzone--docs',
90+
{ type: 'api' }
91+
);
92+
}
93+
}
94+
95+
private _dropEffect: DropEffect = 'copy';
96+
97+
// ──────────────────────────
98+
// IMPLEMENTATION
99+
// ──────────────────────────
100+
101+
/** Timer ID for debounced dragleave — prevents flickering on child drag events. */
102+
private _dragLeaveTimer: ReturnType<typeof setTimeout> | null = null;
103+
104+
public override connectedCallback(): void {
105+
super.connectedCallback();
106+
this.addEventListener('drop', this._onDrop);
107+
this.addEventListener('dragover', this._onDragOver);
108+
this.addEventListener('dragleave', this._onDragLeave);
109+
}
110+
111+
public override disconnectedCallback(): void {
112+
super.disconnectedCallback();
113+
this.removeEventListener('drop', this._onDrop);
114+
this.removeEventListener('dragover', this._onDragOver);
115+
this.removeEventListener('dragleave', this._onDragLeave);
116+
this._clearDragLeaveTimer();
117+
}
118+
119+
/** @internal */
120+
private readonly _onDragOver = (event: DragEvent): void => {
121+
event.preventDefault();
122+
123+
const shouldAcceptEvent = new CustomEvent<DragEvent>(
124+
SWC_DROPZONE_SHOULD_ACCEPT_EVENT,
125+
{
126+
bubbles: true,
127+
cancelable: true,
128+
composed: true,
129+
detail: event,
130+
}
131+
);
132+
133+
const accepted = this.dispatchEvent(shouldAcceptEvent);
134+
135+
if (!event.dataTransfer) {
136+
return;
137+
}
138+
139+
if (!accepted) {
140+
event.dataTransfer.dropEffect = 'none';
141+
return;
142+
}
143+
144+
this._clearDragLeaveTimer();
145+
146+
if (!this.dragged) {
147+
this.dragged = true;
148+
this._onDragStateChange(true);
149+
}
150+
151+
event.dataTransfer.dropEffect = this._dropEffect;
152+
153+
this.dispatchEvent(
154+
new CustomEvent<DragEvent>(SWC_DROPZONE_DRAGOVER_EVENT, {
155+
bubbles: true,
156+
composed: true,
157+
detail: event,
158+
})
159+
);
160+
};
161+
162+
/** @internal */
163+
private readonly _onDragLeave = (event: DragEvent): void => {
164+
if (event.relatedTarget && this.contains(event.relatedTarget as Node)) {
165+
return;
166+
}
167+
168+
this._clearDragLeaveTimer();
169+
170+
this._dragLeaveTimer = setTimeout(() => {
171+
this._dragLeaveTimer = null;
172+
this.dragged = false;
173+
this._onDragStateChange(false);
174+
175+
this.dispatchEvent(
176+
new CustomEvent<DragEvent>(SWC_DROPZONE_DRAGLEAVE_EVENT, {
177+
bubbles: true,
178+
composed: true,
179+
detail: event,
180+
})
181+
);
182+
}, 100);
183+
};
184+
185+
/** @internal */
186+
private readonly _onDrop = (event: DragEvent): void => {
187+
event.preventDefault();
188+
189+
if (!this.dragged) {
190+
return;
191+
}
192+
193+
this._clearDragLeaveTimer();
194+
// Update the status region synchronously before dispatch (matching _onDragLeave),
195+
// and dispatch while `dragged` is still `true` so listeners read correct state.
196+
this._onDragStateChange(false);
197+
this.dispatchEvent(
198+
new CustomEvent<DragEvent>(SWC_DROPZONE_DROP_EVENT, {
199+
bubbles: true,
200+
composed: true,
201+
detail: event,
202+
})
203+
);
204+
this.dragged = false;
205+
};
206+
207+
// ──────────────────────────────
208+
// API TO OVERRIDE
209+
// ──────────────────────────────
210+
211+
/**
212+
* Called when the `dragged` state changes. Subclasses implement this to
213+
* update the shadow DOM status region for accessibility announcements.
214+
*
215+
* @param isDragged - `true` when drag enters; `false` when it leaves.
216+
* @internal
217+
*/
218+
protected abstract _onDragStateChange(isDragged: boolean): void;
219+
220+
/** @internal */
221+
private _clearDragLeaveTimer(): void {
222+
if (this._dragLeaveTimer !== null) {
223+
clearTimeout(this._dragLeaveTimer);
224+
this._dragLeaveTimer = null;
225+
}
226+
}
227+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
// ──────────────────
14+
// DROP EFFECT
15+
// ──────────────────
16+
17+
/** Valid OS drag-cursor feedback values for the drop zone. */
18+
export type DropEffect = 'copy' | 'move' | 'link' | 'none';
19+
20+
export const DROP_EFFECTS = [
21+
'copy',
22+
'move',
23+
'link',
24+
'none',
25+
] as const satisfies readonly DropEffect[];
26+
27+
// ──────────────────
28+
// EVENTS
29+
// ──────────────────
30+
31+
/** Fired when files are dragged over the drop zone; cancelable to reject. */
32+
export const SWC_DROPZONE_SHOULD_ACCEPT_EVENT = 'swc-dropzone-should-accept';
33+
34+
/** Fired when files are dragged over the drop zone and accepted. */
35+
export const SWC_DROPZONE_DRAGOVER_EVENT = 'swc-dropzone-dragover';
36+
37+
/** Fired when dragged files leave the drop zone without being dropped. */
38+
export const SWC_DROPZONE_DRAGLEAVE_EVENT = 'swc-dropzone-dragleave';
39+
40+
/** Fired when files are dropped on the drop zone. */
41+
export const SWC_DROPZONE_DROP_EVENT = 'swc-dropzone-drop';
42+
43+
declare global {
44+
interface GlobalEventHandlersEventMap {
45+
[SWC_DROPZONE_SHOULD_ACCEPT_EVENT]: CustomEvent<DragEvent>;
46+
[SWC_DROPZONE_DRAGOVER_EVENT]: CustomEvent<DragEvent>;
47+
[SWC_DROPZONE_DRAGLEAVE_EVENT]: CustomEvent<DragEvent>;
48+
[SWC_DROPZONE_DROP_EVENT]: CustomEvent<DragEvent>;
49+
}
50+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
export * from './Dropzone.base.js';
13+
export * from './Dropzone.types.js';

2nd-gen/packages/core/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@
6363
"types": "./dist/components/divider/index.d.ts",
6464
"import": "./dist/components/divider/index.js"
6565
},
66+
"./components/dropzone": {
67+
"types": "./dist/components/dropzone/index.d.ts",
68+
"import": "./dist/components/dropzone/index.js"
69+
},
70+
"./components/dropzone/index.js": {
71+
"types": "./dist/components/dropzone/index.d.ts",
72+
"import": "./dist/components/dropzone/index.js"
73+
},
6674
"./components/icon": {
6775
"types": "./dist/components/icon/index.d.ts",
6876
"import": "./dist/components/icon/index.js"
@@ -233,6 +241,12 @@
233241
"components/divider/index.js": [
234242
"dist/components/divider/index.d.ts"
235243
],
244+
"components/dropzone": [
245+
"dist/components/dropzone/index.d.ts"
246+
],
247+
"components/dropzone/index.js": [
248+
"dist/components/dropzone/index.d.ts"
249+
],
236250
"components/icon": [
237251
"dist/components/icon/index.d.ts"
238252
],

0 commit comments

Comments
 (0)