Skip to content

Commit fc00f83

Browse files
authored
rac dropzone (#4451)
* initialize dropzone * added some comments for clarification * edit spacing * added additonal props and passed ref * added dropzone to storybook * update index.ts * fixed linting * fix linting, added actions to dropzone story * fixed focus * allow copy/paste, update render props, styling * can copy, added stories, dropzone with input * fix linting * update focus, added css * update stories, add data attribute * add isVirtualDragging to DropZone * single hidden button, added virtual drag * add tests, update dropzone * clean up, add story * fix tests * fix * minor fix * minor minor fix * add files to react-aria/utils, update css, mobile dropzone * remove console.log * update utils, add press events to dropzone * update aria-label, add focusProps to wrapper div, usePress updates * create FileTrigger * cleanup/add stories * add prop to filetrigger, update comments, change stories * change comments * update useDrop hook to accomodate dropButton * drop target fixes * changes to dnd with button ref, press responder, remove onInputChange * fix prop names, update dnd * add aria label to DropZone, update onChange * fix onChange in FileTrigger
1 parent 8ea4489 commit fc00f83

File tree

11 files changed

+917
-15
lines changed

11 files changed

+917
-15
lines changed

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,6 @@ class DragSession {
481481
}
482482
}
483483

484-
485484
if (item !== this.currentDropItem) {
486485
if (item && typeof this.currentDropTarget.onDropTargetEnter === 'function') {
487486
this.currentDropTarget.onDropTargetEnter(item?.target);
@@ -490,7 +489,7 @@ class DragSession {
490489
item?.element.focus();
491490
this.currentDropItem = item;
492491

493-
// Annouce first drop target after drag start announcement finishes.
492+
// Announce first drop target after drag start announcement finishes.
494493
// Otherwise, it will never get announced because drag start announcement is assertive.
495494
if (!this.initialFocused) {
496495
announce(item?.element.getAttribute('aria-label'), 'polite');

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,10 @@ export function useDrag(options: DragOptions): DragResult {
149149

150150
// Enforce that drops are handled by useDrop.
151151
addGlobalListener(window, 'drop', e => {
152-
if (!DragManager.isValidDropTarget(e.target as Element)) {
153-
e.preventDefault();
154-
e.stopPropagation();
155-
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.');
156-
}
157-
}, {capture: true, once: true});
158-
152+
e.preventDefault();
153+
e.stopPropagation();
154+
console.warn('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.');
155+
}, {once: true});
159156
state.x = e.clientX;
160157
state.y = e.clientY;
161158

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13+
import {AriaButtonProps} from '@react-types/button';
1314
import {DragEvent, HTMLAttributes, RefObject, useRef, useState} from 'react';
1415
import * as DragManager from './DragManager';
1516
import {DragTypes, globalAllowedDropOperations, globalDndState, readFromDataTransfer, setGlobalDnDState, setGlobalDropEffect} from './utils';
@@ -41,14 +42,22 @@ export interface DropOptions {
4142
/** Handler that is called when a valid drag exits the drop target. */
4243
onDropExit?: (e: DropExitEvent) => void,
4344
/** Handler that is called when a valid drag is dropped on the drop target. */
44-
onDrop?: (e: DropEvent) => void
45+
onDrop?: (e: DropEvent) => void,
46+
/**
47+
* Whether the item has an explicit focusable drop affordance to initiate accessible drag and drop mode.
48+
* If true, the dropProps will omit these event handlers, and they will be applied to dropButtonProps instead.
49+
*/
50+
hasDropButton?: boolean
4551
}
4652

4753
export interface DropResult {
4854
/** Props for the droppable element. */
4955
dropProps: HTMLAttributes<HTMLElement>,
5056
/** Whether the drop target is currently focused or hovered. */
51-
isDropTarget: boolean
57+
isDropTarget: boolean,
58+
/** Props for the explicit drop button affordance, if any. */
59+
dropButtonProps?: AriaButtonProps
60+
5261
}
5362

5463
const DROP_ACTIVATE_TIMEOUT = 800;
@@ -58,6 +67,7 @@ const DROP_ACTIVATE_TIMEOUT = 800;
5867
* based drag and drop, in addition to full parity for keyboard and screen reader users.
5968
*/
6069
export function useDrop(options: DropOptions): DropResult {
70+
let {hasDropButton} = options;
6171
let [isDropTarget, setDropTarget] = useState(false);
6272
let state = useRef({
6373
x: 0,
@@ -317,15 +327,16 @@ export function useDrop(options: DropOptions): DropResult {
317327
}), [ref, getDropOperationKeyboard, onDropEnter, onDropExit, onKeyboardDrop, onDropActivate]);
318328

319329
let {dropProps} = useVirtualDrop();
320-
330+
321331
return {
322332
dropProps: {
323-
...dropProps,
333+
...(!hasDropButton && dropProps),
324334
onDragEnter,
325335
onDragOver,
326336
onDragLeave,
327337
onDrop
328338
},
339+
dropButtonProps: {...(hasDropButton && dropProps)},
329340
isDropTarget
330341
};
331342
}

packages/react-aria-components/example/index.css

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,34 @@ html {
175175
}
176176
}
177177
}
178+
179+
.dropzone {
180+
border: 2px solid white;
181+
margin: 20px;
182+
padding: 20px;
183+
outline: none;
184+
185+
&[data-focus-visible][data-focused] {
186+
border: 2px dashed blue;
187+
}
188+
189+
&[data-drop-target]{
190+
border: 2px dashed orange;
191+
}
192+
}
193+
194+
.draggable {
195+
outline: none;
196+
197+
&.focus-ring{
198+
border: 2px solid blue;
199+
}
200+
}
201+
202+
.copyable {
203+
outline: none;
204+
205+
&.focus-ring{
206+
border: 2px solid blue;
207+
}
208+
}

packages/react-aria-components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"dependencies": {
2525
"@internationalized/date": "^3.2.0",
2626
"@react-aria/focus": "^3.12.1",
27+
"@react-aria/interactions": "^3.15.1",
2728
"@react-aria/utils": "^3.17.0",
2829
"@react-stately/table": "^3.9.1",
2930
"@react-types/calendar": "^3.2.1",
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2023 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 {AriaLabelingProps} from '@react-types/shared';
14+
import {ContextValue, Provider, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
15+
import {DropOptions, mergeProps, useClipboard, useDrop, useFocusRing, useHover, useId, VisuallyHidden} from 'react-aria';
16+
import {FileTriggerContext} from './FileTrigger';
17+
import {filterDOMProps, useLabels} from '@react-aria/utils';
18+
import React, {createContext, ForwardedRef, forwardRef, useRef} from 'react';
19+
import {TextContext} from './Text';
20+
21+
export interface DropZoneRenderProps {
22+
/**
23+
* Whether the dropzone is currently hovered with a mouse.
24+
* @selector [data-hovered]
25+
*/
26+
isHovered: boolean,
27+
/**
28+
* Whether the dropzone is focused, either via a mouse or keyboard.
29+
* @selector [data-focused]
30+
*/
31+
isFocused: boolean,
32+
/**
33+
* Whether the dropzone is keyboard focused.
34+
* @selector [data-focus-visible]
35+
*/
36+
isFocusVisible: boolean,
37+
/**
38+
* Whether the dropzone is the drop target.
39+
* @selector [data-drop-target]
40+
*/
41+
isDropTarget: boolean
42+
}
43+
// note: possibly add isDisabled prop in the future
44+
export interface DropZoneProps extends Omit<DropOptions, 'getDropOperationForPoint'>, RenderProps<DropZoneRenderProps>, SlotProps, AriaLabelingProps {}
45+
46+
export const DropZoneContext = createContext<ContextValue<DropZoneProps, HTMLDivElement>>(null);
47+
48+
function DropZone(props: DropZoneProps, ref: ForwardedRef<HTMLDivElement>) {
49+
[props, ref] = useContextProps(props, ref, DropZoneContext);
50+
let buttonRef = useRef<HTMLButtonElement>(null);
51+
let {dropProps, dropButtonProps, isDropTarget} = useDrop({...props, ref: buttonRef, hasDropButton: true});
52+
let {hoverProps, isHovered} = useHover({});
53+
let {focusProps, isFocused, isFocusVisible} = useFocusRing();
54+
let [fileTriggerRef] = useSlot();
55+
56+
let textId = useId();
57+
let labelProps = useLabels({'aria-labelledby': textId});
58+
59+
let {clipboardProps} = useClipboard({
60+
onPaste: (items) => props.onDrop?.({
61+
type: 'drop',
62+
items,
63+
x: 0,
64+
y: 0,
65+
dropOperation: 'copy'
66+
})
67+
});
68+
69+
let renderProps = useRenderProps({
70+
...props,
71+
values: {isHovered, isFocused, isFocusVisible, isDropTarget},
72+
defaultClassName: 'react-aria-DropZone'
73+
});
74+
let DOMProps = filterDOMProps(props);
75+
delete DOMProps.id;
76+
77+
return (
78+
<Provider
79+
values={[
80+
[FileTriggerContext, {ref: fileTriggerRef}],
81+
[TextContext, {id: textId, slot: 'heading'}]
82+
]}>
83+
{/* eslint-disable-next-line */}
84+
<div
85+
{...mergeProps(dropProps, hoverProps, DOMProps)}
86+
{...renderProps}
87+
slot={props.slot}
88+
onClick={() => buttonRef.current?.focus()}
89+
data-hovered={isHovered || undefined}
90+
data-focused={isFocused || undefined}
91+
data-focus-visible={isFocusVisible || undefined}
92+
data-drop-target={isDropTarget || undefined} >
93+
<VisuallyHidden>
94+
<button
95+
{...mergeProps(dropButtonProps, focusProps, clipboardProps, labelProps)}
96+
aria-label="DropZone" // will need to update with string formatter
97+
ref={buttonRef} />
98+
</VisuallyHidden>
99+
{renderProps.children}
100+
</div>
101+
</Provider>
102+
);
103+
}
104+
105+
/**
106+
* A dropzone is an area into which one or multiple objects can be dragged and dropped.
107+
*/
108+
const _DropZone = forwardRef(DropZone);
109+
export {_DropZone as DropZone};
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2023 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 {AriaLabelingProps} from '@react-types/shared';
14+
import {ContextValue, DOMProps, SlotProps, useContextProps} from './utils';
15+
import {filterDOMProps} from '@react-aria/utils';
16+
import {Input} from './Input';
17+
import {PressResponder} from '@react-aria/interactions';
18+
import React, {createContext, ForwardedRef, forwardRef, useRef} from 'react';
19+
import {VisuallyHidden} from 'react-aria';
20+
21+
export interface FileTriggerProps extends SlotProps, DOMProps, AriaLabelingProps {
22+
/**
23+
* Specifies what mime type of files are allowed.
24+
*/
25+
acceptedFileTypes?: Array<string>,
26+
/**
27+
* Whether multiple files can be selected.
28+
*/
29+
allowsMultiple?: boolean,
30+
/**
31+
* Specifies the use of a media capture mechanism to capture the media on the spot.
32+
*/
33+
defaultCamera?: 'user' | 'environment',
34+
/**
35+
* Handler when a user selects a file.
36+
*/
37+
onChange?: (files: FileList | null) => void
38+
}
39+
40+
export const FileTriggerContext = createContext<ContextValue<FileTriggerProps, HTMLDivElement>>(null);
41+
42+
function FileTrigger(props: FileTriggerProps, ref: ForwardedRef<HTMLDivElement>) {
43+
[props, ref] = useContextProps(props, ref, FileTriggerContext);
44+
let {onChange, acceptedFileTypes, className, allowsMultiple, defaultCamera, children, ...otherProps} = props;
45+
let inputRef = useRef<HTMLInputElement>(null);
46+
47+
return (
48+
<div
49+
className={className || 'react-aria-FileTrigger'}
50+
{...filterDOMProps(otherProps)}
51+
ref={ref}
52+
slot={props.slot}>
53+
<PressResponder onPress={() => inputRef.current?.click()}>
54+
{children}
55+
</PressResponder>
56+
<VisuallyHidden>
57+
<Input
58+
type="file"
59+
ref={inputRef}
60+
accept={acceptedFileTypes?.toString()}
61+
onChange={(e) => onChange?.(e.target.files)}
62+
capture={defaultCamera}
63+
multiple={allowsMultiple} />
64+
</VisuallyHidden>
65+
</div>
66+
);
67+
}
68+
69+
/**
70+
* A FileTrigger allows a user to access the file system with either a Button or Link.
71+
*/
72+
const _FileTrigger = forwardRef(FileTrigger);
73+
export {_FileTrigger as FileTrigger};

packages/react-aria-components/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export {ComboBox, ComboBoxContext} from './ComboBox';
1818
export {DateField, DateInput, DateSegment, TimeField, DateFieldContext, TimeFieldContext} from './DateField';
1919
export {DatePicker, DateRangePicker, DatePickerContext, DateRangePickerContext} from './DatePicker';
2020
export {DialogTrigger, Dialog, DialogContext} from './Dialog';
21+
export {DropZone, DropZoneContext} from './DropZone';
22+
export {FileTrigger, FileTriggerContext} from './FileTrigger';
2123
export {GridList, GridListContext} from './GridList';
2224
export {Group, GroupContext} from './Group';
2325
export {Header} from './Header';

0 commit comments

Comments
 (0)