From d2f40f7ef79e57360971a2f3f5e0124aede1aa99 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Fri, 16 May 2025 08:21:31 -0500 Subject: [PATCH 1/2] Set drag column image (#3783) * Set drag column image * Few fixes * Add `draggedColumnKey` state * Update src/HeaderCell.tsx Co-authored-by: Nicolas Stepien <567105+nstepien@users.noreply.github.com> * newline --------- Co-authored-by: Nicolas Stepien <567105+nstepien@users.noreply.github.com> --- src/HeaderCell.tsx | 78 +++++++++++++++++----------- src/HeaderRow.tsx | 7 +-- src/style/core.ts | 2 +- website/routes/ColumnsReordering.tsx | 33 +++++++----- 4 files changed, 73 insertions(+), 47 deletions(-) diff --git a/src/HeaderCell.tsx b/src/HeaderCell.tsx index 5f8577616d..b138a3c015 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { useId, useRef, useState } from 'react'; import { css } from '@linaria/core'; import { useRovingTabIndex } from './hooks'; @@ -43,17 +43,29 @@ export const resizeHandleClassname = css` const cellDraggableClassname = 'rdg-cell-draggable'; const cellDragging = css` - opacity: 0.5; + @layer rdg.HeaderCell { + background-color: var(--rdg-header-draggable-background-color); + } `; const cellDraggingClassname = `rdg-cell-dragging ${cellDragging}`; const cellOver = css` - background-color: var(--rdg-header-draggable-background-color); + @layer rdg.HeaderCell { + background-color: var(--rdg-header-draggable-background-color); + } `; const cellOverClassname = `rdg-cell-drag-over ${cellOver}`; +const dragImageClassname = css` + @layer rdg.HeaderCell { + border-radius: 4px; + width: fit-content; + outline: 2px solid hsl(207, 100%, 50%); + } +`; + type SharedHeaderRowProps = Pick< HeaderRowProps, | 'sortColumns' @@ -70,7 +82,8 @@ export interface HeaderCellProps extends SharedHeaderRowProps { colSpan: number | undefined; rowIdx: number; isCellSelected: boolean; - dragDropKey: string; + draggedColumnKey: string | undefined; + setDraggedColumnKey: (draggedColumnKey: string | undefined) => void; } export default function HeaderCell({ @@ -85,10 +98,11 @@ export default function HeaderCell({ onSortColumnsChange, selectCell, direction, - dragDropKey + draggedColumnKey, + setDraggedColumnKey }: HeaderCellProps) { - const [isDragging, setIsDragging] = useState(false); const [isOver, setIsOver] = useState(false); + const isDragging = draggedColumnKey === column.key; const rowSpan = getHeaderCellRowSpan(column, rowIdx); const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellSelected); const sortIndex = sortColumns?.findIndex((sort) => sort.columnKey === column.key); @@ -99,6 +113,7 @@ export default function HeaderCell({ const ariaSort = sortDirection && !priority ? (sortDirection === 'ASC' ? 'ascending' : 'descending') : undefined; const { sortable, resizable, draggable } = column; + const dragImageId = useId(); const className = getCellClassname(column, column.headerCellClass, { [cellSortableClassname]: sortable, @@ -180,13 +195,18 @@ export default function HeaderCell({ } function onDragStart(event: React.DragEvent) { - event.dataTransfer.setData(dragDropKey, column.key); + const dragImage = event.currentTarget.cloneNode(true) as HTMLDivElement; + dragImage.classList.add(dragImageClassname); + dragImage.id = dragImageId; + event.currentTarget.parentElement!.insertBefore(dragImage, event.currentTarget); + event.dataTransfer.setDragImage(dragImage, 0, 0); event.dataTransfer.dropEffect = 'move'; - setIsDragging(true); + setDraggedColumnKey(column.key); } function onDragEnd() { - setIsDragging(false); + setDraggedColumnKey(undefined); + document.getElementById(dragImageId)?.remove(); } function onDragOver(event: React.DragEvent) { @@ -197,18 +217,9 @@ export default function HeaderCell({ function onDrop(event: React.DragEvent) { setIsOver(false); - // The dragDropKey is derived from the useId() hook, which can sometimes generate keys with uppercase letters. - // When setting data using event.dataTransfer.setData(), the key is automatically converted to lowercase in some browsers. - // To ensure consistent comparison, we normalize the dragDropKey to lowercase before checking its presence in the event's dataTransfer types. - // https://html.spec.whatwg.org/multipage/dnd.html#the-datatransfer-interface - if (event.dataTransfer.types.includes(dragDropKey.toLowerCase())) { - const sourceKey = event.dataTransfer.getData(dragDropKey.toLowerCase()); - if (sourceKey !== column.key) { - // prevent the browser from redirecting in some cases - event.preventDefault(); - onColumnsReorder?.(sourceKey, column.key); - } - } + // prevent the browser from redirecting in some cases + event.preventDefault(); + onColumnsReorder?.(draggedColumnKey!, column.key); } function onDragEnter(event: React.DragEvent) { @@ -223,19 +234,23 @@ export default function HeaderCell({ } } - let draggableProps: React.ComponentProps<'div'> | undefined; + let dragTargetProps: React.ComponentProps<'div'> | undefined; + let dropTargetProps: React.ComponentProps<'div'> | undefined; if (draggable) { - draggableProps = { + dragTargetProps = { draggable: true, - /* events fired on the draggable target */ onDragStart, - onDragEnd, - /* events fired on the drop targets */ - onDragOver, - onDragEnter, - onDragLeave, - onDrop + onDragEnd }; + + if (draggedColumnKey !== undefined && draggedColumnKey !== column.key) { + dropTargetProps = { + onDragOver, + onDragEnter, + onDragLeave, + onDrop + }; + } } return ( @@ -256,7 +271,8 @@ export default function HeaderCell({ onFocus={onFocus} onClick={onClick} onKeyDown={onKeyDown} - {...draggableProps} + {...dragTargetProps} + {...dropTargetProps} > {column.renderHeaderCell({ column, diff --git a/src/HeaderRow.tsx b/src/HeaderRow.tsx index e1e0ec0b18..e9445d3e33 100644 --- a/src/HeaderRow.tsx +++ b/src/HeaderRow.tsx @@ -1,4 +1,4 @@ -import { memo, useId } from 'react'; +import { memo, useState } from 'react'; import { css } from '@linaria/core'; import clsx from 'clsx'; @@ -60,7 +60,7 @@ function HeaderRow({ selectCell, direction }: HeaderRowProps) { - const dragDropKey = useId(); + const [draggedColumnKey, setDraggedColumnKey] = useState(); const cells = []; for (let index = 0; index < columns.length; index++) { @@ -84,7 +84,8 @@ function HeaderRow({ sortColumns={sortColumns} selectCell={selectCell} direction={direction} - dragDropKey={dragDropKey} + draggedColumnKey={draggedColumnKey} + setDraggedColumnKey={setDraggedColumnKey} /> ); } diff --git a/src/style/core.ts b/src/style/core.ts index 1d3040c5c1..9add53d0f6 100644 --- a/src/style/core.ts +++ b/src/style/core.ts @@ -42,7 +42,7 @@ const root = css` @layer rdg.Root { ${lightTheme} - --rdg-selection-color: #66afe9; + --rdg-selection-color: hsl(207, 75%, 66%); --rdg-font-size: 14px; --rdg-cell-frozen-box-shadow: 2px 0 5px -2px rgba(136, 136, 136, 0.3); diff --git a/website/routes/ColumnsReordering.tsx b/website/routes/ColumnsReordering.tsx index 7793008218..cfd198865f 100644 --- a/website/routes/ColumnsReordering.tsx +++ b/website/routes/ColumnsReordering.tsx @@ -106,18 +106,27 @@ function ColumnsReordering() { }, [rows, sortColumns]); function onColumnsReorder(sourceKey: string, targetKey: string) { - setColumnsOrder((columnsOrder) => { - const sourceColumnOrderIndex = columnsOrder.findIndex( - (index) => columns[index].key === sourceKey - ); - const targetColumnOrderIndex = columnsOrder.findIndex( - (index) => columns[index].key === targetKey - ); - const sourceColumnOrder = columnsOrder[sourceColumnOrderIndex]; - const newColumnsOrder = columnsOrder.toSpliced(sourceColumnOrderIndex, 1); - newColumnsOrder.splice(targetColumnOrderIndex, 0, sourceColumnOrder); - return newColumnsOrder; - }); + function reorderColumns() { + setColumnsOrder((columnsOrder) => { + const sourceColumnOrderIndex = columnsOrder.findIndex( + (index) => columns[index].key === sourceKey + ); + const targetColumnOrderIndex = columnsOrder.findIndex( + (index) => columns[index].key === targetKey + ); + const sourceColumnOrder = columnsOrder[sourceColumnOrderIndex]; + const newColumnsOrder = columnsOrder.toSpliced(sourceColumnOrderIndex, 1); + newColumnsOrder.splice(targetColumnOrderIndex, 0, sourceColumnOrder); + return newColumnsOrder; + }); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (document.startViewTransition) { + document.startViewTransition(reorderColumns); + } else { + reorderColumns(); + } } function resetOrderAndWidths() { From 3abb8f3cf61d29e527d1a3e22f9c02c01df22c6e Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Fri, 16 May 2025 09:07:44 -0500 Subject: [PATCH 2/2] Use React element for drag image (#3784) --- src/HeaderCell.tsx | 103 ++++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 44 deletions(-) diff --git a/src/HeaderCell.tsx b/src/HeaderCell.tsx index b138a3c015..e695f19b9d 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -1,4 +1,5 @@ -import { useId, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; +import { flushSync } from 'react-dom'; import { css } from '@linaria/core'; import { useRovingTabIndex } from './hooks'; @@ -63,6 +64,7 @@ const dragImageClassname = css` border-radius: 4px; width: fit-content; outline: 2px solid hsl(207, 100%, 50%); + outline-offset: -2px; } `; @@ -102,6 +104,7 @@ export default function HeaderCell({ setDraggedColumnKey }: HeaderCellProps) { const [isOver, setIsOver] = useState(false); + const dragImageRef = useRef(null); const isDragging = draggedColumnKey === column.key; const rowSpan = getHeaderCellRowSpan(column, rowIdx); const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellSelected); @@ -113,7 +116,6 @@ export default function HeaderCell({ const ariaSort = sortDirection && !priority ? (sortDirection === 'ASC' ? 'ascending' : 'descending') : undefined; const { sortable, resizable, draggable } = column; - const dragImageId = useId(); const className = getCellClassname(column, column.headerCellClass, { [cellSortableClassname]: sortable, @@ -195,18 +197,16 @@ export default function HeaderCell({ } function onDragStart(event: React.DragEvent) { - const dragImage = event.currentTarget.cloneNode(true) as HTMLDivElement; - dragImage.classList.add(dragImageClassname); - dragImage.id = dragImageId; - event.currentTarget.parentElement!.insertBefore(dragImage, event.currentTarget); - event.dataTransfer.setDragImage(dragImage, 0, 0); + // need flushSync to make sure the drag image is rendered before the drag starts + flushSync(() => { + setDraggedColumnKey(column.key); + }); + event.dataTransfer.setDragImage(dragImageRef.current!, 0, 0); event.dataTransfer.dropEffect = 'move'; - setDraggedColumnKey(column.key); } function onDragEnd() { setDraggedColumnKey(undefined); - document.getElementById(dragImageId)?.remove(); } function onDragOver(event: React.DragEvent) { @@ -253,43 +253,58 @@ export default function HeaderCell({ } } + const style: React.CSSProperties = { + ...getHeaderCellStyle(column, rowIdx, rowSpan), + ...getCellStyle(column, colSpan) + }; + + const content = column.renderHeaderCell({ + column, + sortDirection, + priority, + tabIndex: childTabIndex + }); + return ( -
- {column.renderHeaderCell({ - column, - sortDirection, - priority, - tabIndex: childTabIndex - })} - - {resizable && ( - + <> + {isDragging && ( +
+ {content} +
)} -
+
+ {content} + + {resizable && ( + + )} +
+ ); }