Skip to content

Commit 99c137a

Browse files
authored
Error when dropping on a target not created with useDrop (#2900)
1 parent 65a6792 commit 99c137a

File tree

3 files changed

+40
-6
lines changed

3 files changed

+40
-6
lines changed

packages/@react-aria/dnd/src/DragManager.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ function endDragging() {
101101
}
102102
}
103103

104+
export function isValidDropTarget(element: Element): boolean {
105+
for (let target of dropTargets.keys()) {
106+
if (target.contains(element)) {
107+
return true;
108+
}
109+
}
110+
111+
return false;
112+
}
113+
104114
const CANCELED_EVENTS = [
105115
'pointerdown',
106116
'pointermove',
@@ -285,6 +295,11 @@ class DragSession {
285295
}
286296

287297
cancelEvent(e: Event) {
298+
// Allow focusin and focusout on the drag target so focus ring works properly.
299+
if ((e.type === 'focusin' || e.type === 'focusout') && e.target === this.dragTarget?.element) {
300+
return;
301+
}
302+
288303
// Allow default for events that might cancel a click event
289304
if (!CLICK_EVENTS.includes(e.type)) {
290305
e.preventDefault();
@@ -440,6 +455,12 @@ class DragSession {
440455
});
441456
}
442457

458+
// Blur and re-focus the drop target so that the focus ring appears.
459+
if (this.currentDropTarget) {
460+
this.currentDropTarget.element.blur();
461+
this.currentDropTarget.element.focus();
462+
}
463+
443464
this.setCurrentDropTarget(null);
444465
endDragging();
445466
}

packages/@react-aria/dnd/src/useDrag.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {DROP_EFFECT_TO_DROP_OPERATION, DROP_OPERATION, EFFECT_ALLOWED} from './c
1818
// @ts-ignore
1919
import intlMessages from '../intl/*.json';
2020
import ReactDOM from 'react-dom';
21-
import {useDescription} from '@react-aria/utils';
21+
import {useDescription, useGlobalListeners} from '@react-aria/utils';
2222
import {useDragModality} from './utils';
2323
import {useMessageFormatter} from '@react-aria/i18n';
2424
import {writeToDataTransfer} from './utils';
@@ -62,6 +62,7 @@ export function useDrag(options: DragOptions): DragResult {
6262
}).current;
6363
state.options = options;
6464
let [isDragging, setDragging] = useState(false);
65+
let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
6566

6667
let onDragStart = (e: DragEvent) => {
6768
let items = options.getItems();
@@ -114,6 +115,15 @@ export function useDrag(options: DragOptions): DragResult {
114115
}
115116
}
116117

118+
// Enforce that drops are handled by useDrop.
119+
addGlobalListener(window, 'drop', e => {
120+
if (!DragManager.isValidDropTarget(e.target as Element)) {
121+
e.preventDefault();
122+
e.stopPropagation();
123+
throw new Error('Drags initiated from the React Aria useDrag hook may only be dropped on a target created with useDrop. This ensures that a keyboard and screen reader accessible alternative is available.');
124+
}
125+
}, {capture: true, once: true});
126+
117127
if (typeof options.onDragStart === 'function') {
118128
options.onDragStart({
119129
type: 'dragstart',
@@ -160,6 +170,7 @@ export function useDrag(options: DragOptions): DragResult {
160170
}
161171

162172
setDragging(false);
173+
removeAllGlobalListeners();
163174
};
164175

165176
let onPress = (e: PressEvent) => {

packages/@react-aria/dnd/stories/dnd.stories.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -409,11 +409,13 @@ function DraggableCollectionItem({item, state, dragState, onCut}) {
409409
let cellNode = [...item.childNodes][0];
410410
let isSelected = state.selectionManager.isSelected(item.key);
411411

412-
let {rowProps} = useGridRow({node: item}, state, rowRef);
412+
let {rowProps} = useGridRow({
413+
node: item,
414+
shouldSelectOnPressUp: true
415+
}, state, rowRef);
413416
let {gridCellProps} = useGridCell({
414417
node: cellNode,
415-
focusMode: 'cell',
416-
shouldSelectOnPressUp: true
418+
focusMode: 'cell'
417419
}, state, cellRef);
418420

419421
let {dragProps, dragButtonProps} = useDraggableItem({key: item.key}, dragState);
@@ -431,10 +433,10 @@ function DraggableCollectionItem({item, state, dragState, onCut}) {
431433
let id = useId();
432434

433435
return (
434-
<div {...rowProps} ref={rowRef} aria-labelledby={id}>
436+
<div {...mergeProps(rowProps, dragProps)} ref={rowRef} aria-labelledby={id}>
435437
<FocusRing focusRingClass={classNames(dndStyles, 'focus-ring')}>
436438
<div
437-
{...mergeProps(gridCellProps, dragProps, clipboardProps)}
439+
{...mergeProps(gridCellProps, clipboardProps)}
438440
aria-labelledby={id}
439441
ref={cellRef}
440442
className={classNames(dndStyles, 'draggable', {

0 commit comments

Comments
 (0)