From 39de835b957237182f0834b043d5f9afe2f803e7 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 12 Mar 2026 16:56:36 -0700 Subject: [PATCH 01/15] add dnd stories, drag preview, drag handle to S2 ListView --- packages/@react-spectrum/s2/src/ListView.tsx | 183 +++++++++- .../s2/stories/ListView.stories.tsx | 328 +++++++++++++++++- 2 files changed, 499 insertions(+), 12 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index b749a5a4404..e8449923735 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -13,9 +13,8 @@ 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, @@ -37,10 +36,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 DragHandle from '../ui-icons/DragHandle'; import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import {IconContext} from './Icon'; import {ImageContext} from './Image'; @@ -51,11 +53,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 {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. */ @@ -158,8 +160,14 @@ 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(); + + // TODO: how to get the actual item.rendered not just the plain text + if (dragAndDropHooks && dragAndDropHooks.renderDragPreview == null) { + dragAndDropHooks.renderDragPreview = (items) => ; + } + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); let rowHeight = scale === 'large' ? 50 : 40; @@ -239,6 +247,7 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li ({ + 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' + } +}); + +// TODO: check the below drag preview styles, try to reuse styles perhaps later but for now just keep separate +// all of these are from v3 basically +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: 'blue-900', + font: 'ui-sm', + fontWeight: 'bold', + color: 'white' +}); + const centeredWrapper = style({ display: 'flex', alignItems: 'center', @@ -697,6 +804,48 @@ 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... + // let firstItem = items[0]; + let itemLabel = items[0]?.['text/plain'] ?? ''; + let scale = useScale(); + + return ( +
+ {isDraggingMultiple &&
} +
+ + {/* {typeof firstItem.rendered === 'string' ? {firstItem.rendered} : firstItem.rendered} */} + {itemLabel} + {isDraggingMultiple && ( +
{items.length}
+ )} + +
+ + +
+
+ ); +} + export function ListViewItem(props: ListViewItemProps): ReactNode { let ref = useRef(null); let {hasChildItems, ...otherProps} = props; @@ -706,6 +855,7 @@ 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(); return ( {(renderProps) => { let {children} = props; - let {selectionMode, selectionBehavior, isDisabled, id, state} = renderProps; + let {selectionMode, selectionBehavior, isDisabled, id, state, allowsDragging, isFocusVisible} = renderProps; return ( - {renderProps.isFocusVisible && + {renderProps.isFocusVisible &&
} + {allowsDragging && ( +
+ +
+ )} {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( )} @@ -828,4 +990,3 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { ); } - diff --git a/packages/@react-spectrum/s2/stories/ListView.stories.tsx b/packages/@react-spectrum/s2/stories/ListView.stories.tsx index d91c0a26820..03b28e0b0fb 100644 --- a/packages/@react-spectrum/s2/stories/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx @@ -24,7 +24,9 @@ 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'; +import { ListViewDragPreview } from '../src/ListView'; const meta: Meta = { component: ListView, @@ -581,3 +583,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'} +]; + +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); + } + } + }); + // TODO: just for testing + let blah = [{'text/plain': 'awegaweg', rendered: 'awegaweg'}, {'text/plain': 'agwegawkjgakwjegbkawjgbe', rendered: 'agwegawkjgakwjegbkawjgbe'}]; + 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': JSON.stringify(item) + }; + }), + 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'] = itemString; + } + + 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 = 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' +}; From 0c644da59b842b6f0dcfd97a718bf8f917723fe9 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 13 Mar 2026 11:51:04 -0700 Subject: [PATCH 02/15] add insertion indicator and debug the on drop styles --- packages/@react-spectrum/s2/src/ListView.tsx | 176 ++++++++++++++++-- .../s2/stories/ListView.stories.tsx | 36 ++-- 2 files changed, 182 insertions(+), 30 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index e8449923735..6c371e6b64e 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -21,6 +21,8 @@ import { ContextValue, DEFAULT_SLOT, DefaultCollectionRenderer, + DragAndDropContext, + DropIndicator, GridList, GridListItem, GridListItemProps, @@ -41,7 +43,7 @@ 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'; @@ -49,11 +51,11 @@ import {ImageContext} from './Image'; // @ts-ignore import intlMessages from '../intl/*.json'; import LinkOutIcon from '../ui-icons/LinkOut'; +import {mergeProps, useFocusRing, useLocale, useLocalizedStringFormatter, useVisuallyHidden} from 'react-aria'; import {ProgressCircle} from './ProgressCircle'; import {Text, TextContext} from './Content'; import {useActionBarContainer} from './ActionBar'; import {useDOMRef} from '@react-spectrum/utils'; -import {useLocale, useLocalizedStringFormatter, useVisuallyHidden} from 'react-aria'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -114,7 +116,9 @@ 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, @@ -132,16 +136,41 @@ const listView = style({ default: 'gray-25', isQuiet: 'transparent', forcedColors: 'Background' + // TODO: check this + // isDropTarget: dropTargetBackground, + // forcedColors: { + // default: 'Background' + // } }, borderRadius: { default: 'default', isQuiet: 'none' }, borderColor: 'gray-300', + // borderColor: { + // default: 'gray-300', + // // isDropTarget: 'blue-800', + // forcedColors: { + // default: 'ButtonBorder', + // isDropTarget: 'Highlight' + // } + // }, borderWidth: { default: 1, - isQuiet: 0 + isQuiet: 0, + // forcedColors: { + // isDropTarget: 0 + // } }, + // TODO: will need to update the borders since they shift content if we change the width + // for drop target highlighting + // boxShadow: { + // isDropTarget: 'emphasized', + // forcedColors: '[inset 0 0 0 2px var(--hcm-buttonborder, ButtonBorder)]' + // }, + // forcedColorAdjust: { + // isDropTarget: 'none' + // }, borderStyle: 'solid', '--trailing-icon-width': { type: 'width', @@ -168,6 +197,10 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li dragAndDropHooks.renderDragPreview = (items) => ; } + if (dragAndDropHooks) { + dragAndDropHooks.renderDropIndicator = (target) => ; + } + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); let rowHeight = scale === 'large' ? 50 : 40; @@ -241,7 +274,8 @@ 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} @@ -287,7 +322,8 @@ const listitem = style({ outlineStyle: 'none', boxSizing: 'border-box', @@ -295,6 +331,11 @@ const listitem = style({ position: 'absolute', zIndex: -1, @@ -392,6 +449,7 @@ const listRowBackground = style({ + flexGrow: 1, + height: 2, + backgroundColor: { + default: 'transparent', + isDropTarget: 'blue-800', + forcedColors: { + default: 'transparent', + isDropTarget: 'Highlight' + } + }, + borderBottomWidth: { + default: 0, + isDropTarget: 2 + }, + borderColor: { + isDropTarget: 'blue-800', + forcedColors: { + isDropTarget: 'Highlight' + } + }, + forcedColorAdjust: { + isDropTarget: '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: { + isDropTarget: 'none' + } +}); + const centeredWrapper = style({ display: 'flex', alignItems: 'center', @@ -766,6 +886,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; @@ -856,10 +992,15 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { let {direction} = useLocale(); let hasTrailingIcon = hasChildItems || (isLinkOut && !hideLinkOutIcon); let {visuallyHiddenProps} = useVisuallyHidden(); + // TODO this doesn't seem to work + let { + isFocusVisible: isFocusVisibleWithin, + focusProps: focusWithinProps + } = useFocusRing({within: true}); return ( {(renderProps) => { let {children} = props; - let {selectionMode, selectionBehavior, isDisabled, id, state, allowsDragging, isFocusVisible} = renderProps; + let {selectionMode, selectionBehavior, isDisabled, id, state, allowsDragging} = renderProps; return ( } - {allowsDragging && ( + {allowsDragging && !isDisabled && (
diff --git a/packages/@react-spectrum/s2/stories/ListView.stories.tsx b/packages/@react-spectrum/s2/stories/ListView.stories.tsx index 03b28e0b0fb..b2e03e52618 100644 --- a/packages/@react-spectrum/s2/stories/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx @@ -21,12 +21,12 @@ 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'; import {Key} from 'react-aria'; +import {ListViewDragPreview} from '../src/ListView'; import type {Meta, StoryObj} from '@storybook/react'; import {ReactNode, useState} from 'react'; import {style} from '../style' with {type: 'macro'}; import {useAsyncList, useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; -import { ListViewDragPreview } from '../src/ListView'; const meta: Meta = { component: ListView, @@ -42,11 +42,18 @@ const meta: Meta = { styles: style({height: 320}) }, decorators: [ - (Story) => ( -
- -
- ) + (Story, context) => { + let {disableDecorator} = context.parameters; + if (disableDecorator) { + return ; + } + + return ( +
+ +
+ ); + } ] }; @@ -700,7 +707,7 @@ function BetweenLists(props) { let item = list1.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), - 'text/plain': JSON.stringify(item) + 'text/plain': item.name }; }), onReorder(e) { @@ -865,8 +872,8 @@ function BetweenLists(props) { aria-label="First ListView in drag between list example" items={list1.items} dragAndDropHooks={dragAndDropHooksList1} - styles={style({height: 320})} - {...props}> + {...props} + styles={style({height: 320, width: 320})}> {(item: any) => ( {item.name} @@ -881,11 +888,11 @@ function BetweenLists(props) { )} + {...props} + styles={style({height: 320, width: 320})}> {(item: any) => ( {item.name} @@ -905,5 +912,8 @@ function BetweenLists(props) { export const DragBetweenLists: Story = { render: (args) => , - name: 'Drag between lists' + name: 'Drag between lists', + parameters: { + disableDecorator: true + } }; From c209c73d144ffcaf87a04a6d8d402e5d1a6a7b3e Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 13 Mar 2026 13:20:26 -0700 Subject: [PATCH 03/15] fix drop target focus styles to avoid shifting and in HCM --- packages/@react-spectrum/s2/src/ListView.tsx | 111 +++++++++---------- 1 file changed, 52 insertions(+), 59 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 6c371e6b64e..7d746ac6615 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -116,13 +116,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 dropTargetBackground = colorMix('gray-25', 'blue-900', 10); - +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, @@ -135,43 +135,37 @@ const listView = 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, paddingY: 8, backgroundColor: 'transparent', - // backgroundColor: { - // default: 'transparent', - // isDropTarget: dropTargetBackground, - // forcedColors: {default: 'transparent', isDropTarget: 'Highlight'} - // }, color: { default: baseColor('neutral-subdued'), isSelected: baseColor('neutral'), @@ -382,37 +386,25 @@ const listitem = style Date: Fri, 13 Mar 2026 14:43:26 -0700 Subject: [PATCH 04/15] cleanup, HCM fixes, chromatic tests --- .../s2/chromatic/ListView.stories.tsx | 44 ++++ packages/@react-spectrum/s2/src/ListView.tsx | 189 +++++++++--------- .../s2/stories/ListView.stories.tsx | 40 ++-- 3 files changed, 159 insertions(+), 114 deletions(-) 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 7d746ac6615..acac3ce475e 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -21,7 +21,6 @@ import { ContextValue, DEFAULT_SLOT, DefaultCollectionRenderer, - DragAndDropContext, DropIndicator, GridList, GridListItem, @@ -51,11 +50,11 @@ import {ImageContext} from './Image'; // @ts-ignore import intlMessages from '../intl/*.json'; import LinkOutIcon from '../ui-icons/LinkOut'; -import {mergeProps, useFocusRing, useLocale, useLocalizedStringFormatter, useVisuallyHidden} from 'react-aria'; import {ProgressCircle} from './ProgressCircle'; import {Text, TextContext} from './Content'; import {useActionBarContainer} from './ActionBar'; import {useDOMRef} from '@react-spectrum/utils'; +import {useFocusRing, useLocale, useLocalizedStringFormatter, useVisuallyHidden} from 'react-aria'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -186,13 +185,12 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li let {children, isQuiet, selectionStyle = 'checkbox', overflowMode = 'truncate', loadingState, onLoadMore, renderEmptyState: userRenderEmptyState, hideLinkOutIcon = false, dragAndDropHooks, ...otherProps} = props; let scale = useScale(); - // TODO: how to get the actual item.rendered not just the plain text if (dragAndDropHooks && dragAndDropHooks.renderDragPreview == null) { dragAndDropHooks.renderDragPreview = (items) => ; } if (dragAndDropHooks) { - dragAndDropHooks.renderDropIndicator = (target) => ; + dragAndDropHooks.renderDropIndicator = (target) => ; } let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); @@ -601,6 +599,10 @@ let listRowFocusRing = style({ pointerEvents: 'none' }); +let rowWrapper = style({ + display: 'contents' +}); + export let label = style({ gridArea: 'label', alignSelf: 'center', @@ -735,8 +737,6 @@ let dragButton = style<{isFocusVisible?: boolean}>({ } }); -// TODO: check the below drag preview styles, try to reuse styles perhaps later but for now just keep separate -// all of these are from v3 basically let dragPreviewWrapper = style({ position: 'relative' }); @@ -790,10 +790,17 @@ let dragPreviewBadge = style({ paddingX: 8, paddingY: 2, borderRadius: 'sm', - backgroundColor: 'blue-900', + backgroundColor: { + default: 'blue-900', + forcedColors: 'Highlight' + }, font: 'ui-sm', fontWeight: 'bold', - color: 'white' + color: { + default: 'white', + forcedColors: 'HighlightText' + }, + forcedColorAdjust: 'none' }); let insertionIndicatorWrapper = style({ @@ -937,7 +944,8 @@ 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... - // let firstItem = items[0]; + // 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(); @@ -961,15 +969,11 @@ export function ListViewDragPreview(props) { } }] ]}> - {/* {typeof firstItem.rendered === 'string' ? {firstItem.rendered} : firstItem.rendered} */} {itemLabel} {isDraggingMultiple && (
{items.length}
)} - - -
); @@ -985,7 +989,6 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { let {direction} = useLocale(); let hasTrailingIcon = hasChildItems || (isLinkOut && !hideLinkOutIcon); let {visuallyHiddenProps} = useVisuallyHidden(); - // TODO this doesn't seem to work let { isFocusVisible: isFocusVisibleWithin, focusProps: focusWithinProps @@ -993,7 +996,7 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { return ( {(renderProps) => { let {children} = props; - let {selectionMode, selectionBehavior, isDisabled, id, state, allowsDragging} = renderProps; + let {selectionMode, selectionBehavior, isDisabled, id, state, allowsDragging, isFocusVisible} = renderProps; return ( -
- {renderProps.isFocusVisible && +
- } - {allowsDragging && !isDisabled && ( -
- -
- )} - {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 b2e03e52618..bc4dd0c90e2 100644 --- a/packages/@react-spectrum/s2/stories/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx @@ -21,7 +21,6 @@ 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'; import {Key} from 'react-aria'; -import {ListViewDragPreview} from '../src/ListView'; import type {Meta, StoryObj} from '@storybook/react'; import {ReactNode, useState} from 'react'; import {style} from '../style' with {type: 'macro'}; @@ -605,7 +604,8 @@ let reorderItems: Item[] = [ {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: 'n', name: 'Adobe Acrobat', type: 'file'}, + {id: 'o', name: 'Really really really really really long name', type: 'file'} ]; function ReorderExample(props) { @@ -625,24 +625,20 @@ function ReorderExample(props) { } } }); - // TODO: just for testing - let blah = [{'text/plain': 'awegaweg', rendered: 'awegaweg'}, {'text/plain': 'agwegawkjgakwjegbkawjgbe', rendered: 'agwegawkjgakwjegbkawjgbe'}]; + return ( - <> - - - {(item: Item) => ( - - {item.type === 'folder' ? : } - {item.name} - - )} - - + + {(item: Item) => ( + + {item.type === 'folder' ? : } + {item.name} + + )} + ); } @@ -766,7 +762,7 @@ function BetweenLists(props) { } }, getAllowedDropOperations: () => ['move', 'copy'], - shouldAcceptItemDrop: (target) => !!list1.getItem(target.key)!.childNodes, + shouldAcceptItemDrop: (target) => !!list1.getItem(target.key)!.childNodes }); // List 2 should allow reordering, on folder drops, and on root drops @@ -777,7 +773,7 @@ function BetweenLists(props) { let itemString = JSON.stringify(item); dragItem[`${item.type}`] = itemString; if (item.type !== 'unique_type') { - dragItem['text/plain'] = itemString; + dragItem['text/plain'] = item.name; } return dragItem; @@ -808,7 +804,7 @@ function BetweenLists(props) { if (dropOperation === 'copy') { for (let key of keys) { let item: typeof folderList2[0] = {...list2.getItem(key)!}; - item = Math.random().toString(36).slice(2); + item.id = Math.random().toString(36).slice(2); itemsToCopy.push(item); } } From 0766a265cb6f4eb5c00024bbe7bc40db02d06a87 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 13 Mar 2026 17:10:12 -0700 Subject: [PATCH 05/15] fix lint --- packages/@react-spectrum/s2/src/ListView.tsx | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index bf69ef9b318..b2f04736002 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -162,9 +162,7 @@ const listView = style({ default: 'transparent', isDropTarget: 'blue-800', forcedColors: { - default: 'transparent', isDropTarget: 'Highlight' } }, @@ -871,9 +865,7 @@ let insertionIndicatorBar = style<{isDropTarget?: boolean}>({ isDropTarget: 'Highlight' } }, - forcedColorAdjust: { - isDropTarget: 'none' - } + forcedColorAdjust: 'none' }); let insertionIndicatorCircle = style<{isDropTarget: boolean}>({ @@ -899,9 +891,7 @@ let insertionIndicatorCircle = style<{isDropTarget: boolean}>({ isDropTarget: 'Background' } }, - forcedColorAdjust: { - isDropTarget: 'none' - } + forcedColorAdjust: 'none' }); const centeredWrapper = style({ From 75e9e2e65d55bab441ddb1a787b6c0807d737a13 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 19 Mar 2026 15:43:51 -0700 Subject: [PATCH 06/15] stopping point for table --- packages/@react-spectrum/s2/intl/en-US.json | 1 + packages/@react-spectrum/s2/src/ListView.tsx | 4 +- packages/@react-spectrum/s2/src/TableView.tsx | 154 +++++++- .../s2/stories/TableView.stories.tsx | 335 ++++++++++++++++++ 4 files changed, 480 insertions(+), 14 deletions(-) diff --git a/packages/@react-spectrum/s2/intl/en-US.json b/packages/@react-spectrum/s2/intl/en-US.json index c8375745930..1a1e1570bae 100644 --- a/packages/@react-spectrum/s2/intl/en-US.json +++ b/packages/@react-spectrum/s2/intl/en-US.json @@ -31,6 +31,7 @@ "slider.maximum": "Maximum", "slider.minimum": "Minimum", "table.cancel": "Cancel", + "table.drag": "Drag", "table.editCell": "Edit cell", "table.loading": "Loading…", "table.loadingMore": "Loading more…", diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 47c37a8b0c9..7ecc489a2cf 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -188,7 +188,7 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li } if (dragAndDropHooks) { - dragAndDropHooks.renderDropIndicator = (target) => ; + dragAndDropHooks.renderDropIndicator = (target) => ; } let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); @@ -918,7 +918,7 @@ const emptyStateWrapper = style({ // 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}) { +export function InsertionIndicator({target}: {target: ItemDropTarget}) { return ( {({isDropTarget}) => ( diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 829e259ddeb..b087d8fcca9 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -60,10 +60,12 @@ import Chevron from '../ui-icons/Chevron'; import Close from '../s2wf-icons/S2_Icon_Close_20_N.svg'; import {ColumnSize} from '@react-types/table'; import {CustomDialog, DialogContainer} from '..'; -import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, LinkDOMProps, LoadingState, Node} from '@react-types/shared'; +import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, ItemDropTarget, LinkDOMProps, LoadingState, Node} from '@react-types/shared'; +import DragHandle from '../ui-icons/DragHandle'; import {getActiveElement, getOwnerDocument, isFocusWithin, nodeContains, useLayoutEffect, useObjectRef} from '@react-aria/utils'; import {GridNode} from '@react-types/grid'; import {IconContext} from './Icon'; +import {InsertionIndicator, ListViewDragPreview} from './ListView'; // @ts-ignore import intlMessages from '../intl/*.json'; import {LayoutNode} from '@react-stately/layout'; @@ -77,10 +79,10 @@ import SortUpArrow from '../s2wf-icons/S2_Icon_SortUp_20_N.svg'; import {Button as SpectrumButton} from './Button'; import {useActionBarContainer} from './ActionBar'; import {useDOMRef, useMediaQuery} from '@react-spectrum/utils'; +import {useFocusRing, useVisuallyHidden, VisuallyHidden} from 'react-aria'; import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -import {VisuallyHidden} from 'react-aria'; interface S2TableProps { /** Whether the Table should be displayed with a quiet style. */ @@ -122,7 +124,7 @@ interface S2TableProps { } // TODO: Note that loadMore and loadingState are now on the Table instead of on the TableBody -export interface TableViewProps extends Omit, DOMProps, UnsafeStyles, S2TableProps { +export interface TableViewProps extends Omit, DOMProps, UnsafeStyles, S2TableProps { /** Spectrum-defined styles, returned by the `style()` macro. */ styles?: StylesPropWithHeight } @@ -140,7 +142,7 @@ const tableWrapper = style({ overflow: 'clip' }, getAllowedOverrides({height: true})); -const table = style({ +const table = style({ width: 'full', height: 'full', boxSizing: 'border-box', @@ -162,7 +164,23 @@ const table = style ; + } + + if (dragAndDropHooks) { + dragAndDropHooks.renderDropIndicator = (target) => ; + } + let domRef = useDOMRef(ref); let scale = useScale(); @@ -327,6 +358,7 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re let scrollRef = useRef(null); let isCheckboxSelection = selectionMode === 'multiple' || selectionMode === 'single'; + let isDragAndDrop = !!dragAndDropHooks?.useDraggableCollectionState; let {selectedKeys, onSelectionChange, actionBar, actionBarHeight} = useActionBarContainer({...props, scrollRef}); @@ -350,7 +382,11 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re : undefined, // No need for estimated headingHeight since the headers aren't affected by overflow mode: wrap headingHeight: DEFAULT_HEADER_HEIGHT[scale], - loaderHeight: 60 + loaderHeight: 60, + // TODO: figure out why this is cut off + // TODO: override the layout in RAC and have the dropIndicators get tabindex 1, do same for gridlist + // 8px circle + 2px top + 2px bottom padding + dropIndicatorThickness: 12 }}> table({ ...renderProps, isCheckboxSelection, + isDragAndDrop, isQuiet })} selectionBehavior="toggle" selectionMode={selectionMode} onRowAction={onAction} + dragAndDropHooks={dragAndDropHooks} {...otherProps} selectedKeys={selectedKeys} defaultSelectedKeys={undefined} @@ -899,15 +937,28 @@ export interface TableHeaderProps extends Omit, 'style */ export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableHeader({columns, dependencies, children}: TableHeaderProps, ref: DOMRef) { let scale = useScale(); - let {selectionBehavior, selectionMode} = useTableOptions(); + let {selectionBehavior, selectionMode, allowsDragging} = useTableOptions(); let {isQuiet} = useContext(InternalTableContext); let domRef = useDOMRef(ref); + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); return ( + {allowsDragging && ( + // @ts-ignore + + {/* TODO: intl, need to grab for other locales */} + {({isFocusVisible}) => ( + <> + {isFocusVisible && } + {stringFormatter.format('table.drag')} + + )} + + )} {/* Add extra columns for selection. */} {selectionBehavior === 'toggle' && ( // Also isSticky prop is applied just for the layout, will decide what the RAC api should be later @@ -1017,6 +1068,42 @@ const checkboxCellStyle = style({ backgroundColor: '--rowBackgroundColor' }); +// const dragCellStyle = style({ +// ...commonCellStyles, +// ...stickyCell, +// paddingStart: 12, +// paddingEnd: 4, +// alignContent: 'center', +// height: 'calc(100% - 1px)', +// borderBottomWidth: 0, +// backgroundColor: '--rowBackgroundColor' +// }); + +const dragButton = style({ + alignItems: 'center', + justifyContent: 'center', + height: 22, + width: 16, + padding: 0, + margin: 0, + backgroundColor: 'transparent', + borderStyle: 'none', + borderRadius: 'sm', + outlineStyle: { + default: 'none', + isFocusVisible: 'solid' + }, + outlineColor: { + default: 'focus-ring', + forcedColors: 'Highlight' + }, + outlineWidth: 2, + '--iconPrimary': { + type: 'fill', + value: 'currentColor' + } +}); + const cellContent = style({ truncate: true, whiteSpace: { @@ -1080,7 +1167,7 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef {({id, isFocusVisible, hasChildItems, isTreeColumn, isExpanded, isDisabled}) => ( <> - {hasChildItems && isTreeColumn && + {hasChildItems && isTreeColumn && } {children} @@ -1253,7 +1340,7 @@ export const EditableCell = forwardRef(function EditableCell(props: EditableCell {...otherProps}> {({id, isFocusVisible, hasChildItems, isTreeColumn, isExpanded, isDisabled}) => ( <> - {hasChildItems && isTreeColumn && + {hasChildItems && isTreeColumn && } } /> @@ -1544,7 +1631,22 @@ const row = style({ // isFocusVisible: 'solid' // } // }, - outlineStyle: 'none', + outlineStyle: { + default: 'none', + isDropTarget: 'solid' + }, + outlineWidth: { + isDropTarget: 2 + }, + outlineOffset: { + isDropTarget: -2 + }, + outlineColor: { + isDropTarget: { + default: 'blue-800', + forcedColors: 'Highlight' + } + }, borderTopWidth: 0, borderBottomWidth: 1, borderStartWidth: 0, @@ -1570,9 +1672,19 @@ export interface RowProps extends Pick, 'id' | 'columns' | 'is * A row within a ``. */ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row({id, columns, children, dependencies = [], ...otherProps}: RowProps, ref: DOMRef) { - let {selectionBehavior, selectionMode} = useTableOptions(); + let {selectionBehavior, selectionMode, allowsDragging} = useTableOptions(); let tableVisualOptions = useContext(InternalTableContext); let domRef = useDOMRef(ref); + let {visuallyHiddenProps} = useVisuallyHidden(); + let { + // TODO: can't move these props to an internal wrapper unlike listview since it expects cell children... + // Row doesn't have a data selector for focus visible either... + // TODO: options -> add data-focusvisible withing to RAC row (render prop already exists) + // have cell render props also have row focus within provided to it + // have cell also have table row render props alongside cell render props + isFocusVisible: isFocusVisibleWithin, + focusProps: focusWithinProps + } = useFocusRing({within: true}); return ( + {allowsDragging && ( + // TODO: this isn't being sticky when selection isn't enabled + // @ts-ignore + + {/* TODO: check if this isDisabled is enough, should be if selection and action is disabled */} + {/* can try to move this into cell perhaps but focusvisible needs to be set on the row and we'd need + to only render it once via something similar to isTreeCOlumn */} + {!otherProps.isDisabled && ( + + ) + } + + )} {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( // Not sure what we want to do with this className, in Cell it currently overrides the className that would have been applied. // The `spread` otherProps must be after className in Cell. diff --git a/packages/@react-spectrum/s2/stories/TableView.stories.tsx b/packages/@react-spectrum/s2/stories/TableView.stories.tsx index 1bee8605955..c3fbf5166b5 100644 --- a/packages/@react-spectrum/s2/stories/TableView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TableView.stories.tsx @@ -36,6 +36,7 @@ import { Text, TextField } from '../src'; +import {useDragAndDrop} from 'react-aria-components'; import {categorizeArgTypes, getActionArgs} from './utils'; import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; import Filter from '../s2wf-icons/S2_Icon_Filter_20_N.svg'; @@ -1848,3 +1849,337 @@ function NestedInlineEditExample(args) { export const TableWithNestedRowsAndInlineEditing: StoryObj = { render: (args) => }; + +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: [] as any[]}, + {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: [] as any[]}, + {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 dragColumns = [ + {name: 'ID', id: 'id', width: 40}, + {name: 'Name', id: 'name', width: 300, isRowHeader: true}, + {name: 'Type', id: 'type'} +]; + +function ReorderableTableExample(props) { + let list = useListData({initialItems: folderList1}); + + let acceptedDragTypes = ['file', 'folder', 'text/plain']; + let {dragAndDropHooks} = useDragAndDrop({ + getItems: (keys) => [...keys].map(key => { + let item = list.getItem(key)!; + return { + [`${item.type}`]: JSON.stringify(item), + 'text/plain': item.name + }; + }), + onReorder: async (e) => { + let { + keys, + target, + dropOperation + } = e; + action('onReorder')(e); + + let itemsToCopy: typeof folderList1 = []; + if (dropOperation === 'copy') { + for (let key of keys) { + let item: typeof folderList1[0] = {...list.getItem(key)!}; + item.id = Math.random().toString(36).slice(2); + itemsToCopy.push(item); + } + } + + if (target.dropPosition === 'before') { + if (dropOperation === 'move') { + list.moveBefore(target.key, [...keys]); + } else if (dropOperation === 'copy') { + list.insertBefore(target.key, ...itemsToCopy); + } + } else if (target.dropPosition === 'after') { + if (dropOperation === 'move') { + list.moveAfter(target.key, [...keys]); + } else if (dropOperation === 'copy') { + list.insertAfter(target.key, ...itemsToCopy); + } + } + }, + acceptedDragTypes + }); + + return ( + + + {column => {column.name}} + + + {item => ( + + {(column) => { + return {item[column.id]}; + }} + + )} + + + ); +} + +export const DragAndDropReorder: StoryObj = { + render: (args) => , + name: 'Drag and drop reorder' +}; + +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 BetweenTables(props) { + let list1 = useListData({initialItems: folderList1}); + let list2 = useListData({initialItems: folderList2}); + let acceptedDragTypes = ['file', 'folder', 'text/plain']; + + // table 1 should allow on item drops and external drops, but disallow reordering/internal drops + let {dragAndDropHooks: dragAndDropHooksTable1} = useDragAndDrop({ + getItems: (keys) => [...keys].map(key => { + let item = list1.getItem(key)!; + return { + [`${item.type}`]: JSON.stringify(item), + 'text/plain': item.name + }; + }), + onInsert: async (e) => { + let { + items, + target + } = e; + action('onInsertTable1')(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('onRootDropTable1')(e); + let processedItems = await itemProcessor(e.items, acceptedDragTypes); + list1.append(...processedItems); + }, + onItemDrop: async (e) => { + let { + items, + target, + isInternal, + dropOperation + } = e; + action('onItemDropTable1')(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('onDragEndTable1')(e); + if (dropOperation === 'move' && !isInternal) { + list1.remove(...keys); + } + }, + getAllowedDropOperations: () => ['move', 'copy'], + shouldAcceptItemDrop: (target) => !!list1.getItem(target.key)?.childNodes, + }); + + // table 2 should allow reordering, on folder drops, and on root drops + let {dragAndDropHooks: dragAndDropHooksTable2} = 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'] = itemString; + } + + return dragItem; + }), + onInsert: async (e) => { + let { + items, + target + } = e; + action('onInsertTable2')(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('onReorderTable2')(e); + + let itemsToCopy: typeof folderList1 = []; + if (dropOperation === 'copy') { + for (let key of keys) { + let item: typeof folderList1[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('onRootDropTable2')(e); + let processedItems = await itemProcessor(e.items, acceptedDragTypes); + list2.prepend(...processedItems); + }, + onItemDrop: async (e) => { + let { + items, + target, + isInternal, + dropOperation + } = e; + action('onItemDropTable2')(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('onDragEndTable2')(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 ( +
+ + + {column => {column.name}} + + + {item => ( + + {(column) => { + return {item[column.id]}; + }} + + )} + + + + + {column => {column.name}} + + + {item => ( + + {(column) => { + return {item[column.id]}; + }} + + )} + + +
+ ); +} + +export const DragBetweenTables: StoryObj = { + render: (args) => , + name: 'Drag between tables' +}; From 375ac9323598f31ca44afa9d9c5da77df1e9642b Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 23 Mar 2026 14:29:54 -0700 Subject: [PATCH 07/15] fix lint, listview drop indicator styling for root and insertion specifically when items are selected and you are trying to do a root or insertion betwen the selected items --- packages/@react-spectrum/s2/src/ListView.tsx | 52 +++++++++++-------- packages/@react-spectrum/s2/src/TableView.tsx | 2 +- .../s2/stories/TableView.stories.tsx | 2 +- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index b7ca7e1bd6e..20fc01ecd1b 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -12,7 +12,7 @@ import {ActionButtonGroupContext} from './ActionButtonGroup'; import {ActionMenuContext} from './ActionMenu'; -import {baseColor, colorMix, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; +import {baseColor, color, colorMix, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; import {Button} from 'react-aria-components/Button'; import {centerBaseline} from './CenterBaseline'; import {Checkbox} from './Checkbox'; @@ -22,8 +22,9 @@ import {Collection} from 'react-aria/private/collections/CollectionBuilder'; import {CollectionRendererContext, DefaultCollectionRenderer} from 'react-aria-components/Collection'; import {ContextValue, DEFAULT_SLOT, Provider, SlotProps, useSlottedContext} from 'react-aria-components/utils'; import {controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; -import {createContext, forwardRef, ReactElement, ReactNode, useContext, useRef} from 'react'; +import {createContext, forwardRef, ReactElement, ReactNode, useContext, useEffect, useRef, useState} from 'react'; import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, ItemDropTarget, LoadingState} from '@react-types/shared'; +import DragHandle from '../ui-icons/DragHandle'; import {DropIndicator} from 'react-aria-components/useDragAndDrop'; import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import { @@ -50,12 +51,12 @@ import {useDOMRef} from './useDOMRef'; import {useFocusRing} from 'react-aria/useFocusRing'; import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; -import {useVisuallyHidden} from 'react-aria/VisuallyHidden'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -import {Virtualizer} from 'react-aria-components/Virtualizer'; +import {useVisuallyHidden} from 'react-aria/VisuallyHidden'; +import {LayoutInfo, Virtualizer} from 'react-aria-components/Virtualizer'; -export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | 'layout' | 'render' | 'keyboardNavigationBehavior' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { +export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | 'layout' | 'render' | 'keyboardNavigationBehavior' | 'orientation' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { /** Spectrum-defined styles, returned by the `style()` macro. */ styles?: StylesPropWithHeight, /** The current loading state of the ListView. */ @@ -118,7 +119,6 @@ const listView = style extends ListLayout { + getDropTargetLayoutInfo(target: ItemDropTarget): LayoutInfo { + let layoutInfo = super.getDropTargetLayoutInfo(target); + layoutInfo.zIndex = 1; + return layoutInfo; + } +} + /** * A ListView displays a list of interactive items, and allows a user to navigate, select, or perform an action. */ @@ -258,7 +268,7 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li className={(props.UNSAFE_className || '') + listViewWrapper(null, props.styles)} style={props.UNSAFE_style}> ['move', 'copy'], - shouldAcceptItemDrop: (target) => !!list1.getItem(target.key)?.childNodes, + shouldAcceptItemDrop: (target) => !!list1.getItem(target.key)?.childNodes }); // table 2 should allow reordering, on folder drops, and on root drops From 657338496778405dc976aa734654599009bef7b2 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 23 Mar 2026 15:25:16 -0700 Subject: [PATCH 08/15] export ListViewDragPreview so people can customize it --- packages/@react-spectrum/s2/exports/index.ts | 4 +- packages/@react-spectrum/s2/src/ListView.tsx | 19 +++++--- .../s2/stories/ListView.stories.tsx | 43 +++++++++++++++---- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/packages/@react-spectrum/s2/exports/index.ts b/packages/@react-spectrum/s2/exports/index.ts index a52502c85e6..149fc709a03 100644 --- a/packages/@react-spectrum/s2/exports/index.ts +++ b/packages/@react-spectrum/s2/exports/index.ts @@ -56,7 +56,7 @@ export {Image, ImageContext} from '../src/Image'; export {ImageCoordinator} from '../src/ImageCoordinator'; export {InlineAlert, InlineAlertContext} from '../src/InlineAlert'; export {Link, LinkContext} from '../src/Link'; -export {ListView, ListViewContext, ListViewItem} from '../src/ListView'; +export {ListView, ListViewContext, ListViewItem, ListViewDragPreview} from '../src/ListView'; export {MenuItem, MenuTrigger, Menu, MenuSection, SubmenuTrigger, UnavailableMenuItemTrigger, MenuContext} from '../src/Menu'; export {Meter, MeterContext} from '../src/Meter'; export {NotificationBadge, NotificationBadgeContext} from '../src/NotificationBadge'; @@ -144,7 +144,7 @@ export type {InlineAlertProps} from '../src/InlineAlert'; export type {ImageProps} from '../src/Image'; export type {ImageCoordinatorProps} from '../src/ImageCoordinator'; export type {LinkProps} from '../src/Link'; -export type {ListViewProps, ListViewItemProps} from '../src/ListView'; +export type {ListViewProps, ListViewItemProps, ListViewDragPreviewProps} from '../src/ListView'; export type {MenuTriggerProps, MenuProps, MenuItemProps, MenuSectionProps, SubmenuTriggerProps, UnavailableMenuItemTriggerProps} from '../src/Menu'; export type {MeterProps} from '../src/Meter'; export type {NotificationBadgeProps} from '../src/NotificationBadge'; diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 20fc01ecd1b..bb26eab3506 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -23,7 +23,7 @@ import {CollectionRendererContext, DefaultCollectionRenderer} from 'react-aria-c import {ContextValue, DEFAULT_SLOT, Provider, SlotProps, useSlottedContext} from 'react-aria-components/utils'; import {controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {createContext, forwardRef, ReactElement, ReactNode, useContext, useEffect, useRef, useState} from 'react'; -import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, ItemDropTarget, LoadingState} from '@react-types/shared'; +import {DOMProps, DOMRef, DOMRefValue, DragItem, forwardRefType, GlobalDOMAttributes, ItemDropTarget, LoadingState} from '@react-types/shared'; import DragHandle from '../ui-icons/DragHandle'; import {DropIndicator} from 'react-aria-components/useDragAndDrop'; import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; @@ -361,6 +361,8 @@ const listitem = style({ 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 .' @@ -977,12 +978,18 @@ function isLastItem(id: Key | undefined, state: ListState) { return state.collection.getLastKey() === id; } +export interface ListViewDragPreviewProps { + /** The currently dragged items, sourced from renderDragPreview. */ + items: DragItem[], + /** The overflow mode to be applied on the drag preview. */ + overflowMode: ListViewStylesProps['overflowMode'], + /** The contents of the drag preview. Supports the "label", "description", and "icon" slots. */ + children: ReactNode +} + 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(); @@ -1006,7 +1013,7 @@ export function ListViewDragPreview(props) { } }] ]}> - {itemLabel} + {props.children ?? {itemLabel}} {isDraggingMultiple && (
{items.length}
)} diff --git a/packages/@react-spectrum/s2/stories/ListView.stories.tsx b/packages/@react-spectrum/s2/stories/ListView.stories.tsx index 9bad524ec42..59fab0f3022 100644 --- a/packages/@react-spectrum/s2/stories/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx @@ -28,7 +28,7 @@ import FolderOpen from '../spectrum-illustrations/linear/FolderOpen'; import {IllustratedMessage} from '../src/IllustratedMessage'; import {Image} from '../src/Image'; import {Key} from '@react-types/shared'; -import {ListView, ListViewItem} from '../src/ListView'; +import {ListView, ListViewDragPreview, ListViewItem} from '../src/ListView'; import {MenuItem} from '../src/Menu'; import type {Meta, StoryObj} from '@storybook/react'; import {ReactNode, useState} from 'react'; @@ -619,22 +619,45 @@ let reorderItems: Item[] = [ {id: 'o', name: 'Really really really really really long name', type: 'file'} ]; +function CustomDragPreview(props) { + let {items, parentList} = props; + let id = items[0].id; + let item = parentList.getItem(id); + return ( + + {item.name} + {item.type === 'folder' && + <> + + {items.childNodes && {`contains ${item.childNodes.length} dropped item(s)`}} + + } + {item.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 ?? ''})); - }, + getItems: (keys) => [...keys].map(key => { + let item = list.getItem(key)!; + return { + id: item.id.toString(), + 'text/plain': item?.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); } - } + }, + renderDragPreview: (items) => }); return ( @@ -643,7 +666,7 @@ function ReorderExample(props) { items={list.items} dragAndDropHooks={dragAndDropHooks} {...props}> - {(item: Item) => ( + {(item: any) => ( {item.type === 'folder' ? : } {item.name} @@ -713,6 +736,7 @@ function BetweenLists(props) { getItems: (keys) => [...keys].map(key => { let item = list1.getItem(key)!; return { + id: item.id, [`${item.type}`]: JSON.stringify(item), 'text/plain': item.name }; @@ -773,7 +797,8 @@ function BetweenLists(props) { } }, getAllowedDropOperations: () => ['move', 'copy'], - shouldAcceptItemDrop: (target) => !!list1.getItem(target.key)!.childNodes + shouldAcceptItemDrop: (target) => !!list1.getItem(target.key)!.childNodes, + renderDragPreview: (items) => }); // List 2 should allow reordering, on folder drops, and on root drops @@ -782,6 +807,7 @@ function BetweenLists(props) { let item = list2.getItem(key)!; let dragItem = {}; let itemString = JSON.stringify(item); + dragItem['id'] = item.id; dragItem[`${item.type}`] = itemString; if (item.type !== 'unique_type') { dragItem['text/plain'] = item.name; @@ -870,7 +896,8 @@ function BetweenLists(props) { } }, getAllowedDropOperations: () => ['move', 'copy'], - shouldAcceptItemDrop: (target) => !!list2.getItem(target.key)!.childNodes + shouldAcceptItemDrop: (target) => !!list2.getItem(target.key)!.childNodes, + renderDragPreview: (items) => }); return ( From 6835682d4b7657f128617bfc04178aa5e6ed46cc Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 23 Mar 2026 15:57:04 -0700 Subject: [PATCH 09/15] table drag preview and zindex update on indicator --- packages/@react-spectrum/s2/exports/index.ts | 4 +- packages/@react-spectrum/s2/src/ListView.tsx | 17 ++-- packages/@react-spectrum/s2/src/TableView.tsx | 84 ++++++++++++++++++- .../s2/stories/ListView.stories.tsx | 2 +- .../s2/stories/TableView.stories.tsx | 28 ++++++- 5 files changed, 117 insertions(+), 18 deletions(-) diff --git a/packages/@react-spectrum/s2/exports/index.ts b/packages/@react-spectrum/s2/exports/index.ts index 149fc709a03..2ef076767c1 100644 --- a/packages/@react-spectrum/s2/exports/index.ts +++ b/packages/@react-spectrum/s2/exports/index.ts @@ -77,7 +77,7 @@ export {Skeleton, useIsSkeleton} from '../src/Skeleton'; export {SkeletonCollection} from '../src/SkeletonCollection'; export {StatusLight, StatusLightContext} from '../src/StatusLight'; export {Switch, SwitchContext} from '../src/Switch'; -export {TableView, TableHeader, TableBody, Row, Cell, Column, TableContext, EditableCell} from '../src/TableView'; +export {TableView, TableHeader, TableBody, Row, Cell, Column, TableContext, EditableCell, TableViewDragPreview} from '../src/TableView'; export {Tabs, TabList, Tab, TabPanel, TabsContext} from '../src/Tabs'; export {TagGroup, Tag, TagGroupContext} from '../src/TagGroup'; export {TextArea, TextField, TextAreaContext, TextFieldContext} from '../src/TextField'; @@ -164,7 +164,7 @@ export type {SkeletonProps} from '../src/Skeleton'; export type {SkeletonCollectionProps} from '../src/SkeletonCollection'; export type {StatusLightProps} from '../src/StatusLight'; export type {SwitchProps} from '../src/Switch'; -export type {TableViewProps, TableHeaderProps, TableBodyProps, RowProps, CellProps, ColumnProps} from '../src/TableView'; +export type {TableViewProps, TableHeaderProps, TableBodyProps, RowProps, CellProps, ColumnProps, TableDragPreviewProps} from '../src/TableView'; export type {TabsProps, TabProps, TabListProps, TabPanelProps} from '../src/Tabs'; export type {TagGroupProps, TagProps} from '../src/TagGroup'; export type {TextFieldProps, TextAreaProps, TextFieldRef} from '../src/TextField'; diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index bb26eab3506..a95adc11332 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -781,11 +781,11 @@ let dragButton = style<{isFocusVisible?: boolean}>({ } }); -let dragPreviewWrapper = style({ +export let dragPreviewWrapper = style({ position: 'relative' }); -let dragPreviewCardBack = style({ +export let dragPreviewCardBack = style({ position: 'absolute', zIndex: -1, top: 4, @@ -799,7 +799,7 @@ let dragPreviewCardBack = style({ backgroundColor: 'gray-25' }); -let dragPreviewCard = style<{scale?: 'medium' | 'large'}>({ +export let dragPreviewCard = style<{scale?: 'medium' | 'large'}>({ boxSizing: 'border-box', paddingX: 0, paddingY: 8, @@ -827,7 +827,7 @@ let dragPreviewCard = style<{scale?: 'medium' | 'large'}>({ borderColor: 'blue-900' }); -let dragPreviewBadge = style({ +export let dragPreviewBadge = style({ gridArea: 'badge', alignSelf: 'center', paddingX: 8, @@ -983,11 +983,14 @@ export interface ListViewDragPreviewProps { items: DragItem[], /** The overflow mode to be applied on the drag preview. */ overflowMode: ListViewStylesProps['overflowMode'], - /** The contents of the drag preview. Supports the "label", "description", and "icon" slots. */ - children: ReactNode + /** + * The contents of the drag preview. Supports the "label", "description", and "icon" slots. + * If no children are provided, defaults to the first drag item's plain text content. + */ + children?: ReactNode } -export function ListViewDragPreview(props) { +export function ListViewDragPreview(props: ListViewDragPreviewProps) { let {items, overflowMode} = props; let isDraggingMultiple = items.length > 1; let itemLabel = items[0]?.['text/plain'] ?? ''; diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 5fdfcbd1c39..ae50774b4c7 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -52,14 +52,15 @@ import {controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} fr import {css} from '../style/style-macro' with {type: 'macro'}; import {CustomDialog} from './CustomDialog'; import {DialogContainer} from './DialogContainer'; -import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, ItemDropTarget, LinkDOMProps, LoadingState, Node} from '@react-types/shared'; +import {DOMProps, DOMRef, DOMRefValue, DragItem, forwardRefType, GlobalDOMAttributes, ItemDropTarget, LinkDOMProps, LoadingState, Node} from '@react-types/shared'; import DragHandle from '../ui-icons/DragHandle'; +import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import {Form} from 'react-aria-components/Form'; import {getActiveElement, isFocusWithin, nodeContains} from 'react-aria/private/utils/shadowdom/DOMFunctions'; import {getOwnerDocument} from 'react-aria/private/utils/domHelpers'; import {GridNode} from 'react-stately/private/grid/GridCollection'; import {IconContext} from './Icon'; -import {InsertionIndicator, ListViewDragPreview} from './ListView'; +import {dragPreviewBadge, dragPreviewCardBack, dragPreviewWrapper, InsertionIndicator, label} from './ListView'; // @ts-ignore import intlMessages from '../intl/*.json'; import {Key} from '@react-types/shared'; @@ -73,6 +74,7 @@ import {CheckboxContext as RACCheckboxContext} from 'react-aria-components/Check import {Popover as RACPopover} from 'react-aria-components/Popover'; import React, {createContext, CSSProperties, FormEvent, FormHTMLAttributes, ForwardedRef, forwardRef, ReactElement, ReactNode, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {Rect} from 'react-stately/private/virtualizer/Rect'; +import {Text, TextContext} from './Content'; import SortDownArrow from '../s2wf-icons/S2_Icon_SortDown_20_N.svg'; import SortUpArrow from '../s2wf-icons/S2_Icon_SortUp_20_N.svg'; import {Button as SpectrumButton} from './Button'; @@ -87,7 +89,7 @@ import {useObjectRef} from 'react-aria/useObjectRef'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; import {useVisuallyHidden} from 'react-aria/VisuallyHidden'; -import {Virtualizer} from 'react-aria-components/Virtualizer'; +import {LayoutInfo, Virtualizer} from 'react-aria-components/Virtualizer'; import {VisuallyHidden} from 'react-aria/VisuallyHidden'; interface S2TableProps { @@ -208,6 +210,74 @@ const table = style({ + boxSizing: 'border-box', + paddingX: 0, + paddingY: 8, + backgroundColor: 'gray-25', + color: baseColor('neutral'), + position: 'relative', + display: 'grid', + gridTemplateAreas: [ + '. label badge .' + ], + gridTemplateColumns: [edgeToText(40), 'minmax(0, 1fr)', 'auto', edgeToText(40)], + gridTemplateRows: 'auto', + alignItems: 'baseline', + minHeight: { + default: 40, + scale: { + large: 50 + } + }, + width: 200, + borderRadius: 'default', + borderWidth: 1, + borderStyle: 'solid', + borderColor: 'blue-900' +}); + +export interface TableDragPreviewProps { + /** The currently dragged items, sourced from renderDragPreview. */ + items: DragItem[], + /** The overflow mode to be applied on the drag preview. */ + overflowMode: S2TableProps['overflowMode'], + /** + * The contents of the drag preview. Supports the default text slot. + * If no children are provided, defaults to the first drag item's plain text content. + */ + children?: ReactNode +} + +export function TableViewDragPreview(props: TableDragPreviewProps) { + let {items, overflowMode} = props; + let isDraggingMultiple = items.length > 1; + let itemLabel = items[0]?.['text/plain'] ?? ''; + let scale = useScale(); + + return ( +
+ {isDraggingMultiple &&
} +
+ + {props.children ?? {itemLabel}} + {isDraggingMultiple && ( +
{items.length}
+ )} +
+
+
+ ); +} + // component-height-100 const DEFAULT_HEADER_HEIGHT = { medium: 32, @@ -302,6 +372,12 @@ export class S2TableLayout extends TableLayout { layoutNode.layoutInfo.allowOverflow = true; return layoutNode; } + + getDropTargetLayoutInfo(target: ItemDropTarget): LayoutInfo { + let layoutInfo = super.getDropTargetLayoutInfo(target); + layoutInfo.zIndex = 1; + return layoutInfo; + } } export const TableContext = createContext, DOMRefValue>>(null); @@ -330,7 +406,7 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re } = props; if (dragAndDropHooks && dragAndDropHooks.renderDragPreview == null) { - dragAndDropHooks.renderDragPreview = (items) => ; + dragAndDropHooks.renderDragPreview = (items) => ; } if (dragAndDropHooks) { diff --git a/packages/@react-spectrum/s2/stories/ListView.stories.tsx b/packages/@react-spectrum/s2/stories/ListView.stories.tsx index 59fab0f3022..346711c5644 100644 --- a/packages/@react-spectrum/s2/stories/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx @@ -801,7 +801,7 @@ function BetweenLists(props) { renderDragPreview: (items) => }); -// List 2 should allow reordering, on folder drops, and on root drops + // 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)!; diff --git a/packages/@react-spectrum/s2/stories/TableView.stories.tsx b/packages/@react-spectrum/s2/stories/TableView.stories.tsx index 07bb448df7c..65af393ca77 100644 --- a/packages/@react-spectrum/s2/stories/TableView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TableView.stories.tsx @@ -22,12 +22,15 @@ import { TableBody, TableHeader, TableView, + TableViewDragPreview, TableViewProps } from '../src/TableView'; import {Collection} from 'react-aria/private/collections/CollectionBuilder'; import {Content, Heading, Text} from '../src/Content'; import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; +import File from '../s2wf-icons/S2_Icon_File_20_N.svg'; import Filter from '../s2wf-icons/S2_Icon_Filter_20_N.svg'; +import Folder from '../s2wf-icons/S2_Icon_Folder_20_N.svg'; import FolderOpen from '../spectrum-illustrations/linear/FolderOpen'; import {IllustratedMessage} from '../src/IllustratedMessage'; import {Key} from '@react-types/shared'; @@ -1887,6 +1890,17 @@ export const TableWithNestedRowsAndInlineEditing: StoryObj = { render: (args) => }; +function CustomDragPreview(props) { + let {items, parentList} = props; + let id = items[0].id; + let item = parentList.getItem(id); + return ( + + {`${item.name} (${item.type})`} + + ); +} + let folderList1 = [ {id: '1', type: 'file', name: 'Adobe Photoshop'}, {id: '2', type: 'file', name: 'Adobe XD'}, @@ -1920,6 +1934,7 @@ function ReorderableTableExample(props) { getItems: (keys) => [...keys].map(key => { let item = list.getItem(key)!; return { + id: item.id, [`${item.type}`]: JSON.stringify(item), 'text/plain': item.name }; @@ -1955,7 +1970,8 @@ function ReorderableTableExample(props) { } } }, - acceptedDragTypes + acceptedDragTypes, + renderDragPreview: (items) => }); return ( @@ -2016,6 +2032,7 @@ function BetweenTables(props) { getItems: (keys) => [...keys].map(key => { let item = list1.getItem(key)!; return { + id: item.id, [`${item.type}`]: JSON.stringify(item), 'text/plain': item.name }; @@ -2070,7 +2087,8 @@ function BetweenTables(props) { } }, getAllowedDropOperations: () => ['move', 'copy'], - shouldAcceptItemDrop: (target) => !!list1.getItem(target.key)?.childNodes + shouldAcceptItemDrop: (target) => !!list1.getItem(target.key)?.childNodes, + renderDragPreview: (items) => }); // table 2 should allow reordering, on folder drops, and on root drops @@ -2079,9 +2097,10 @@ function BetweenTables(props) { let item = list2.getItem(key)!; let dragItem = {}; let itemString = JSON.stringify(item); + dragItem['id'] = item.id; dragItem[`${item.type}`] = itemString; if (item.type !== 'unique_type') { - dragItem['text/plain'] = itemString; + dragItem['text/plain'] = item.name; } return dragItem; @@ -2167,7 +2186,8 @@ function BetweenTables(props) { } }, getAllowedDropOperations: () => ['move', 'copy'], - shouldAcceptItemDrop: (target) => !!list2.getItem(target.key)?.childNodes + shouldAcceptItemDrop: (target) => !!list2.getItem(target.key)?.childNodes, + renderDragPreview: (items) => }); From 9f12eb760a9767c7fcaef832fd19cc19b2fbbbe4 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 23 Mar 2026 17:14:21 -0700 Subject: [PATCH 10/15] fix drag cell styles but stuck on the visually hidden --- packages/@react-spectrum/s2/src/TableView.tsx | 60 ++++++++++--------- packages/react-aria-components/src/Table.tsx | 7 ++- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index ae50774b4c7..3fe26d7eb19 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -204,8 +204,9 @@ const table = style @@ -1031,7 +1030,7 @@ export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function className={tableHeader}> {allowsDragging && ( // @ts-ignore - + {/* TODO: intl, need to grab for other locales */} {({isFocusVisible}) => ( <> @@ -1150,16 +1149,16 @@ const checkboxCellStyle = style({ backgroundColor: '--rowBackgroundColor' }); -// const dragCellStyle = style({ -// ...commonCellStyles, -// ...stickyCell, -// paddingStart: 12, -// paddingEnd: 4, -// alignContent: 'center', -// height: 'calc(100% - 1px)', -// borderBottomWidth: 0, -// backgroundColor: '--rowBackgroundColor' -// }); +const dragCellStyle = style({ + ...commonCellStyles, + ...stickyCell, + paddingStart: 4, + paddingEnd: 4, + alignContent: 'center', + height: 'calc(100% - 1px)', + borderBottomWidth: 0, + backgroundColor: '--rowBackgroundColor' +}); const dragButton = style({ alignItems: 'center', @@ -1186,6 +1185,21 @@ const dragButton = style({ } }); +const visuallyHidden = css(` + &:not(:is([role="row"][data-focus-visible-within] *)) { + border: 0; + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; + } +`); + const cellContent = style({ truncate: true, whiteSpace: { @@ -1757,16 +1771,6 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row add data-focusvisible withing to RAC row (render prop already exists) - // have cell render props also have row focus within provided to it - // have cell also have table row render props alongside cell render props - isFocusVisible: isFocusVisibleWithin, - focusProps: focusWithinProps - } = useFocusRing({within: true}); return ( ( {allowsDragging && ( - // TODO: this isn't being sticky when selection isn't enabled // @ts-ignore - + {/* TODO: check if this isDisabled is enough, should be if selection and action is disabled */} {/* can try to move this into cell perhaps but focusvisible needs to be set on the row and we'd need to only render it once via something similar to isTreeCOlumn */} {!otherProps.isDisabled && ( ) diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index e8b352c5871..5a22a94518a 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -260,7 +260,7 @@ class TableCollection extends BaseCollection implements ITableCollection Date: Tue, 24 Mar 2026 10:11:34 -0700 Subject: [PATCH 11/15] fix visually hidden for drag handle and begin root drop styles --- packages/@react-spectrum/s2/src/TableView.tsx | 94 +++++++++++-------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 3fe26d7eb19..9c22bd4c4ce 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -150,6 +150,7 @@ const tableWrapper = style({ overflow: 'clip' }, getAllowedOverrides({height: true})); +const dropTargetBackground = colorMix('gray-25', 'blue-900', 10); const table = style({ width: 'full', height: 'full', @@ -163,32 +164,27 @@ const table = style({ height: 'full', position: 'relative', boxSizing: 'border-box', - backgroundColor: '--rowBackgroundColor', + backgroundColor: { + default: '--rowBackgroundColor', + ':is([role="grid"][data-drop-target] *)': { + // TODO: these will need to have a blend, selected should be a bit darker, and teh default should be a bit darker than the background + // color on the body. Will need to apply the same color scheme to the checkbox cell + default: 'transparent', + isSelected: 'transparent' + }, + }, '--rowBackgroundColor': { type: 'backgroundColor', value: rowBackgroundColor @@ -1727,6 +1739,8 @@ const row = style({ // isFocusVisible: 'solid' // } // }, + // TODO: this literally runs into the same problem as noted above for the focusedColors experience when the user targets the + // row for a "on" drop operation outlineStyle: { default: 'none', isDropTarget: 'solid' @@ -1792,7 +1806,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row dragButton({isFocusVisible}) + visuallyHidden}> + className={dragButton}> ) From 1b8f434bb73ad0053fb0ea744e550fc2770e8147 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 24 Mar 2026 11:37:38 -0700 Subject: [PATCH 12/15] add root drop styling --- packages/@react-spectrum/s2/src/TableView.tsx | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 9c22bd4c4ce..28dd2fb5d93 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -166,9 +166,8 @@ const table = style({ height: 'full', position: 'relative', boxSizing: 'border-box', - backgroundColor: { - default: '--rowBackgroundColor', - ':is([role="grid"][data-drop-target] *)': { - // TODO: these will need to have a blend, selected should be a bit darker, and teh default should be a bit darker than the background - // color on the body. Will need to apply the same color scheme to the checkbox cell - default: 'transparent', - isSelected: 'transparent' - }, - }, + backgroundColor: '--rowBackgroundColor', '--rowBackgroundColor': { type: 'backgroundColor', value: rowBackgroundColor From a6a22f2adde4fed61cd176e1b0d84e28be353cbe Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 24 Mar 2026 14:34:31 -0700 Subject: [PATCH 13/15] extend row drop target outline style conditionall in sticky cells so it doesnt look cut off also does the same for HCM for general row focus --- packages/@react-spectrum/s2/src/ListView.tsx | 38 ++++++---- packages/@react-spectrum/s2/src/TableView.tsx | 74 +++++++++---------- 2 files changed, 59 insertions(+), 53 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index a95adc11332..fdf1699b161 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -306,6 +306,8 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li const selectedBackground = colorMix('gray-25', 'gray-900', 7); const selectedActiveBackground = colorMix('gray-25', 'gray-900', 10); +// TODO: removed the background color in HCM for highlight selection since it made it hard to see the focus +// ring of the drag button, this matches v3 anyways. thoughts? const listitem = style({ @@ -1134,9 +1135,26 @@ const stickyCell = { backgroundColor: 'gray-25' } as const; +// Bit gross but this is needed because the sticky cells currently cover/partially cover styles that the row applies so that +// they don't appear when the table is scrolled. The below basically just continues the inset outline that the row has when +// it is focused as a drop target +const rowDropTargetStickyOutline = { + boxShadow: { + default: 'none', + ':is([role="row"][data-drop-target] *)': { + default: `[inset 0 2px 0 0 ${color('blue-800')}, inset 0 -1px 0 0 ${color('blue-800')}]`, + forcedColors: '[inset 0 2px 0 0 Highlight, inset 0 -1px 0 0 Highlight]' + }, + ':is([role="row"][data-focus-visible] *)': { + forcedColors: '[inset 0 2px 0 0 Highlight, inset 0 -1px 0 0 Highlight]' + } + } +} as const; + const checkboxCellStyle = style({ ...commonCellStyles, ...stickyCell, + ...rowDropTargetStickyOutline, paddingStart: 16, alignContent: 'center', height: 'calc(100% - 1px)', @@ -1147,6 +1165,7 @@ const checkboxCellStyle = style({ const dragCellStyle = style({ ...commonCellStyles, ...stickyCell, + ...rowDropTargetStickyOutline, paddingStart: 4, paddingEnd: 4, alignContent: 'center', @@ -1726,47 +1745,28 @@ const row = style({ forcedColors: 'Highlight' } }, - // TODO: outline here is to emulate v3 forcedColors experience but runs into the same problem where the sticky column covers the outline - // This doesn't quite work because it gets cut off by the checkbox cell background masking element, figure out another way. Could shrink the checkbox cell's content even more - // and offset it by margin top but that messes up the checkbox centering a bit - // outlineWidth: { - // forcedColors: { - // isFocusVisible: 2 - // } - // }, - // outlineOffset: { - // forcedColors: { - // isFocusVisible: -1 - // } - // }, - // outlineColor: { - // forcedColors: { - // isFocusVisible: 'ButtonBorder' - // } - // }, - // outlineStyle: { - // default: 'none', - // forcedColors: { - // isFocusVisible: 'solid' - // } - // }, - // TODO: this literally runs into the same problem as noted above for the focusedColors experience when the user targets the - // row for a "on" drop operation outlineStyle: { default: 'none', - isDropTarget: 'solid' + isDropTarget: 'solid', + forcedColors: { + isFocusVisible: 'solid' + } }, outlineWidth: { - isDropTarget: 2 + isDropTarget: 2, + forcedColors: { + isFocusVisible: 2 + } }, outlineOffset: { - isDropTarget: -2 + isDropTarget: -2, + forcedColors: { + isFocusVisible: -2 + } }, outlineColor: { - isDropTarget: { - default: 'blue-800', - forcedColors: 'Highlight' - } + isDropTarget: 'blue-800', + forcedColors: 'Highlight' }, borderTopWidth: 0, borderBottomWidth: 1, @@ -1806,7 +1806,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row row({ ...renderProps, ...tableVisualOptions - }) + (renderProps.isFocusVisible ? ' ' + css('&:before { content: ""; display: inline-block; position: sticky; inset-inline-start: 0; width: 3px; height: 100%; margin-inline-end: -3px; margin-block-end: 1px; z-index: 3; background-color: var(--rowFocusIndicatorColor)') : '')} + }) + (renderProps.isFocusVisible || renderProps.isDropTarget ? ' ' + css('&:before { content: ""; display: inline-block; position: sticky; inset-inline-start: 0; width: 3px; height: 100%; margin-inline-end: -3px; margin-block-end: 1px; z-index: 3; background-color: var(--rowFocusIndicatorColor)') : '')} {...otherProps}> {allowsDragging && ( // @ts-ignore From 8383fb0adf4cf97737aa979fd264f153e937b45d Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 24 Mar 2026 16:07:53 -0700 Subject: [PATCH 14/15] fix various disabledBehavior cases and add translations --- packages/@react-spectrum/s2/intl/ar-AE.json | 1 + packages/@react-spectrum/s2/intl/bg-BG.json | 1 + packages/@react-spectrum/s2/intl/cs-CZ.json | 1 + packages/@react-spectrum/s2/intl/da-DK.json | 1 + packages/@react-spectrum/s2/intl/de-DE.json | 1 + packages/@react-spectrum/s2/intl/el-GR.json | 1 + packages/@react-spectrum/s2/intl/es-ES.json | 1 + packages/@react-spectrum/s2/intl/et-EE.json | 1 + packages/@react-spectrum/s2/intl/fi-FI.json | 1 + packages/@react-spectrum/s2/intl/fr-FR.json | 1 + packages/@react-spectrum/s2/intl/he-IL.json | 1 + packages/@react-spectrum/s2/intl/hr-HR.json | 1 + packages/@react-spectrum/s2/intl/hu-HU.json | 1 + packages/@react-spectrum/s2/intl/it-IT.json | 1 + packages/@react-spectrum/s2/intl/ja-JP.json | 1 + packages/@react-spectrum/s2/intl/ko-KR.json | 1 + packages/@react-spectrum/s2/intl/lt-LT.json | 1 + packages/@react-spectrum/s2/intl/lv-LV.json | 1 + packages/@react-spectrum/s2/intl/nb-NO.json | 1 + packages/@react-spectrum/s2/intl/nl-NL.json | 1 + packages/@react-spectrum/s2/intl/pl-PL.json | 1 + packages/@react-spectrum/s2/intl/pt-BR.json | 1 + packages/@react-spectrum/s2/intl/pt-PT.json | 1 + packages/@react-spectrum/s2/intl/ro-RO.json | 1 + packages/@react-spectrum/s2/intl/ru-RU.json | 1 + packages/@react-spectrum/s2/intl/sk-SK.json | 1 + packages/@react-spectrum/s2/intl/sl-SI.json | 1 + packages/@react-spectrum/s2/intl/sr-SP.json | 1 + packages/@react-spectrum/s2/intl/sv-SE.json | 1 + packages/@react-spectrum/s2/intl/tr-TR.json | 1 + packages/@react-spectrum/s2/intl/uk-UA.json | 1 + packages/@react-spectrum/s2/intl/zh-CN.json | 1 + packages/@react-spectrum/s2/intl/zh-TW.json | 1 + packages/@react-spectrum/s2/src/ListView.tsx | 26 +++++++-------- packages/@react-spectrum/s2/src/TableView.tsx | 33 ++++++++----------- .../s2/stories/ListView.stories.tsx | 3 ++ .../s2/stories/TableView.stories.tsx | 6 ++-- 37 files changed, 65 insertions(+), 36 deletions(-) diff --git a/packages/@react-spectrum/s2/intl/ar-AE.json b/packages/@react-spectrum/s2/intl/ar-AE.json index a66391647e6..fd9c15149d8 100644 --- a/packages/@react-spectrum/s2/intl/ar-AE.json +++ b/packages/@react-spectrum/s2/intl/ar-AE.json @@ -31,6 +31,7 @@ "slider.maximum": "أقصى", "slider.minimum": "أدنى", "table.cancel": "إلغاء", + "table.drag": "سحب", "table.editCell": "تعديل الخلية", "table.loading": "جارٍ التحميل...", "table.loadingMore": "جارٍ تحميل المزيد...", diff --git a/packages/@react-spectrum/s2/intl/bg-BG.json b/packages/@react-spectrum/s2/intl/bg-BG.json index c70ca77f057..e8c88d92877 100644 --- a/packages/@react-spectrum/s2/intl/bg-BG.json +++ b/packages/@react-spectrum/s2/intl/bg-BG.json @@ -31,6 +31,7 @@ "slider.maximum": "Максимум", "slider.minimum": "Минимум", "table.cancel": "Отказ", + "table.drag": "Плъзнете", "table.editCell": "Редактиране на клетка", "table.loading": "Зареждане...", "table.loadingMore": "Зареждане на още...", diff --git a/packages/@react-spectrum/s2/intl/cs-CZ.json b/packages/@react-spectrum/s2/intl/cs-CZ.json index 60ccac47a4c..e16ea158b89 100644 --- a/packages/@react-spectrum/s2/intl/cs-CZ.json +++ b/packages/@react-spectrum/s2/intl/cs-CZ.json @@ -31,6 +31,7 @@ "slider.maximum": "Maximum", "slider.minimum": "Minimum", "table.cancel": "Zrušit", + "table.drag": "Přetáhnout", "table.editCell": "Upravit buňku", "table.loading": "Načítání...", "table.loadingMore": "Načítání dalších...", diff --git a/packages/@react-spectrum/s2/intl/da-DK.json b/packages/@react-spectrum/s2/intl/da-DK.json index 005336329b0..7c32d0692ee 100644 --- a/packages/@react-spectrum/s2/intl/da-DK.json +++ b/packages/@react-spectrum/s2/intl/da-DK.json @@ -31,6 +31,7 @@ "slider.maximum": "Maksimum", "slider.minimum": "Minimum", "table.cancel": "Annuller", + "table.drag": "Træk", "table.editCell": "Rediger celle", "table.loading": "Indlæser...", "table.loadingMore": "Indlæser flere...", diff --git a/packages/@react-spectrum/s2/intl/de-DE.json b/packages/@react-spectrum/s2/intl/de-DE.json index 8e696210662..331ba998331 100644 --- a/packages/@react-spectrum/s2/intl/de-DE.json +++ b/packages/@react-spectrum/s2/intl/de-DE.json @@ -31,6 +31,7 @@ "slider.maximum": "Maximum", "slider.minimum": "Minimum", "table.cancel": "Abbrechen", + "table.drag": "Ziehen", "table.editCell": "Zelle bearbeiten", "table.loading": "Laden...", "table.loadingMore": "Mehr laden ...", diff --git a/packages/@react-spectrum/s2/intl/el-GR.json b/packages/@react-spectrum/s2/intl/el-GR.json index f4f7d60e37a..5a4bbe0f93c 100644 --- a/packages/@react-spectrum/s2/intl/el-GR.json +++ b/packages/@react-spectrum/s2/intl/el-GR.json @@ -31,6 +31,7 @@ "slider.maximum": "Μέγιστο", "slider.minimum": "Ελάχιστο", "table.cancel": "Ακύρωση", + "table.drag": "Μεταφορά", "table.editCell": "Επεξεργασία κελιού", "table.loading": "Φόρτωση...", "table.loadingMore": "Φόρτωση περισσότερων...", diff --git a/packages/@react-spectrum/s2/intl/es-ES.json b/packages/@react-spectrum/s2/intl/es-ES.json index 6b6551ee497..04cacae4880 100644 --- a/packages/@react-spectrum/s2/intl/es-ES.json +++ b/packages/@react-spectrum/s2/intl/es-ES.json @@ -31,6 +31,7 @@ "slider.maximum": "Máximo", "slider.minimum": "Mínimo", "table.cancel": "Cancelar", + "table.drag": "Arrastrar", "table.editCell": "Editar celda", "table.loading": "Cargando…", "table.loadingMore": "Cargando más…", diff --git a/packages/@react-spectrum/s2/intl/et-EE.json b/packages/@react-spectrum/s2/intl/et-EE.json index a9ac34575f6..c5ef8d69641 100644 --- a/packages/@react-spectrum/s2/intl/et-EE.json +++ b/packages/@react-spectrum/s2/intl/et-EE.json @@ -31,6 +31,7 @@ "slider.maximum": "Maksimaalne", "slider.minimum": "Minimaalne", "table.cancel": "Tühista", + "table.drag": "Lohista", "table.editCell": "Muuda lahtrit", "table.loading": "Laadimine...", "table.loadingMore": "Laadi rohkem...", diff --git a/packages/@react-spectrum/s2/intl/fi-FI.json b/packages/@react-spectrum/s2/intl/fi-FI.json index 2abfc9a4c84..06a7af7a2bd 100644 --- a/packages/@react-spectrum/s2/intl/fi-FI.json +++ b/packages/@react-spectrum/s2/intl/fi-FI.json @@ -31,6 +31,7 @@ "slider.maximum": "Maksimi", "slider.minimum": "Minimi", "table.cancel": "Peruuta", + "table.drag": "Vedä", "table.editCell": "Muokkaa solua", "table.loading": "Ladataan…", "table.loadingMore": "Ladataan lisää…", diff --git a/packages/@react-spectrum/s2/intl/fr-FR.json b/packages/@react-spectrum/s2/intl/fr-FR.json index afef2ad5752..67907cc37ce 100644 --- a/packages/@react-spectrum/s2/intl/fr-FR.json +++ b/packages/@react-spectrum/s2/intl/fr-FR.json @@ -31,6 +31,7 @@ "slider.maximum": "Maximum", "slider.minimum": "Minimum", "table.cancel": "Annuler", + "table.drag": "Faire glisser", "table.editCell": "Modifier la cellule", "table.loading": "Chargement...", "table.loadingMore": "Chargement supplémentaire...", diff --git a/packages/@react-spectrum/s2/intl/he-IL.json b/packages/@react-spectrum/s2/intl/he-IL.json index 9fe25ac115b..4e20ed953c0 100644 --- a/packages/@react-spectrum/s2/intl/he-IL.json +++ b/packages/@react-spectrum/s2/intl/he-IL.json @@ -31,6 +31,7 @@ "slider.maximum": "מקסימום", "slider.minimum": "מינימום", "table.cancel": "ביטול", + "table.drag": "גרור", "table.editCell": "עריכת תא", "table.loading": "טוען...", "table.loadingMore": "טוען עוד...", diff --git a/packages/@react-spectrum/s2/intl/hr-HR.json b/packages/@react-spectrum/s2/intl/hr-HR.json index c566c400924..47c1d6efb7d 100644 --- a/packages/@react-spectrum/s2/intl/hr-HR.json +++ b/packages/@react-spectrum/s2/intl/hr-HR.json @@ -31,6 +31,7 @@ "slider.maximum": "Najviše", "slider.minimum": "Najmanje", "table.cancel": "Poništi", + "table.drag": "Povucite", "table.editCell": "Uredi ćeliju", "table.loading": "Učitavam...", "table.loadingMore": "Učitavam još...", diff --git a/packages/@react-spectrum/s2/intl/hu-HU.json b/packages/@react-spectrum/s2/intl/hu-HU.json index f82e54bec92..96421940d0b 100644 --- a/packages/@react-spectrum/s2/intl/hu-HU.json +++ b/packages/@react-spectrum/s2/intl/hu-HU.json @@ -31,6 +31,7 @@ "slider.maximum": "Maximum", "slider.minimum": "Minimum", "table.cancel": "Mégse", + "table.drag": "Húzás", "table.editCell": "Cella szerkesztése", "table.loading": "Betöltés folyamatban…", "table.loadingMore": "Továbbiak betöltése folyamatban…", diff --git a/packages/@react-spectrum/s2/intl/it-IT.json b/packages/@react-spectrum/s2/intl/it-IT.json index a66e379f5af..f3a63076f15 100644 --- a/packages/@react-spectrum/s2/intl/it-IT.json +++ b/packages/@react-spectrum/s2/intl/it-IT.json @@ -31,6 +31,7 @@ "slider.maximum": "Massimo", "slider.minimum": "Minimo", "table.cancel": "Annulla", + "table.drag": "Trascina", "table.editCell": "Modifica cella", "table.loading": "Caricamento...", "table.loadingMore": "Caricamento altri...", diff --git a/packages/@react-spectrum/s2/intl/ja-JP.json b/packages/@react-spectrum/s2/intl/ja-JP.json index bb06130fef8..7b5481cd72c 100644 --- a/packages/@react-spectrum/s2/intl/ja-JP.json +++ b/packages/@react-spectrum/s2/intl/ja-JP.json @@ -31,6 +31,7 @@ "slider.maximum": "最大", "slider.minimum": "最小", "table.cancel": "キャンセル", + "table.drag": "ドラッグ", "table.editCell": "セルを編集", "table.loading": "読み込み中...", "table.loadingMore": "さらに読み込み中...", diff --git a/packages/@react-spectrum/s2/intl/ko-KR.json b/packages/@react-spectrum/s2/intl/ko-KR.json index e010ac6591c..ed1cdefd539 100644 --- a/packages/@react-spectrum/s2/intl/ko-KR.json +++ b/packages/@react-spectrum/s2/intl/ko-KR.json @@ -31,6 +31,7 @@ "slider.maximum": "최대", "slider.minimum": "최소", "table.cancel": "취소", + "table.drag": "드래그", "table.editCell": "셀 편집", "table.loading": "로드 중…", "table.loadingMore": "추가 로드 중…", diff --git a/packages/@react-spectrum/s2/intl/lt-LT.json b/packages/@react-spectrum/s2/intl/lt-LT.json index e52c74583a6..14a5de70039 100644 --- a/packages/@react-spectrum/s2/intl/lt-LT.json +++ b/packages/@react-spectrum/s2/intl/lt-LT.json @@ -31,6 +31,7 @@ "slider.maximum": "Daugiausia", "slider.minimum": "Mažiausia", "table.cancel": "Atšaukti", + "table.drag": "Vilkti", "table.editCell": "Redaguoti langelį", "table.loading": "Įkeliama...", "table.loadingMore": "Įkeliama daugiau...", diff --git a/packages/@react-spectrum/s2/intl/lv-LV.json b/packages/@react-spectrum/s2/intl/lv-LV.json index 389ea6f8b33..ac009435241 100644 --- a/packages/@react-spectrum/s2/intl/lv-LV.json +++ b/packages/@react-spectrum/s2/intl/lv-LV.json @@ -31,6 +31,7 @@ "slider.maximum": "Maksimālā vērtība", "slider.minimum": "Minimālā vērtība", "table.cancel": "Atcelt", + "table.drag": "Vilkšana", "table.editCell": "Rediģēt šūnu", "table.loading": "Notiek ielāde...", "table.loadingMore": "Tiek ielādēts vēl...", diff --git a/packages/@react-spectrum/s2/intl/nb-NO.json b/packages/@react-spectrum/s2/intl/nb-NO.json index d53f0d8aa59..5a0cac5d422 100644 --- a/packages/@react-spectrum/s2/intl/nb-NO.json +++ b/packages/@react-spectrum/s2/intl/nb-NO.json @@ -31,6 +31,7 @@ "slider.maximum": "Maksimum", "slider.minimum": "Minimum", "table.cancel": "Avbryt", + "table.drag": "Dra", "table.editCell": "Rediger celle", "table.loading": "Laster inn...", "table.loadingMore": "Laster inn flere...", diff --git a/packages/@react-spectrum/s2/intl/nl-NL.json b/packages/@react-spectrum/s2/intl/nl-NL.json index a861126de64..b5efe3224ae 100644 --- a/packages/@react-spectrum/s2/intl/nl-NL.json +++ b/packages/@react-spectrum/s2/intl/nl-NL.json @@ -31,6 +31,7 @@ "slider.maximum": "Maximum", "slider.minimum": "Minimum", "table.cancel": "Annuleren", + "table.drag": "Slepen", "table.editCell": "Cel bewerken", "table.loading": "Laden...", "table.loadingMore": "Meer laden...", diff --git a/packages/@react-spectrum/s2/intl/pl-PL.json b/packages/@react-spectrum/s2/intl/pl-PL.json index 14104c6cbbc..f66771ac302 100644 --- a/packages/@react-spectrum/s2/intl/pl-PL.json +++ b/packages/@react-spectrum/s2/intl/pl-PL.json @@ -31,6 +31,7 @@ "slider.maximum": "Maksimum", "slider.minimum": "Minimum", "table.cancel": "Anuluj", + "table.drag": "Przeciągnij", "table.editCell": "Edytuj komórkę", "table.loading": "Wczytywanie...", "table.loadingMore": "Wczytywanie większej liczby...", diff --git a/packages/@react-spectrum/s2/intl/pt-BR.json b/packages/@react-spectrum/s2/intl/pt-BR.json index b9f826287db..fc920093184 100644 --- a/packages/@react-spectrum/s2/intl/pt-BR.json +++ b/packages/@react-spectrum/s2/intl/pt-BR.json @@ -31,6 +31,7 @@ "slider.maximum": "Máximo", "slider.minimum": "Mínimo", "table.cancel": "Cancelar", + "table.drag": "Arraste", "table.editCell": "Editar célula", "table.loading": "Carregando...", "table.loadingMore": "Carregando mais...", diff --git a/packages/@react-spectrum/s2/intl/pt-PT.json b/packages/@react-spectrum/s2/intl/pt-PT.json index bb6acd6c981..0c309cd582e 100644 --- a/packages/@react-spectrum/s2/intl/pt-PT.json +++ b/packages/@react-spectrum/s2/intl/pt-PT.json @@ -31,6 +31,7 @@ "slider.maximum": "Máximo", "slider.minimum": "Mínimo", "table.cancel": "Cancelar", + "table.drag": "Arrastar", "table.editCell": "Editar célula", "table.loading": "A carregar...", "table.loadingMore": "A carregar mais...", diff --git a/packages/@react-spectrum/s2/intl/ro-RO.json b/packages/@react-spectrum/s2/intl/ro-RO.json index 050df91e413..a194f3dd837 100644 --- a/packages/@react-spectrum/s2/intl/ro-RO.json +++ b/packages/@react-spectrum/s2/intl/ro-RO.json @@ -31,6 +31,7 @@ "slider.maximum": "Maximum", "slider.minimum": "Minimum", "table.cancel": "Anulare", + "table.drag": "Trageți", "table.editCell": "Editați celula", "table.loading": "Se încarcă...", "table.loadingMore": "Se încarcă mai multe...", diff --git a/packages/@react-spectrum/s2/intl/ru-RU.json b/packages/@react-spectrum/s2/intl/ru-RU.json index cfb6c4d1ded..1548b59d0b0 100644 --- a/packages/@react-spectrum/s2/intl/ru-RU.json +++ b/packages/@react-spectrum/s2/intl/ru-RU.json @@ -31,6 +31,7 @@ "slider.maximum": "Максимум", "slider.minimum": "Минимум", "table.cancel": "Отмена", + "table.drag": "Перетаскивание", "table.editCell": "Редактировать ячейку", "table.loading": "Загрузка...", "table.loadingMore": "Дополнительная загрузка...", diff --git a/packages/@react-spectrum/s2/intl/sk-SK.json b/packages/@react-spectrum/s2/intl/sk-SK.json index a29590ac115..26bea942988 100644 --- a/packages/@react-spectrum/s2/intl/sk-SK.json +++ b/packages/@react-spectrum/s2/intl/sk-SK.json @@ -31,6 +31,7 @@ "slider.maximum": "Maximum", "slider.minimum": "Minimum", "table.cancel": "Zrušiť", + "table.drag": "Presunúť", "table.editCell": "Upraviť bunku", "table.loading": "Načítava sa...", "table.loadingMore": "Načítava sa viac...", diff --git a/packages/@react-spectrum/s2/intl/sl-SI.json b/packages/@react-spectrum/s2/intl/sl-SI.json index 39656e853b5..75cd20ed807 100644 --- a/packages/@react-spectrum/s2/intl/sl-SI.json +++ b/packages/@react-spectrum/s2/intl/sl-SI.json @@ -31,6 +31,7 @@ "slider.maximum": "Največji", "slider.minimum": "Najmanj", "table.cancel": "Prekliči", + "table.drag": "Povleci", "table.editCell": "Uredi celico", "table.loading": "Nalaganje...", "table.loadingMore": "Nalaganje več vsebine...", diff --git a/packages/@react-spectrum/s2/intl/sr-SP.json b/packages/@react-spectrum/s2/intl/sr-SP.json index d6e89eb94fb..4bfa3ae75e1 100644 --- a/packages/@react-spectrum/s2/intl/sr-SP.json +++ b/packages/@react-spectrum/s2/intl/sr-SP.json @@ -31,6 +31,7 @@ "slider.maximum": "Najviše", "slider.minimum": "Najmanje", "table.cancel": "Otkaži", + "table.drag": "Prevuci", "table.editCell": "Uredi ćeliju", "table.loading": "Učitavam...", "table.loadingMore": "Učitavam još...", diff --git a/packages/@react-spectrum/s2/intl/sv-SE.json b/packages/@react-spectrum/s2/intl/sv-SE.json index 12026081606..c7229bc8e55 100644 --- a/packages/@react-spectrum/s2/intl/sv-SE.json +++ b/packages/@react-spectrum/s2/intl/sv-SE.json @@ -31,6 +31,7 @@ "slider.maximum": "Maximum", "slider.minimum": "Minimum", "table.cancel": "Avbryt", + "table.drag": "Dra", "table.editCell": "Redigera cell", "table.loading": "Läser in...", "table.loadingMore": "Läser in mer...", diff --git a/packages/@react-spectrum/s2/intl/tr-TR.json b/packages/@react-spectrum/s2/intl/tr-TR.json index ee8f9b014a6..e0c2c26654d 100644 --- a/packages/@react-spectrum/s2/intl/tr-TR.json +++ b/packages/@react-spectrum/s2/intl/tr-TR.json @@ -31,6 +31,7 @@ "slider.maximum": "Maksimum", "slider.minimum": "Minimum", "table.cancel": "İptal et", + "table.drag": "Sürükle", "table.editCell": "Hücreyi düzenle", "table.loading": "Yükleniyor...", "table.loadingMore": "Daha fazla yükleniyor...", diff --git a/packages/@react-spectrum/s2/intl/uk-UA.json b/packages/@react-spectrum/s2/intl/uk-UA.json index 1446a24e72e..cd6a894956b 100644 --- a/packages/@react-spectrum/s2/intl/uk-UA.json +++ b/packages/@react-spectrum/s2/intl/uk-UA.json @@ -31,6 +31,7 @@ "slider.maximum": "Максимум", "slider.minimum": "Мінімум", "table.cancel": "Скасувати", + "table.drag": "Перетягнути", "table.editCell": "Редагувати клітинку", "table.loading": "Завантаження…", "table.loadingMore": "Завантаження інших об’єктів...", diff --git a/packages/@react-spectrum/s2/intl/zh-CN.json b/packages/@react-spectrum/s2/intl/zh-CN.json index d2d266cbc94..a385d658555 100644 --- a/packages/@react-spectrum/s2/intl/zh-CN.json +++ b/packages/@react-spectrum/s2/intl/zh-CN.json @@ -31,6 +31,7 @@ "slider.maximum": "最大", "slider.minimum": "最小", "table.cancel": "取消", + "table.drag": "拖动", "table.editCell": "编辑单元格", "table.loading": "正在加载...", "table.loadingMore": "正在加载更多...", diff --git a/packages/@react-spectrum/s2/intl/zh-TW.json b/packages/@react-spectrum/s2/intl/zh-TW.json index ed50a588af8..48caecc340c 100644 --- a/packages/@react-spectrum/s2/intl/zh-TW.json +++ b/packages/@react-spectrum/s2/intl/zh-TW.json @@ -31,6 +31,7 @@ "slider.maximum": "最大值", "slider.minimum": "最小值", "table.cancel": "取消", + "table.drag": "拖曳", "table.editCell": "編輯儲存格", "table.loading": "載入中…", "table.loadingMore": "正在載入更多…", diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index fdf1699b161..1b439f7b8af 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -12,7 +12,7 @@ import {ActionButtonGroupContext} from './ActionButtonGroup'; import {ActionMenuContext} from './ActionMenu'; -import {baseColor, color, colorMix, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; +import {baseColor, colorMix, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; import {Button} from 'react-aria-components/Button'; import {centerBaseline} from './CenterBaseline'; import {Checkbox} from './Checkbox'; @@ -22,7 +22,7 @@ import {Collection} from 'react-aria/private/collections/CollectionBuilder'; import {CollectionRendererContext, DefaultCollectionRenderer} from 'react-aria-components/Collection'; import {ContextValue, DEFAULT_SLOT, Provider, SlotProps, useSlottedContext} from 'react-aria-components/utils'; import {controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; -import {createContext, forwardRef, ReactElement, ReactNode, useContext, useEffect, useRef, useState} from 'react'; +import {createContext, forwardRef, ReactElement, ReactNode, useContext, useRef} from 'react'; import {DOMProps, DOMRef, DOMRefValue, DragItem, forwardRefType, GlobalDOMAttributes, ItemDropTarget, LoadingState} from '@react-types/shared'; import DragHandle from '../ui-icons/DragHandle'; import {DropIndicator} from 'react-aria-components/useDragAndDrop'; @@ -41,6 +41,7 @@ import {ImageContext} from './Image'; // @ts-ignore import intlMessages from '../intl/*.json'; import {Key} from '@react-types/shared'; +import {LayoutInfo, Virtualizer} from 'react-aria-components/Virtualizer'; import LinkOutIcon from '../ui-icons/LinkOut'; import {ListLayout} from 'react-stately/private/layout/ListLayout'; import {ListState} from 'react-stately/useListState'; @@ -54,7 +55,6 @@ import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatte import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; import {useVisuallyHidden} from 'react-aria/VisuallyHidden'; -import {LayoutInfo, Virtualizer} from 'react-aria-components/Virtualizer'; export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | 'layout' | 'render' | 'keyboardNavigationBehavior' | 'orientation' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { /** Spectrum-defined styles, returned by the `style()` macro. */ @@ -118,7 +118,7 @@ const listView = style } - {allowsDragging && !isDisabled && ( + {allowsDragging && (
- + {!isDisabled && ( + + )}
)} {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 478de833cc1..d2dae3abca7 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -54,16 +54,17 @@ import {CustomDialog} from './CustomDialog'; import {DialogContainer} from './DialogContainer'; import {DOMProps, DOMRef, DOMRefValue, DragItem, forwardRefType, GlobalDOMAttributes, ItemDropTarget, LinkDOMProps, LoadingState, Node} from '@react-types/shared'; import DragHandle from '../ui-icons/DragHandle'; +import {dragPreviewBadge, dragPreviewCardBack, dragPreviewWrapper, InsertionIndicator, label} from './ListView'; import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import {Form} from 'react-aria-components/Form'; import {getActiveElement, isFocusWithin, nodeContains} from 'react-aria/private/utils/shadowdom/DOMFunctions'; import {getOwnerDocument} from 'react-aria/private/utils/domHelpers'; import {GridNode} from 'react-stately/private/grid/GridCollection'; import {IconContext} from './Icon'; -import {dragPreviewBadge, dragPreviewCardBack, dragPreviewWrapper, InsertionIndicator, label} from './ListView'; // @ts-ignore import intlMessages from '../intl/*.json'; import {Key} from '@react-types/shared'; +import {LayoutInfo, Virtualizer} from 'react-aria-components/Virtualizer'; import {LayoutNode} from 'react-stately/private/layout/ListLayout'; import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu'; import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg'; @@ -74,13 +75,12 @@ import {CheckboxContext as RACCheckboxContext} from 'react-aria-components/Check import {Popover as RACPopover} from 'react-aria-components/Popover'; import React, {createContext, CSSProperties, FormEvent, FormHTMLAttributes, ForwardedRef, forwardRef, ReactElement, ReactNode, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {Rect} from 'react-stately/private/virtualizer/Rect'; -import {Text, TextContext} from './Content'; import SortDownArrow from '../s2wf-icons/S2_Icon_SortDown_20_N.svg'; import SortUpArrow from '../s2wf-icons/S2_Icon_SortUp_20_N.svg'; import {Button as SpectrumButton} from './Button'; +import {Text, TextContext} from './Content'; import {useActionBarContainer} from './ActionBar'; import {useDOMRef} from './useDOMRef'; -import {useFocusRing} from 'react-aria/useFocusRing'; import {useLayoutEffect} from 'react-aria/private/utils/useLayoutEffect'; import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; @@ -88,8 +88,6 @@ import {useMediaQuery} from './useMediaQuery'; import {useObjectRef} from 'react-aria/useObjectRef'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -import {useVisuallyHidden} from 'react-aria/VisuallyHidden'; -import {LayoutInfo, Virtualizer} from 'react-aria-components/Virtualizer'; import {VisuallyHidden} from 'react-aria/VisuallyHidden'; interface S2TableProps { @@ -164,12 +162,8 @@ const table = style(null); let isCheckboxSelection = selectionMode === 'multiple' || selectionMode === 'single'; @@ -486,6 +482,7 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re selectionMode={selectionMode} onRowAction={onAction} dragAndDropHooks={dragAndDropHooks} + disabledBehavior={disabledBehavior} {...otherProps} selectedKeys={selectedKeys} defaultSelectedKeys={undefined} @@ -1027,7 +1024,6 @@ export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function {allowsDragging && ( // @ts-ignore - {/* TODO: intl, need to grab for other locales */} {({isFocusVisible}) => ( <> {isFocusVisible && } @@ -1197,7 +1193,7 @@ const dragButton = style({ // TODO: no clip or clipPath, but this might be sufficient? height: { default: 1, - ':is([role="row"][data-focus-visible-within] *)': 22, + ':is([role="row"][data-focus-visible-within] *)': 22 }, width: { default: 1, @@ -1205,7 +1201,7 @@ const dragButton = style({ }, margin: { default: '[-1]', - ':is([role="row"][data-focus-visible-within] *)': 0 + ':is([role="row"][data-focus-visible-within] *)': 0 }, overflow: { default: 'hidden', @@ -1811,10 +1807,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row - {/* TODO: check if this isDisabled is enough, should be if selection and action is disabled */} - {/* can try to move this into cell perhaps but focusvisible needs to be set on the row and we'd need - to only render it once via something similar to isTreeCOlumn */} - {!otherProps.isDisabled && ( + {!(otherProps.isDisabled && tableVisualOptions.disabledBehavior === 'all') && (