Skip to content

Commit e54e3c5

Browse files
ryo-manbaLFDanLureidbarbersnowystinger
authored
Add conditional DnD support (#5641)
* Add conditional DnD support to Table component * fix: Correct the logic for disabling drag and drop * refactor * fix: Custom hooks order with React rules of hooks * fix: function naming * fix: Check for the existence of dragAndDropHooks * fix: lint * fix: format * Refactor drag disablement logic in draggable item and collection state hooks * modified the condition of disabled flag * Add test for DraggableTable with selection * Add isDisabled prop support to useDroppableCollectionState * Refactor draggable list naming for clarity * fix lint * fix lint * Revert "Refactor draggable list naming for clarity" This reverts commit 9433c63 * Refactor draggable list naming for clarity * Update packages/react-aria-components/src/Table.tsx Co-authored-by: Daniel Lu <danilu@adobe.com> * Clean up isDisabled prop handling * fix lint --------- Co-authored-by: Daniel Lu <danilu@adobe.com> Co-authored-by: Daniel Lu <dl1644@gmail.com> Co-authored-by: Reid Barber <reid@reidbarber.com> Co-authored-by: Robert Snow <rsnow@adobe.com>
1 parent 5d86732 commit e54e3c5

File tree

9 files changed

+298
-27
lines changed

9 files changed

+298
-27
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const MESSAGES = {
6565
*/
6666
export function useDraggableItem(props: DraggableItemProps, state: DraggableCollectionState): DraggableItemResult {
6767
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/dnd');
68-
let isDisabled = state.selectionManager.isDisabled(props.key);
68+
let isDisabled = state.isDisabled || state.selectionManager.isDisabled(props.key);
6969
let {dragProps, dragButtonProps} = useDrag({
7070
getItems() {
7171
return state.getItems(props.key);
@@ -92,8 +92,8 @@ export function useDraggableItem(props: DraggableItemProps, state: DraggableColl
9292
let item = state.collection.getItem(props.key);
9393
let numKeysForDrag = state.getKeysForDrag(props.key).size;
9494
let isSelected = numKeysForDrag > 1 && state.selectionManager.isSelected(props.key);
95-
let dragButtonLabel: string;
96-
let description: string;
95+
let dragButtonLabel: string | undefined;
96+
let description: string | undefined;
9797

9898
// Override description to include selected item count.
9999
let modality = useDragModality();
@@ -136,13 +136,13 @@ export function useDraggableItem(props: DraggableItemProps, state: DraggableColl
136136
// Require Alt key if there is a conflicting action.
137137
dragProps.onKeyDownCapture = e => {
138138
if (e.altKey) {
139-
onKeyDownCapture(e);
139+
onKeyDownCapture?.(e);
140140
}
141141
};
142142

143143
dragProps.onKeyUpCapture = e => {
144144
if (e.altKey) {
145-
onKeyUpCapture(e);
145+
onKeyUpCapture?.(e);
146146
}
147147
};
148148
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export function DroppableListBoxExample(props) {
7676
};
7777

7878
return (
79-
<DroppableListBox items={list.items} onDrop={onDrop} ref={ref}>
79+
<DroppableListBox items={list.items} onDrop={onDrop} ref={ref} isDisabled={props.isDisabled}>
8080
{item => (
8181
<Item textValue={item.text}>
8282
{item.type === 'folder' && <Folder size="S" />}
@@ -104,6 +104,7 @@ export const DroppableListBox = React.forwardRef(function (props: any, ref) {
104104
}));
105105

106106
let dropState = useDroppableCollectionState({
107+
isDisabled: props.isDisabled,
107108
collection: state.collection,
108109
selectionManager: state.selectionManager,
109110
getDropOperation: (target, _, allowedOperations) => {

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ function DialogButton({children}) {
342342
);
343343
}
344344

345-
function DraggableCollectionExample() {
345+
function DraggableCollectionExample(props) {
346346
let list = useListData({
347347
initialItems: [
348348
{id: 'foo', type: 'folder', text: 'Foo'},
@@ -362,7 +362,7 @@ function DraggableCollectionExample() {
362362
};
363363

364364
return (
365-
<DraggableCollection items={list.items} selectedKeys={list.selectedKeys} onSelectionChange={list.setSelectedKeys} onDragEnd={onDragEnd} onCut={onCut}>
365+
<DraggableCollection items={list.items} selectedKeys={list.selectedKeys} onSelectionChange={list.setSelectedKeys} onDragEnd={onDragEnd} onCut={onCut} isDisabled={props.isDisabled}>
366366
{item => (
367367
<Item textValue={item.text}>
368368
{item.type === 'folder' && <Folder size="S" />}
@@ -374,6 +374,7 @@ function DraggableCollectionExample() {
374374
}
375375

376376
function DraggableCollection(props) {
377+
let {isDisabled} = props;
377378
let ref = React.useRef<HTMLDivElement>(null);
378379
let state = useListState(props);
379380
let gridState = useGridState({
@@ -400,6 +401,7 @@ function DraggableCollection(props) {
400401

401402
let preview = useRef(null);
402403
let dragState = useDraggableCollectionState({
404+
isDisabled,
403405
collection: gridState.collection,
404406
selectionManager: gridState.selectionManager,
405407
getItems(keys) {
@@ -517,3 +519,35 @@ function DraggableCollectionItem({item, state, dragState, onCut}) {
517519
</div>
518520
);
519521
}
522+
523+
export const DraggableEnabledDisabledControl = {
524+
render: (args) => (
525+
<Flex direction="row" gap="size-200" alignItems="center" wrap>
526+
<DraggableCollectionExample {...args} />
527+
<DroppableListBoxExample />
528+
</Flex>
529+
),
530+
name: 'Draggable Enable/Disable control',
531+
argTypes: {
532+
isDisabled: {
533+
control: 'boolean',
534+
defaultValue: true
535+
}
536+
}
537+
};
538+
539+
export const DroppableEnabledDisabledControl = {
540+
render: (args) => (
541+
<Flex direction="row" gap="size-200" alignItems="center" wrap>
542+
<DraggableCollectionExample />
543+
<DroppableListBoxExample {...args} />
544+
</Flex>
545+
),
546+
name: 'Droppable Enable/Disable control',
547+
argTypes: {
548+
isDisabled: {
549+
control: 'boolean',
550+
defaultValue: true
551+
}
552+
}
553+
};

packages/@react-stately/dnd/src/useDraggableCollectionState.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export interface DraggableCollectionStateOptions extends DraggableCollectionProp
1818
/** A collection of items. */
1919
collection: Collection<Node<unknown>>,
2020
/** An interface for reading and updating multiple selection state. */
21-
selectionManager: MultipleSelectionManager
21+
selectionManager: MultipleSelectionManager,
22+
/** Whether the drag events should be disabled. */
23+
isDisabled?: boolean
2224
}
2325

2426
export interface DraggableCollectionState {
@@ -30,6 +32,8 @@ export interface DraggableCollectionState {
3032
draggedKey: Key | null,
3133
/** The keys of the items that are currently being dragged. */
3234
draggingKeys: Set<Key>,
35+
/** Whether drag events are disabled. */
36+
isDisabled?: boolean,
3337
/** Returns whether the given key is currently being dragged. */
3438
isDragging(key: Key): boolean,
3539
/** Returns the keys of the items that will be dragged with the given key (e.g. selected items). */
@@ -54,6 +58,7 @@ export interface DraggableCollectionState {
5458
export function useDraggableCollectionState(props: DraggableCollectionStateOptions): DraggableCollectionState {
5559
let {
5660
getItems,
61+
isDisabled,
5762
collection,
5863
selectionManager,
5964
onDragStart,
@@ -95,6 +100,7 @@ export function useDraggableCollectionState(props: DraggableCollectionStateOptio
95100
getItems(key) {
96101
return getItems(getKeys(key));
97102
},
103+
isDisabled,
98104
preview,
99105
getAllowedDropOperations,
100106
startDrag(key, event) {

packages/@react-stately/dnd/src/useDroppableCollectionState.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ export interface DroppableCollectionStateOptions extends Omit<DroppableCollectio
2626
/** A collection of items. */
2727
collection: Collection<Node<unknown>>,
2828
/** An interface for reading and updating multiple selection state. */
29-
selectionManager: MultipleSelectionManager
29+
selectionManager: MultipleSelectionManager,
30+
/** Whether the drop events should be disabled. */
31+
isDisabled?: boolean
3032
}
3133

3234
export interface DroppableCollectionState {
@@ -36,6 +38,8 @@ export interface DroppableCollectionState {
3638
selectionManager: MultipleSelectionManager,
3739
/** The current drop target. */
3840
target: DropTarget | null,
41+
/** Whether drop events are disabled. */
42+
isDisabled?: boolean,
3943
/** Sets the current drop target. */
4044
setTarget(target: DropTarget): void,
4145
/** Returns whether the given target is equivalent to the current drop target. */
@@ -50,6 +54,7 @@ export interface DroppableCollectionState {
5054
export function useDroppableCollectionState(props: DroppableCollectionStateOptions): DroppableCollectionState {
5155
let {
5256
acceptedDragTypes = 'all',
57+
isDisabled,
5358
onInsert,
5459
onRootDrop,
5560
onItemDrop,
@@ -75,6 +80,10 @@ export function useDroppableCollectionState(props: DroppableCollectionStateOptio
7580
};
7681

7782
let defaultGetDropOperation = useCallback((e: DropOperationEvent) => {
83+
if (isDisabled) {
84+
return 'cancel';
85+
}
86+
7887
let {
7988
target,
8089
types,
@@ -101,11 +110,12 @@ export function useDroppableCollectionState(props: DroppableCollectionStateOptio
101110
}
102111

103112
return 'cancel';
104-
}, [acceptedDragTypes, getDropOperation, onInsert, onRootDrop, onItemDrop, shouldAcceptItemDrop, onReorder, onDrop]);
113+
}, [isDisabled, acceptedDragTypes, getDropOperation, onInsert, onRootDrop, onItemDrop, shouldAcceptItemDrop, onReorder, onDrop]);
105114

106115
return {
107116
collection,
108117
selectionManager,
118+
isDisabled,
109119
target,
110120
setTarget(newTarget) {
111121
if (this.isDropTarget(newTarget)) {

packages/react-aria-components/src/Table.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -306,18 +306,18 @@ function Table(props: TableProps, ref: ForwardedRef<HTMLTableElement>) {
306306

307307
let {dragAndDropHooks} = props;
308308
let selectionManager = state.selectionManager;
309-
let isListDraggable = !!dragAndDropHooks?.useDraggableCollectionState;
310-
let isListDroppable = !!dragAndDropHooks?.useDroppableCollectionState;
311-
let dragHooksProvided = useRef(isListDraggable);
312-
let dropHooksProvided = useRef(isListDroppable);
309+
let hasDragHooks = !!dragAndDropHooks?.useDraggableCollectionState;
310+
let hasDropHooks = !!dragAndDropHooks?.useDroppableCollectionState;
311+
let dragHooksProvided = useRef(hasDragHooks);
312+
let dropHooksProvided = useRef(hasDropHooks);
313313
useEffect(() => {
314-
if (dragHooksProvided.current !== isListDraggable) {
314+
if (dragHooksProvided.current !== hasDragHooks) {
315315
console.warn('Drag hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
316316
}
317-
if (dropHooksProvided.current !== isListDroppable) {
317+
if (dropHooksProvided.current !== hasDropHooks) {
318318
console.warn('Drop hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
319319
}
320-
}, [isListDraggable, isListDroppable]);
320+
}, [hasDragHooks, hasDropHooks]);
321321

322322
let dragState: DraggableCollectionState | undefined = undefined;
323323
let dropState: DroppableCollectionState | undefined = undefined;
@@ -326,7 +326,7 @@ function Table(props: TableProps, ref: ForwardedRef<HTMLTableElement>) {
326326
let dragPreview: JSX.Element | null = null;
327327
let preview = useRef<DragPreviewRenderer>(null);
328328

329-
if (isListDraggable && dragAndDropHooks) {
329+
if (hasDragHooks && dragAndDropHooks) {
330330
dragState = dragAndDropHooks.useDraggableCollectionState!({
331331
collection,
332332
selectionManager,
@@ -340,7 +340,7 @@ function Table(props: TableProps, ref: ForwardedRef<HTMLTableElement>) {
340340
: null;
341341
}
342342

343-
if (isListDroppable && dragAndDropHooks) {
343+
if (hasDropHooks && dragAndDropHooks) {
344344
dropState = dragAndDropHooks.useDroppableCollectionState!({
345345
collection,
346346
selectionManager
@@ -374,13 +374,16 @@ function Table(props: TableProps, ref: ForwardedRef<HTMLTableElement>) {
374374
}
375375
});
376376

377+
let isListDraggable = !!(hasDragHooks && !dragState?.isDisabled);
378+
let isListDroppable = !!(hasDropHooks && !dropState?.isDisabled);
379+
377380
let {selectionBehavior, selectionMode, disallowEmptySelection} = state.selectionManager;
378381
let ctx = useMemo(() => ({
379382
selectionBehavior: selectionMode === 'none' ? null : selectionBehavior,
380383
selectionMode,
381384
disallowEmptySelection,
382-
allowsDragging: isListDraggable
383-
}), [selectionBehavior, selectionMode, disallowEmptySelection, isListDraggable]);
385+
allowsDragging: hasDragHooks
386+
}), [selectionBehavior, selectionMode, disallowEmptySelection, hasDragHooks]);
384387

385388
let style = renderProps.style;
386389
let tableContainerContext = useContext(ResizableTableContainerContext);

packages/react-aria-components/src/useDragAndDrop.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ interface DropHooks {
6363
ListDropTargetDelegate: typeof ListDropTargetDelegate
6464
}
6565

66-
export type DragAndDropHooks = DragHooks & DropHooks;
66+
export type DragAndDropHooks = DragHooks & DropHooks
6767

6868
export interface DragAndDrop {
6969
/** Drag and drop hooks for the collection element. */
@@ -88,7 +88,9 @@ export interface DragAndDropOptions extends Omit<DraggableCollectionProps, 'prev
8888
*/
8989
renderDropIndicator?: (target: DropTarget) => JSX.Element,
9090
/** A custom delegate object that provides drop targets for pointer coordinates within the collection. */
91-
dropTargetDelegate?: DropTargetDelegate
91+
dropTargetDelegate?: DropTargetDelegate,
92+
/** Whether the drag and drop events should be disabled. */
93+
isDisabled?: boolean
9294
}
9395

9496
/**
@@ -106,12 +108,12 @@ export function useDragAndDrop(options: DragAndDropOptions): DragAndDrop {
106108
renderDragPreview,
107109
renderDropIndicator,
108110
dropTargetDelegate
109-
} = options;
111+
} = options;
110112

111113
let isDraggable = !!getItems;
112114
let isDroppable = !!(onDrop || onInsert || onItemDrop || onReorder || onRootDrop);
113115

114-
let hooks = {} as DragHooks & DropHooks;
116+
let hooks = {} as DragAndDropHooks;
115117
if (isDraggable) {
116118
hooks.useDraggableCollectionState = function useDraggableCollectionStateOverride(props: DraggableCollectionStateOpts) {
117119
return useDraggableCollectionState({...props, ...options} as DraggableCollectionStateOptions);

0 commit comments

Comments
 (0)