diff --git a/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx b/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx index b59ee06e31d..ef31408b4ae 100644 --- a/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx @@ -13,7 +13,9 @@ import {ActionButton, ActionButtonGroup, ActionMenu, Content, Heading, IllustratedMessage, Image, ListView, ListViewItem, MenuItem, Text} from '../src'; import {checkers} from './check'; import Delete from '../s2wf-icons/S2_Icon_Delete_20_N.svg'; +import {DragBetweenLists, Reorderable} from '../stories/ListView.stories'; import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; +import {expect, userEvent, within} from 'storybook/test'; import File from '../s2wf-icons/S2_Icon_File_20_N.svg'; import Folder from '../s2wf-icons/S2_Icon_Folder_20_N.svg'; import FolderOpen from '../spectrum-illustrations/linear/FolderOpen'; @@ -253,3 +255,45 @@ export const EmptyState: Story = { ) }; + +export const InsertionIndicator: Story = { + ...Reorderable, + play: async ({canvasElement}) => { + await userEvent.tab(); + await userEvent.keyboard('[Tab]'); + // TODO: strangely enough tabbing via user event actually focuses the drag handle and not just the row + // can't reproduce manually + // await userEvent.keyboard('[ArrowRight]'); + await userEvent.keyboard('[Enter]'); + let body = canvasElement.ownerDocument.body; + await within(body).findByText('Insert between Adobe Photoshop and Adobe XD'); + } +}; + +export const RootDrop: Story = { + ...DragBetweenLists, + play: async () => { + await userEvent.tab(); + await userEvent.keyboard('[Tab]'); + // await userEvent.keyboard('[ArrowRight]'); + await userEvent.keyboard('[Enter]'); + await userEvent.keyboard('[Tab]'); + expect(document.activeElement).toHaveRole('button'); + expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on'); + } +}; + +export const OnFolderDrop: Story = { + ...DragBetweenLists, + play: async () => { + await userEvent.tab(); + await userEvent.keyboard('[Tab]'); + // await userEvent.keyboard('[ArrowRight]'); + await userEvent.keyboard('[Enter]'); + await userEvent.keyboard('[Tab]'); + await userEvent.keyboard('[ArrowDown]'); + await userEvent.keyboard('[ArrowDown]'); + expect(document.activeElement).toHaveRole('button'); + expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on Pictures'); + } +}; diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 912dee58764..47c37a8b0c9 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -13,15 +13,15 @@ import {ActionButtonGroupContext} from './ActionButtonGroup'; import {ActionMenuContext} from './ActionMenu'; import {baseColor, colorMix, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; -import {centerBaseline} from './CenterBaseline'; -import {Checkbox} from './Checkbox'; import { + Button, CheckboxContext, Collection, CollectionRendererContext, ContextValue, DEFAULT_SLOT, DefaultCollectionRenderer, + DropIndicator, GridList, GridListItem, GridListItemProps, @@ -37,10 +37,13 @@ import { useSlottedContext, Virtualizer } from 'react-aria-components'; +import {centerBaseline} from './CenterBaseline'; +import {Checkbox} from './Checkbox'; import Chevron from '../ui-icons/Chevron'; import {controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {createContext, forwardRef, ReactElement, ReactNode, useContext, useRef} from 'react'; -import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, LoadingState} from '@react-types/shared'; +import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, ItemDropTarget, LoadingState} from '@react-types/shared'; +import DragHandle from '../ui-icons/DragHandle'; import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import {IconContext} from './Icon'; import {ImageContext} from './Image'; @@ -51,11 +54,11 @@ import {ProgressCircle} from './ProgressCircle'; import {Text, TextContext} from './Content'; import {useActionBarContainer} from './ActionBar'; import {useDOMRef} from '@react-spectrum/utils'; -import {useLocale, useLocalizedStringFormatter} from 'react-aria'; +import {useFocusRing, useLocale, useLocalizedStringFormatter, useVisuallyHidden} from 'react-aria'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | 'dragAndDropHooks' | 'layout' | 'render' | 'keyboardNavigationBehavior' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { +export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | 'layout' | 'render' | 'keyboardNavigationBehavior' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { /** Spectrum-defined styles, returned by the `style()` macro. */ styles?: StylesPropWithHeight, /** The current loading state of the ListView. */ @@ -112,11 +115,13 @@ const listViewWrapper = style({ // When any row has a trailing icon, reserve space so actions align. const hasTrailingIconRows = ':has([data-has-trailing-icon]) [role="row"]'; -const listView = style({ +const dropTargetBackground = colorMix('gray-25', 'blue-900', 10); +const listView = style({ ...focusRing(), outlineOffset: { default: -2, - isQuiet: -1 + isQuiet: -1, + isDropTarget: -2 }, userSelect: 'none', minHeight: 0, @@ -129,7 +134,10 @@ const listView = style({ backgroundColor: { default: 'gray-25', isQuiet: 'transparent', - forcedColors: 'Background' + isDropTarget: { + default: dropTargetBackground, + forcedColors: 'Background' + } }, borderRadius: { default: 'default', @@ -141,6 +149,20 @@ const listView = style({ isQuiet: 0 }, borderStyle: 'solid', + // use outline with negative offset instead of border for the drop target styling to avoid layout shifting + outlineWidth: { + isDropTarget: 2 + }, + outlineStyle: { + isDropTarget: 'solid' + }, + outlineColor: { + isDropTarget: 'blue-800', + forcedColors: { + isDropTarget: 'Highlight' + } + }, + forcedColorAdjust: 'none', '--trailing-icon-width': { type: 'width', value: { @@ -158,8 +180,17 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li ref: DOMRef ) { [props, ref] = useSpectrumContextProps(props, ref, ListViewContext); - let {children, isQuiet, selectionStyle = 'checkbox', overflowMode = 'truncate', loadingState, onLoadMore, renderEmptyState: userRenderEmptyState, hideLinkOutIcon = false, ...otherProps} = props; + let {children, isQuiet, selectionStyle = 'checkbox', overflowMode = 'truncate', loadingState, onLoadMore, renderEmptyState: userRenderEmptyState, hideLinkOutIcon = false, dragAndDropHooks, ...otherProps} = props; let scale = useScale(); + + if (dragAndDropHooks && dragAndDropHooks.renderDragPreview == null) { + dragAndDropHooks.renderDragPreview = (items) => ; + } + + if (dragAndDropHooks) { + dragAndDropHooks.renderDropIndicator = (target) => ; + } + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); let rowHeight = scale === 'large' ? 50 : 40; @@ -233,12 +264,14 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li layout={ListLayout} layoutOptions={{ estimatedRowHeight: rowHeight, - loaderHeight: 60 + loaderHeight: 60, + dropIndicatorThickness: 12 // 8 + 2 + 2 aka circle height + the circle thickness * 2 }}> listView({ ...renderProps, - isQuiet + isQuiet, + isDropTarget: renderProps.isDropTarget })} selectedKeys={selectedKeys} defaultSelectedKeys={undefined} @@ -278,9 +312,25 @@ const listitem = style({ - outlineStyle: 'none', + outlineStyle: { + default: 'none', + isDropTarget: 'solid' + }, + outlineWidth: { + isDropTarget: 2 + }, + outlineOffset: { + isDropTarget: -2 + }, + outlineColor: { + isDropTarget: 'blue-800', + forcedColors: { + isDropTarget: 'Highlight' + } + }, boxSizing: 'border-box', columnGap: 0, paddingX: 0, @@ -305,10 +355,10 @@ const listitem = style({ position: 'absolute', zIndex: -1, @@ -392,6 +443,7 @@ const listRowBackground = style({ + alignItems: 'center', + justifyContent: 'center', + // TODO: arbitrary, basically taken from v3 + height: 22, + width: 16, + padding: 0, + margin: 0, + backgroundColor: 'transparent', + borderStyle: 'none', + borderRadius: 'sm', + // TODO: this mimicks v3 too, do we want halo focus ring? + outlineStyle: { + default: 'none', + isFocusVisible: 'solid' + }, + outlineColor: { + default: 'focus-ring', + forcedColors: 'Highlight' + }, + outlineWidth: 2, + '--iconPrimary': { + type: 'fill', + value: 'currentColor' + } +}); + +let dragPreviewWrapper = style({ + position: 'relative' +}); + +let dragPreviewCardBack = style({ + position: 'absolute', + zIndex: -1, + top: 4, + left: 4, + width: 200, + height: 'full', + borderRadius: 'default', + borderWidth: 1, + borderStyle: 'solid', + borderColor: 'blue-900', + backgroundColor: 'gray-25' +}); + +let dragPreviewCard = style<{scale?: 'medium' | 'large'}>({ + boxSizing: 'border-box', + paddingX: 0, + paddingY: 8, + backgroundColor: 'gray-25', + color: baseColor('neutral'), + position: 'relative', + display: 'grid', + // TODO get rid of description and icon if we end up not being able to grab those from the node + gridTemplateAreas: [ + '. icon label badge .', + '. . description badge .' + ], + gridTemplateColumns: [edgeToText(40), 'auto', 'minmax(0, 1fr)', 'auto', edgeToText(40)], + gridTemplateRows: '1fr auto', + alignItems: 'baseline', + minHeight: { + default: 40, + scale: { + large: 50 + } + }, + width: 200, + borderRadius: 'default', + borderWidth: 1, + borderStyle: 'solid', + borderColor: 'blue-900' +}); + +let dragPreviewBadge = style({ + gridArea: 'badge', + alignSelf: 'center', + paddingX: 8, + paddingY: 2, + borderRadius: 'sm', + backgroundColor: { + default: 'blue-900', + forcedColors: 'Highlight' + }, + font: 'ui-sm', + fontWeight: 'bold', + color: { + default: 'white', + forcedColors: 'HighlightText' + }, + forcedColorAdjust: 'none' +}); + +let insertionIndicatorWrapper = style({ + position: 'absolute', + inset: 0, + display: 'flex', + alignItems: 'center' +}); + +let insertionIndicatorBar = style<{isDropTarget?: boolean}>({ + flexGrow: 1, + height: 2, + backgroundColor: { + default: 'transparent', + isDropTarget: 'blue-800', + forcedColors: { + isDropTarget: 'Highlight' + } + }, + borderBottomWidth: { + default: 0, + isDropTarget: 2 + }, + borderColor: { + isDropTarget: 'blue-800', + forcedColors: { + isDropTarget: 'Highlight' + } + }, + forcedColorAdjust: 'none' +}); + +let insertionIndicatorCircle = style<{isDropTarget: boolean}>({ + width: 8, + height: 8, + borderRadius: 'full', + borderWidth: { + isDropTarget: 2 + }, + borderStyle: { + isDropTarget: 'solid' + }, + borderColor: { + isDropTarget: 'blue-800', + forcedColors: { + isDropTarget: 'Highlight' + } + }, + backgroundColor: { + isDropTarget: 'gray-25', + forcedColors: { + default: 'transparent', + isDropTarget: 'Background' + } + }, + forcedColorAdjust: 'none' +}); + const centeredWrapper = style({ display: 'flex', alignItems: 'center', @@ -699,6 +916,22 @@ const emptyStateWrapper = style({ padding: 16 }); +// TODO: since I'm not using absolute positioning, the drop indicator at the very top isn't flush with the top edge of the listview +// maybe ok? +function InsertionIndicatorVisual({target}: {target: ItemDropTarget}) { + return ( + + {({isDropTarget}) => ( +
+
+
+
+
+ )} + + ); +} + function ListSelectionCheckbox({isDisabled}: {isDisabled: boolean}) { let selectionContext = useSlottedContext(CheckboxContext, 'selection'); let isSelectionDisabled = isDisabled || !!selectionContext?.isDisabled; @@ -737,6 +970,45 @@ function isLastItem(id: Key | undefined, state: ListState) { return state.collection.getLastKey() === id; } +export function ListViewDragPreview(props) { + let {items, overflowMode} = props; + let isDraggingMultiple = items.length > 1; + // TODO: item here doesn't have rendered, cuz unlike in v3, we don't have access to the collection nodes at this level... + // alternatives are to perhaps export this and allow the user to pass in label/description/etc nodes as children or allow them to render + // anything they way and just provide the current as a default + let itemLabel = items[0]?.['text/plain'] ?? ''; + let scale = useScale(); + + return ( +
+ {isDraggingMultiple &&
} +
+ + {itemLabel} + {isDraggingMultiple && ( +
{items.length}
+ )} +
+
+
+ ); +} + export function ListViewItem(props: ListViewItemProps): ReactNode { let ref = useRef(null); let {hasChildItems, ...otherProps} = props; @@ -746,6 +1018,11 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { let textValue = props.textValue || (typeof props.children === 'string' ? props.children : undefined); let {direction} = useLocale(); let hasTrailingIcon = hasChildItems || (isLinkOut && !hideLinkOutIcon); + let {visuallyHiddenProps} = useVisuallyHidden(); + let { + isFocusVisible: isFocusVisibleWithin, + focusProps: focusWithinProps + } = useFocusRing({within: true}); return ( {(renderProps) => { let {children} = props; - let {selectionMode, selectionBehavior, isDisabled, id, state} = renderProps; + let {selectionMode, selectionBehavior, isDisabled, id, state, allowsDragging, isFocusVisible} = renderProps; return ( -
- {renderProps.isFocusVisible && +
- } - {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( - - )} - {typeof children === 'string' ? {children} : children} - {isLinkOut && !hideLinkOutIcon && ( -
- + {renderProps.isFocusVisible && +
+ } + {allowsDragging && !isDisabled && ( +
+ +
+ )} + {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( + + )} + {typeof children === 'string' ? {children} : children} + {isLinkOut && !hideLinkOutIcon && ( +
+ -
- )} - {hasChildItems && !isLinkOut && ( -
- +
+ )} + {hasChildItems && !isLinkOut && ( +
+ -
- )} + })({direction})} /> +
+ )} +
); }} ); } - diff --git a/packages/@react-spectrum/s2/stories/ListView.stories.tsx b/packages/@react-spectrum/s2/stories/ListView.stories.tsx index d2fd339d4d0..5e0d6d895c1 100644 --- a/packages/@react-spectrum/s2/stories/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx @@ -24,7 +24,8 @@ import {Key} from 'react-aria'; import type {Meta, StoryObj} from '@storybook/react'; import {ReactNode, useState} from 'react'; import {style} from '../style' with {type: 'macro'}; -import {useAsyncList} from 'react-stately'; +import {useAsyncList, useListData} from 'react-stately'; +import {useDragAndDrop} from 'react-aria-components'; const meta: Meta = { component: ListView, @@ -41,11 +42,18 @@ const meta: Meta = { styles: style({height: 320}) }, decorators: [ - (Story) => ( -
- -
- ) + (Story, context) => { + let {disableDecorator} = context.parameters; + if (disableDecorator) { + return ; + } + + return ( +
+ +
+ ); + } ] }; @@ -582,3 +590,327 @@ export const WithActionBarEmphasized: Story = { }, name: 'with ActionBar (emphasized)' }; + +let reorderItems: Item[] = [ + {id: 'a', name: 'Adobe Photoshop', type: 'file'}, + {id: 'b', name: 'Adobe XD', type: 'file'}, + {id: 'c', name: 'Documents', type: 'folder'}, + {id: 'd', name: 'Adobe InDesign', type: 'file'}, + {id: 'e', name: 'Utilities', type: 'folder'}, + {id: 'f', name: 'Adobe AfterEffects', type: 'file'}, + {id: 'g', name: 'Adobe Illustrator', type: 'file'}, + {id: 'h', name: 'Adobe Lightroom', type: 'file'}, + {id: 'i', name: 'Adobe Premiere Pro', type: 'file'}, + {id: 'j', name: 'Adobe Fresco', type: 'file'}, + {id: 'k', name: 'Adobe Dreamweaver', type: 'file'}, + {id: 'l', name: 'Adobe Connect', type: 'file'}, + {id: 'm', name: 'Pictures', type: 'folder'}, + {id: 'n', name: 'Adobe Acrobat', type: 'file'}, + {id: 'o', name: 'Really really really really really long name', type: 'file'} +]; + +function ReorderExample(props) { + let list = useListData({ + initialItems: reorderItems + }); + + let {dragAndDropHooks} = useDragAndDrop({ + getItems: (keys) => { + return [...keys].map(key => ({'text/plain': list.getItem(key)?.name ?? ''})); + }, + onReorder(e) { + if (e.target.dropPosition === 'before') { + list.moveBefore(e.target.key, e.keys); + } else if (e.target.dropPosition === 'after') { + list.moveAfter(e.target.key, e.keys); + } + } + }); + + return ( + + {(item: Item) => ( + + {item.type === 'folder' ? : } + {item.name} + + )} + + ); +} + +export const Reorderable: Story = { + render: (args) => , + name: 'Drag and drop reordering' +}; + +let folderList1 = [ + {id: '1', type: 'file', name: 'Adobe Photoshop'}, + {id: '2', type: 'file', name: 'Adobe XD'}, + {id: '3', type: 'folder', name: 'Documents', childNodes: [] as any[]}, + {id: '4', type: 'file', name: 'Adobe InDesign'}, + {id: '5', type: 'folder', name: 'Utilities', childNodes: []}, + {id: '6', type: 'file', name: 'Adobe AfterEffects'} +]; + +let folderList2 = [ + {id: '7', type: 'folder', name: 'Pictures', childNodes: [] as any[]}, + {id: '8', type: 'file', name: 'Adobe Fresco'}, + {id: '9', type: 'folder', name: 'Apps', childNodes: []}, + {id: '10', type: 'file', name: 'Adobe Illustrator'}, + {id: '11', type: 'file', name: 'Adobe Lightroom'}, + {id: '12', type: 'file', name: 'Adobe Dreamweaver'}, + {id: '13', type: 'unique_type', name: 'invalid drag item'} +]; + +let itemProcessor = async (items, acceptedDragTypes) => { + let processedItems: any[] = []; + let text = ''; + for (let item of items) { + for (let type of acceptedDragTypes) { + if (item.kind === 'text' && item.types.has(type)) { + text = await item.getText(type); + processedItems.push(JSON.parse(text)); + break; + } else if (item.types.size === 1 && item.types.has('text/plain')) { + // Fallback for Chrome Android case: https://bugs.chromium.org/p/chromium/issues/detail?id=1293803 + // Multiple drag items are contained in a single string so we need to split them out + text = await item.getText('text/plain'); + processedItems = text.split('\n').map(val => JSON.parse(val)); + break; + } + } + } + return processedItems; +}; + +function BetweenLists(props) { + let list1 = useListData({ + initialItems: folderList1 + }); + + let list2 = useListData({ + initialItems: folderList2 + }); + let acceptedDragTypes = ['file', 'folder', 'text/plain']; + + // List 1 should allow on item drops and external drops, but disallow reordering/internal drops + let {dragAndDropHooks: dragAndDropHooksList1} = useDragAndDrop({ + getItems: (keys) => [...keys].map(key => { + let item = list1.getItem(key)!; + return { + [`${item.type}`]: JSON.stringify(item), + 'text/plain': item.name + }; + }), + onReorder(e) { + if (e.target.dropPosition === 'before') { + list1.moveBefore(e.target.key, e.keys); + } else if (e.target.dropPosition === 'after') { + list1.moveAfter(e.target.key, e.keys); + } + }, + onInsert: async (e) => { + let { + items, + target + } = e; + action('onInsertList1')(e); + let processedItems = await itemProcessor(items, acceptedDragTypes); + + if (target.dropPosition === 'before') { + list1.insertBefore(target.key, ...processedItems); + } else if (target.dropPosition === 'after') { + list1.insertAfter(target.key, ...processedItems); + } + }, + onRootDrop: async (e) => { + action('onRootDropList1')(e); + let processedItems = await itemProcessor(e.items, acceptedDragTypes); + list1.append(...processedItems); + }, + onItemDrop: async (e) => { + let { + items, + target, + isInternal, + dropOperation + } = e; + action('onItemDropList1')(e); + let processedItems = await itemProcessor(items, acceptedDragTypes); + let targetItem = list1.getItem(target.key)!; + list1.update(target.key, {...targetItem, childNodes: [...(targetItem.childNodes || []), ...processedItems]}); + + if (isInternal && dropOperation === 'move') { + let keysToRemove = processedItems.map(item => item.id); + list1.remove(...keysToRemove); + } + }, + acceptedDragTypes, + onDragEnd: (e) => { + let { + dropOperation, + isInternal, + keys + } = e; + action('onDragEndList1')(e); + if (dropOperation === 'move' && !isInternal) { + list1.remove(...keys); + } + }, + getAllowedDropOperations: () => ['move', 'copy'], + shouldAcceptItemDrop: (target) => !!list1.getItem(target.key)!.childNodes + }); + +// List 2 should allow reordering, on folder drops, and on root drops + let {dragAndDropHooks: dragAndDropHooksList2} = useDragAndDrop({ + getItems: (keys) => [...keys].map(key => { + let item = list2.getItem(key)!; + let dragItem = {}; + let itemString = JSON.stringify(item); + dragItem[`${item.type}`] = itemString; + if (item.type !== 'unique_type') { + dragItem['text/plain'] = item.name; + } + + return dragItem; + }), + onInsert: async (e) => { + let { + items, + target + } = e; + action('onInsertList2')(e); + let processedItems = await itemProcessor(items, acceptedDragTypes); + + if (target.dropPosition === 'before') { + list2.insertBefore(target.key, ...processedItems); + } else if (target.dropPosition === 'after') { + list2.insertAfter(target.key, ...processedItems); + } + }, + onReorder: async (e) => { + let { + keys, + target, + dropOperation + } = e; + action('onReorderList2')(e); + + let itemsToCopy: typeof folderList2 = []; + if (dropOperation === 'copy') { + for (let key of keys) { + let item: typeof folderList2[0] = {...list2.getItem(key)!}; + item.id = Math.random().toString(36).slice(2); + itemsToCopy.push(item); + } + } + + if (target.dropPosition === 'before') { + if (dropOperation === 'move') { + list2.moveBefore(target.key, [...keys]); + } else if (dropOperation === 'copy') { + list2.insertBefore(target.key, ...itemsToCopy); + } + } else if (target.dropPosition === 'after') { + if (dropOperation === 'move') { + list2.moveAfter(target.key, [...keys]); + } else if (dropOperation === 'copy') { + list2.insertAfter(target.key, ...itemsToCopy); + } + } + }, + onRootDrop: async (e) => { + action('onRootDropList2')(e); + let processedItems = await itemProcessor(e.items, acceptedDragTypes); + list2.prepend(...processedItems); + }, + onItemDrop: async (e) => { + let { + items, + target, + isInternal, + dropOperation + } = e; + action('onItemDropList2')(e); + let processedItems = await itemProcessor(items, acceptedDragTypes); + let targetItem = list2.getItem(target.key)!; + list2.update(target.key, {...targetItem, childNodes: [...(targetItem.childNodes || []), ...processedItems]}); + + if (isInternal && dropOperation === 'move') { + let keysToRemove = processedItems.map(item => item.id); + list2.remove(...keysToRemove); + } + }, + acceptedDragTypes, + onDragEnd: (e) => { + let { + dropOperation, + isInternal, + keys + } = e; + action('onDragEndList2')(e); + if (dropOperation === 'move' && !isInternal) { + let keysToRemove = [...keys].filter(key => list2.getItem(key)!.type !== 'unique_type'); + list2.remove(...keysToRemove); + } + }, + getAllowedDropOperations: () => ['move', 'copy'], + shouldAcceptItemDrop: (target) => !!list2.getItem(target.key)!.childNodes + }); + + return ( +
+ + {(item: any) => ( + + {item.name} + {item.type === 'folder' && + <> + + {`contains ${item.childNodes.length} dropped item(s)`} + + } + {item.type === 'file' && } + + )} + + + {(item: any) => ( + + {item.name} + {item.type === 'folder' && + <> + + {`contains ${item.childNodes.length} dropped item(s)`} + + } + {item.type === 'file' && } + + )} + +
+ ); +} + +export const DragBetweenLists: Story = { + render: (args) => , + name: 'Drag between lists', + parameters: { + disableDecorator: true + } +};