Skip to content

Commit 66d65a9

Browse files
feat(dnd): pass keys and draggedKey as arguments to renderDropIndicator (#8459)
* pass keys and draggedKey as arguments to renderDropIndicator * fix styles on story * make keys optional --------- Co-authored-by: Robert Snow <[email protected]>
1 parent e8dcc2b commit 66d65a9

File tree

12 files changed

+197
-23
lines changed

12 files changed

+197
-23
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export interface CollectionBranchProps {
114114
/** The parent node of the items to render. */
115115
parent: Node<unknown>,
116116
/** A function that renders a drop indicator between items. */
117-
renderDropIndicator?: (target: ItemDropTarget) => ReactNode
117+
renderDropIndicator?: (target: ItemDropTarget, keys?: Set<Key>, draggedKey?: Key) => ReactNode
118118
}
119119

120120
export interface CollectionRootProps extends HTMLAttributes<HTMLElement> {
@@ -125,7 +125,7 @@ export interface CollectionRootProps extends HTMLAttributes<HTMLElement> {
125125
/** A ref to the scroll container for the collection. */
126126
scrollRef?: RefObject<HTMLElement | null>,
127127
/** A function that renders a drop indicator between items. */
128-
renderDropIndicator?: (target: ItemDropTarget) => ReactNode
128+
renderDropIndicator?: (target: ItemDropTarget, keys?: Set<Key>, draggedKey?: Key) => ReactNode
129129
}
130130

131131
export interface CollectionRenderer {
@@ -153,7 +153,7 @@ export const DefaultCollectionRenderer: CollectionRenderer = {
153153
function useCollectionRender(
154154
collection: ICollection<Node<unknown>>,
155155
parent: Node<unknown> | null,
156-
renderDropIndicator?: (target: ItemDropTarget) => ReactNode
156+
renderDropIndicator?: (target: ItemDropTarget, keys?: Set<Key>, draggedKey?: Key) => ReactNode
157157
) {
158158
return useCachedChildren({
159159
items: parent ? collection.getChildren!(parent.key) : collection,
@@ -175,7 +175,7 @@ function useCollectionRender(
175175
});
176176
}
177177

178-
export function renderAfterDropIndicators(collection: ICollection<Node<unknown>>, node: Node<unknown>, renderDropIndicator: (target: ItemDropTarget) => ReactNode): ReactNode {
178+
export function renderAfterDropIndicators(collection: ICollection<Node<unknown>>, node: Node<unknown>, renderDropIndicator: (target: ItemDropTarget, keys?: Set<Key>, draggedKey?: Key) => ReactNode): ReactNode {
179179
let key = node.key;
180180
let keyAfter = collection.getKeyAfter(key);
181181
let nextItemInFlattenedCollection = keyAfter != null ? collection.getItem(keyAfter) : null;

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,23 @@ export const DropIndicator = forwardRef(function DropIndicator(props: DropIndica
4747

4848
type RenderDropIndicatorRetValue = ((target: ItemDropTarget) => ReactNode | undefined) | undefined
4949

50-
export function useRenderDropIndicator(dragAndDropHooks?: DragAndDropHooks, dropState?: DroppableCollectionState): RenderDropIndicatorRetValue {
50+
export function useRenderDropIndicator(dragAndDropHooks?: DragAndDropHooks, dropState?: DroppableCollectionState, dragState?: DraggableCollectionState): RenderDropIndicatorRetValue {
5151
let renderDropIndicator = dragAndDropHooks?.renderDropIndicator;
5252
let isVirtualDragging = dragAndDropHooks?.isVirtualDragging?.();
5353
let fn = useCallback((target: ItemDropTarget) => {
5454
// Only show drop indicators when virtual dragging or this is the current drop target.
5555
if (isVirtualDragging || dropState?.isDropTarget(target)) {
56-
return renderDropIndicator ? renderDropIndicator(target) : <DropIndicator target={target} />;
56+
if (renderDropIndicator) {
57+
let keys = dragState?.draggingKeys ?? undefined;
58+
let draggedKey = dragState?.draggedKey ?? undefined;
59+
return renderDropIndicator(target, keys, draggedKey);
60+
} else {
61+
return <DropIndicator target={target} />;
62+
}
5763
}
5864
// We invalidate whenever the target changes.
5965
// eslint-disable-next-line react-hooks/exhaustive-deps
60-
}, [dropState?.target, isVirtualDragging, renderDropIndicator]);
66+
}, [dropState?.target, isVirtualDragging, renderDropIndicator, dragState?.draggingKeys, dragState?.draggedKey]);
6167
return dragAndDropHooks?.useDropIndicator ? fn : undefined;
6268
}
6369

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ function GridListInner<T extends object>({props, collection, gridListRef: ref}:
253253
collection={collection}
254254
scrollRef={ref}
255255
persistedKeys={useDndPersistedKeys(selectionManager, dragAndDropHooks, dropState)}
256-
renderDropIndicator={useRenderDropIndicator(dragAndDropHooks, dropState)} />
256+
renderDropIndicator={useRenderDropIndicator(dragAndDropHooks, dropState, dragState)} />
257257
</Provider>
258258
{emptyState}
259259
{dragPreview}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ function ListBoxInner<T extends object>({state: inputState, props, listBoxRef}:
261261
collection={collection}
262262
scrollRef={listBoxRef}
263263
persistedKeys={useDndPersistedKeys(selectionManager, dragAndDropHooks, dropState)}
264-
renderDropIndicator={useRenderDropIndicator(dragAndDropHooks, dropState)} />
264+
renderDropIndicator={useRenderDropIndicator(dragAndDropHooks, dropState, dragState)} />
265265
</Provider>
266266
{emptyState}
267267
{dragPreview}
@@ -274,7 +274,7 @@ export interface ListBoxSectionProps<T> extends SectionProps<T> {}
274274

275275
function ListBoxSectionInner<T extends object>(props: ListBoxSectionProps<T>, ref: ForwardedRef<HTMLElement>, section: Node<T>, className = 'react-aria-ListBoxSection') {
276276
let state = useContext(ListStateContext)!;
277-
let {dragAndDropHooks, dropState} = useContext(DragAndDropContext)!;
277+
let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext)!;
278278
let {CollectionBranch} = useContext(CollectionRendererContext);
279279
let [headingRef, heading] = useSlot();
280280
let {headingProps, groupProps} = useListBoxSection({
@@ -298,7 +298,7 @@ function ListBoxSectionInner<T extends object>(props: ListBoxSectionProps<T>, re
298298
<CollectionBranch
299299
collection={state.collection}
300300
parent={section}
301-
renderDropIndicator={useRenderDropIndicator(dragAndDropHooks, dropState)} />
301+
renderDropIndicator={useRenderDropIndicator(dragAndDropHooks, dropState, dragState)} />
302302
</HeaderContext.Provider>
303303
</section>
304304
);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,7 @@ export const TableBody = /*#__PURE__*/ createBranchComponent('tablebody', <T ext
927927
let {isVirtualized} = useContext(CollectionRendererContext);
928928
let collection = state.collection;
929929
let {CollectionBranch} = useContext(CollectionRendererContext);
930-
let {dragAndDropHooks, dropState} = useContext(DragAndDropContext);
930+
let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext);
931931
let isDroppable = !!dragAndDropHooks?.useDroppableCollectionState && !dropState?.isDisabled;
932932
let isRootDropTarget = isDroppable && !!dropState && (dropState.isDropTarget({type: 'root'}) ?? false);
933933

@@ -985,7 +985,7 @@ export const TableBody = /*#__PURE__*/ createBranchComponent('tablebody', <T ext
985985
<CollectionBranch
986986
collection={collection}
987987
parent={collection.body}
988-
renderDropIndicator={useRenderDropIndicator(dragAndDropHooks, dropState)} />
988+
renderDropIndicator={useRenderDropIndicator(dragAndDropHooks, dropState, dragState)} />
989989
{emptyState}
990990
</TBody>
991991
);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ function TreeInner<T extends object>({props, collection, treeRef: ref}: TreeInne
405405
collection={state.collection}
406406
persistedKeys={useDndPersistedKeys(state.selectionManager, dragAndDropHooks, dropState)}
407407
scrollRef={ref}
408-
renderDropIndicator={useRenderDropIndicator(dragAndDropHooks, dropState)} />
408+
renderDropIndicator={useRenderDropIndicator(dragAndDropHooks, dropState, dragState)} />
409409
</Provider>
410410
{emptyState}
411411
</div>

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {CollectionBranchProps, CollectionRenderer, CollectionRendererContext, CollectionRootProps, renderAfterDropIndicators} from './Collection';
14-
import {DropTargetDelegate, ItemDropTarget, Node} from '@react-types/shared';
14+
import {DropTargetDelegate, ItemDropTarget, Key, Node} from '@react-types/shared';
1515
import {Layout, ReusableView, useVirtualizerState, VirtualizerState} from '@react-stately/virtualizer';
1616
import React, {createContext, JSX, ReactNode, useContext, useMemo} from 'react';
1717
import {useScrollView, VirtualizerItem} from '@react-aria/virtualizer';
@@ -117,14 +117,14 @@ function CollectionBranch({parent, renderDropIndicator}: CollectionBranchProps)
117117
return renderChildren(parentView, Array.from(parentView.children), renderDropIndicator);
118118
}
119119

120-
function renderChildren(parent: View | null, children: View[], renderDropIndicator?: (target: ItemDropTarget) => ReactNode) {
120+
function renderChildren(parent: View | null, children: View[], renderDropIndicator?: (target: ItemDropTarget, keys?: Set<Key>, draggedKey?: Key) => ReactNode) {
121121
return children.map(view => renderWrapper(parent, view, renderDropIndicator));
122122
}
123123

124124
function renderWrapper(
125125
parent: View | null,
126126
reusableView: View,
127-
renderDropIndicator?: (target: ItemDropTarget) => ReactNode
127+
renderDropIndicator?: (target: ItemDropTarget, keys?: Set<Key>, draggedKey?: Key) => ReactNode
128128
): ReactNode {
129129
let rendered = (
130130
<VirtualizerItem
@@ -141,9 +141,9 @@ function renderWrapper(
141141
if (node?.type === 'item' && renderDropIndicator && layout.getDropTargetLayoutInfo) {
142142
rendered = (
143143
<React.Fragment key={reusableView.key}>
144-
{renderDropIndicatorWrapper(parent, reusableView, {type: 'item', key: reusableView.content!.key, dropPosition: 'before'}, renderDropIndicator)}
144+
{renderDropIndicatorWrapper(parent, reusableView, {type: 'item', key: reusableView.content!.key, dropPosition: 'before'}, (target, keys, draggedKey) => renderDropIndicator(target, keys, draggedKey))}
145145
{rendered}
146-
{renderAfterDropIndicators(collection, node, target => renderDropIndicatorWrapper(parent, reusableView, target, renderDropIndicator))}
146+
{renderAfterDropIndicators(collection, node, (target, keys, draggedKey) => renderDropIndicatorWrapper(parent, reusableView, target, (innerTarget, innerKeys, innerDraggedKey) => renderDropIndicator(innerTarget, innerKeys, innerDraggedKey), keys, draggedKey))}
147147
</React.Fragment>
148148
);
149149
}
@@ -155,9 +155,11 @@ function renderDropIndicatorWrapper(
155155
parent: View | null,
156156
reusableView: View,
157157
target: ItemDropTarget,
158-
renderDropIndicator: (target: ItemDropTarget) => ReactNode
158+
renderDropIndicator: (target: ItemDropTarget, keys?: Set<Key>, draggedKey?: Key) => ReactNode,
159+
keys: Set<Key> = new Set(),
160+
draggedKey?: Key
159161
) {
160-
let indicator = renderDropIndicator(target);
162+
let indicator = renderDropIndicator(target, keys, draggedKey);
161163
if (indicator) {
162164
let layoutInfo = reusableView.virtualizer.layout.getDropTargetLayoutInfo!(target);
163165
indicator = (

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ interface DropHooks {
5959
useDroppableCollection?: (props: DroppableCollectionOptions, state: DroppableCollectionState, ref: RefObject<HTMLElement | null>) => DroppableCollectionResult,
6060
useDroppableItem?: (options: DroppableItemOptions, state: DroppableCollectionState, ref: RefObject<HTMLElement | null>) => DroppableItemResult,
6161
useDropIndicator?: (props: AriaDropIndicatorProps, state: DroppableCollectionState, ref: RefObject<HTMLElement | null>) => DropIndicatorAria,
62-
renderDropIndicator?: (target: DropTarget) => JSX.Element,
62+
renderDropIndicator?: (target: DropTarget, keys?: Set<Key>, draggedKey?: Key) => JSX.Element,
6363
dropTargetDelegate?: DropTargetDelegate,
6464
ListDropTargetDelegate: typeof ListDropTargetDelegate
6565
}
@@ -87,7 +87,7 @@ export interface DragAndDropOptions extends Omit<DraggableCollectionProps, 'prev
8787
* This should render a `<DropIndicator>` element. If this function is not provided, a
8888
* default DropIndicator is provided.
8989
*/
90-
renderDropIndicator?: (target: DropTarget) => JSX.Element,
90+
renderDropIndicator?: (target: DropTarget, keys?: Set<Key>, draggedKey?: Key) => JSX.Element,
9191
/** A custom delegate object that provides drop targets for pointer coordinates within the collection. */
9292
dropTargetDelegate?: DropTargetDelegate,
9393
/** Whether the drag and drop events should be disabled. */

packages/react-aria-components/stories/ListBox.stories.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,70 @@ ListBoxDnd.story = {
181181
}
182182
};
183183

184+
export const ListBoxDndCustomDropIndicator = (props: ListBoxProps<typeof albums[0]>) => {
185+
let list = useListData({
186+
initialItems: albums
187+
});
188+
189+
let {dragAndDropHooks} = useDragAndDrop({
190+
getItems: (keys) => [...keys].map(key => ({'text/plain': list.getItem(key)?.title ?? ''})),
191+
onReorder(e) {
192+
if (e.target.dropPosition === 'before') {
193+
list.moveBefore(e.target.key, e.keys);
194+
} else if (e.target.dropPosition === 'after') {
195+
list.moveAfter(e.target.key, e.keys);
196+
}
197+
},
198+
renderDropIndicator(target, keys, draggedKey) {
199+
return (
200+
<DropIndicator target={target} style={({isDropTarget}) => ({width: '150px', height: '150px', background: isDropTarget ? 'blue' : 'transparent', color: 'white', display: isDropTarget ? 'flex' : 'none', alignItems: 'center', justifyContent: 'center', flexDirection: 'column'})}>
201+
<div>
202+
keys: {keys ? Array.from(keys).join(', ') : 'undefined'}
203+
</div>
204+
<div>
205+
draggedKey: {draggedKey}
206+
</div>
207+
</DropIndicator>
208+
);
209+
}
210+
});
211+
212+
return (
213+
<ListBox
214+
{...props}
215+
aria-label="Albums"
216+
items={list.items}
217+
selectionMode="multiple"
218+
dragAndDropHooks={dragAndDropHooks}>
219+
{item => (
220+
<ListBoxItem>
221+
<img src={item.image} alt="" />
222+
<Text slot="label">{item.title}</Text>
223+
<Text slot="description">{item.artist}</Text>
224+
</ListBoxItem>
225+
)}
226+
</ListBox>
227+
);
228+
};
229+
230+
ListBoxDndCustomDropIndicator.story = {
231+
args: {
232+
layout: 'stack',
233+
orientation: 'horizontal'
234+
},
235+
argTypes: {
236+
layout: {
237+
control: 'radio',
238+
options: ['stack', 'grid']
239+
},
240+
orientation: {
241+
control: 'radio',
242+
options: ['horizontal', 'vertical']
243+
}
244+
}
245+
};
246+
247+
184248
export const ListBoxHover = () => (
185249
<ListBox className={styles.menu} aria-label="test listbox" onAction={action('onAction')} >
186250
<MyListBoxItem onHoverStart={action('onHoverStart')} onHoverChange={action('onHoverChange')} onHoverEnd={action('onHoverEnd')}>Hover</MyListBoxItem>

packages/react-aria-components/test/GridList.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,42 @@ describe('GridList', () => {
851851
expect(onReorder).toHaveBeenCalledTimes(1);
852852
});
853853

854+
it('should pass keys and draggedKey to renderDropIndicator', async () => {
855+
let onReorder = jest.fn();
856+
let renderDropIndicatorCalls = [];
857+
let mockRenderDropIndicator = jest.fn((target, keys, draggedKey) => {
858+
renderDropIndicatorCalls.push({target, keys, draggedKey});
859+
return <DropIndicator target={target}>Keys: {keys ? keys.size : 0} DraggedKey: {draggedKey || 'none'}</DropIndicator>;
860+
});
861+
862+
let {getAllByRole} = render(
863+
<DraggableGridList
864+
onReorder={onReorder}
865+
renderDropIndicator={mockRenderDropIndicator} />
866+
);
867+
868+
await user.tab();
869+
await user.keyboard('{ArrowRight}');
870+
await user.keyboard('{Enter}');
871+
act(() => jest.runAllTimers());
872+
873+
expect(mockRenderDropIndicator).toHaveBeenCalled();
874+
875+
renderDropIndicatorCalls.forEach(call => {
876+
expect(call.target).toBeDefined();
877+
expect(call.keys).toBeInstanceOf(Set);
878+
expect(call.keys.size).toBe(1);
879+
expect(call.keys.has('cat')).toBe(true);
880+
expect(call.draggedKey).toBe('cat');
881+
});
882+
883+
let rows = getAllByRole('row');
884+
expect(rows[0]).toHaveTextContent('Keys: 1 DraggedKey: cat');
885+
886+
fireEvent.keyDown(document.activeElement, {key: 'Enter'});
887+
fireEvent.keyUp(document.activeElement, {key: 'Enter'});
888+
});
889+
854890
it('should support dropping on rows', async () => {
855891
let onItemDrop = jest.fn();
856892
let {getAllByRole} = render(<>

0 commit comments

Comments
 (0)