@@ -536,6 +536,7 @@ export function TableGrid({
536536 const containerRef = useRef < HTMLDivElement > ( null )
537537 const scrollRef = useRef < HTMLDivElement > ( null )
538538 const columnDragPointerXRef = useRef < number | null > ( null )
539+ const columnDragPointerYRef = useRef < number | null > ( null )
539540 const columnDragScrollFrameRef = useRef < number | null > ( null )
540541 const theadRef = useRef < HTMLTableSectionElement > ( null )
541542 const tbodyRef = useRef < HTMLTableSectionElement > ( null )
@@ -1770,21 +1771,91 @@ export function TableGrid({
17701771
17711772 const stopColumnDragAutoScroll = useCallback ( ( ) => {
17721773 columnDragPointerXRef . current = null
1774+ columnDragPointerYRef . current = null
17731775 if ( columnDragScrollFrameRef . current !== null ) {
17741776 cancelAnimationFrame ( columnDragScrollFrameRef . current )
17751777 columnDragScrollFrameRef . current = null
17761778 }
17771779 } , [ ] )
17781780
1779- function startColumnDragAutoScroll ( pointerX : number ) {
1781+ const handleColumnDragLeave = useCallback ( ( ) => {
1782+ dropTargetColumnNameRef . current = null
1783+ setDropTargetColumnName ( null )
1784+ } , [ ] )
1785+
1786+ const updateColumnDropTarget = useCallback (
1787+ ( columnName : string , side : 'left' | 'right' ) => {
1788+ const dragged = dragColumnNameRef . current
1789+ if ( ! dragged ) return
1790+
1791+ const cols = schemaColumnsRef . current
1792+ const draggedGid = cols . find ( ( c ) => getColumnId ( c ) === dragged ) ?. workflowGroupId
1793+ const targetGid = cols . find ( ( c ) => getColumnId ( c ) === columnName ) ?. workflowGroupId
1794+ if (
1795+ ( draggedGid && draggedGid === targetGid ) ||
1796+ pinnedColumnsRef . current . includes ( dragged ) !== pinnedColumnsRef . current . includes ( columnName )
1797+ ) {
1798+ handleColumnDragLeave ( )
1799+ return
1800+ }
1801+
1802+ if ( columnName === dropTargetColumnNameRef . current && side === dropSideRef . current ) return
1803+ dropTargetColumnNameRef . current = columnName
1804+ dropSideRef . current = side
1805+ setDropTargetColumnName ( columnName )
1806+ setDropSide ( side )
1807+ } ,
1808+ [ handleColumnDragLeave ]
1809+ )
1810+
1811+ function updateColumnDropTargetAtPoint ( pointerX : number , pointerY : number ) {
1812+ const hoveredElement = document . elementFromPoint ( pointerX , pointerY )
1813+ const header = hoveredElement ?. closest < HTMLElement > ( 'th[data-column-drag-target]' )
1814+ if ( ! header || ! theadRef . current ?. contains ( header ) ) {
1815+ handleColumnDragLeave ( )
1816+ return
1817+ }
1818+
1819+ const columnName = header . dataset . columnDragTarget
1820+ if ( ! columnName ) {
1821+ handleColumnDragLeave ( )
1822+ return
1823+ }
1824+
1825+ const targetGroupId = header . dataset . columnDragGroup
1826+ let { left, right } = header . getBoundingClientRect ( )
1827+ if ( targetGroupId ) {
1828+ const groupHeaders = theadRef . current . querySelectorAll < HTMLElement > (
1829+ 'th[data-column-drag-group]'
1830+ )
1831+ for ( const groupHeader of groupHeaders ) {
1832+ if ( groupHeader . dataset . columnDragGroup !== targetGroupId ) continue
1833+ const rect = groupHeader . getBoundingClientRect ( )
1834+ left = Math . min ( left , rect . left )
1835+ right = Math . max ( right , rect . right )
1836+ }
1837+ }
1838+
1839+ updateColumnDropTarget ( columnName , pointerX < left + ( right - left ) / 2 ? 'left' : 'right' )
1840+ }
1841+
1842+ function startColumnDragAutoScroll ( pointerX : number , pointerY : number ) {
17801843 columnDragPointerXRef . current = pointerX
1844+ columnDragPointerYRef . current = pointerY
17811845 if ( columnDragScrollFrameRef . current !== null ) return
17821846
17831847 const tick = ( ) => {
17841848 columnDragScrollFrameRef . current = null
17851849 const scrollEl = scrollRef . current
17861850 const currentPointerX = columnDragPointerXRef . current
1787- if ( ! scrollEl || currentPointerX === null || ! dragColumnNameRef . current ) return
1851+ const currentPointerY = columnDragPointerYRef . current
1852+ if (
1853+ ! scrollEl ||
1854+ currentPointerX === null ||
1855+ currentPointerY === null ||
1856+ ! dragColumnNameRef . current
1857+ )
1858+ return
17881859
17891860 const scrollRect = scrollEl . getBoundingClientRect ( )
17901861 const velocity = horizontalEdgeScrollVelocity ( {
@@ -1799,6 +1870,7 @@ export function TableGrid({
17991870 const previousScrollLeft = scrollEl . scrollLeft
18001871 scrollEl . scrollLeft += velocity
18011872 if ( scrollEl . scrollLeft !== previousScrollLeft ) {
1873+ updateColumnDropTargetAtPoint ( currentPointerX , currentPointerY )
18021874 columnDragScrollFrameRef . current = requestAnimationFrame ( tick )
18031875 }
18041876 }
@@ -1821,44 +1893,15 @@ export function TableGrid({
18211893 [ stopColumnDragAutoScroll ]
18221894 )
18231895
1824- const handleColumnDragOver = useCallback ( ( columnName : string , side : 'left' | 'right' ) => {
1825- const dragged = dragColumnNameRef . current
1826- const cols = schemaColumnsRef . current
1827- const targetCol = cols . find ( ( c ) => getColumnId ( c ) === columnName )
1828- const targetGid = targetCol ?. workflowGroupId
1829-
1830- // Suppress drop targeting while hovering siblings of the dragged column's
1831- // own group: reordering inside a group is meaningless (the group renders
1832- // as a unit) and the chasing indicator just flickers.
1833- if ( dragged ) {
1834- const draggedGid = cols . find ( ( c ) => getColumnId ( c ) === dragged ) ?. workflowGroupId
1835- if ( draggedGid && draggedGid === targetGid ) {
1836- if ( dropTargetColumnNameRef . current !== null ) setDropTargetColumnName ( null )
1837- return
1838- }
1839- }
1840-
1841- // Reorder is restricted to within a single zone so a cross-zone drop
1842- // indicator never appears for an insertion the grid would refuse.
1843- if ( dragged ) {
1844- const pinned = pinnedColumnsRef . current
1845- if ( pinned . includes ( dragged ) !== pinned . includes ( columnName ) ) {
1846- if ( dropTargetColumnNameRef . current !== null ) setDropTargetColumnName ( null )
1847- return
1848- }
1849- }
1850-
1851- // Workflow groups: skip per-`<th>` writes and let `handleScrollDragOver`
1852- // do the bookkeeping. The scroll handler computes side from the group's
1853- // full bounds, so it stays stable across sibling cursor moves; the per-th
1854- // events would otherwise oscillate name + side as the cursor crosses each
1855- // sibling's midpoint.
1856- if ( targetGid ) return
1857-
1858- if ( columnName === dropTargetColumnNameRef . current && side === dropSideRef . current ) return
1859- setDropTargetColumnName ( columnName )
1860- setDropSide ( side )
1861- } , [ ] )
1896+ const handleColumnDragOver = useCallback (
1897+ ( columnName : string , side : 'left' | 'right' ) => {
1898+ const cols = schemaColumnsRef . current
1899+ const targetCol = cols . find ( ( c ) => getColumnId ( c ) === columnName )
1900+ if ( targetCol ?. workflowGroupId ) return
1901+ updateColumnDropTarget ( columnName , side )
1902+ } ,
1903+ [ updateColumnDropTarget ]
1904+ )
18621905
18631906 const handleColumnDragEnd = useCallback ( ( ) => {
18641907 stopColumnDragAutoScroll ( )
@@ -1998,11 +2041,6 @@ export function TableGrid({
19982041 setDropSide ( 'left' )
19992042 } , [ stopColumnDragAutoScroll ] )
20002043
2001- const handleColumnDragLeave = useCallback ( ( ) => {
2002- dropTargetColumnNameRef . current = null
2003- setDropTargetColumnName ( null )
2004- } , [ ] )
2005-
20062044 function handleScrollDragOver ( e : React . DragEvent ) {
20072045 const draggedName = dragColumnNameRef . current
20082046 if ( ! draggedName ) return
@@ -2014,48 +2052,9 @@ export function TableGrid({
20142052 if ( pinnedColumnsRef . current . includes ( draggedName ) ) {
20152053 stopColumnDragAutoScroll ( )
20162054 } else {
2017- startColumnDragAutoScroll ( e . clientX )
2018- }
2019- const scrollRect = scrollEl . getBoundingClientRect ( )
2020- const cursorX = e . clientX - scrollRect . left + scrollEl . scrollLeft
2021-
2022- const cols = columnsRef . current
2023- const draggedGid = cols . find ( ( c ) => c . key === dragColumnNameRef . current ) ?. workflowGroupId
2024- let left = checkboxColWidth
2025- let i = 0
2026- while ( i < cols . length ) {
2027- const col = cols [ i ]
2028- // Treat fanned-out groups as monolithic drop targets; accumulate across siblings.
2029- // Clamp `groupSize` to remaining columns: dragover fires constantly and can
2030- // race a column removal where the cached `groupSize` outpaces `cols.length`.
2031- const groupSize = Math . min ( col . groupSize , cols . length - i )
2032- let groupWidth = 0
2033- for ( let j = 0 ; j < groupSize ; j ++ ) {
2034- groupWidth += columnWidthsRef . current [ cols [ i + j ] . key ] ?? COL_WIDTH
2035- }
2036- if ( cursorX < left + groupWidth ) {
2037- // Inside the dragged column's own group → no-op drop, no indicator.
2038- if ( draggedGid && col . workflowGroupId === draggedGid ) {
2039- if ( dropTargetColumnNameRef . current !== null ) setDropTargetColumnName ( null )
2040- return
2041- }
2042- const pinned = pinnedColumnsRef . current
2043- const draggedName = dragColumnNameRef . current
2044- if ( draggedName && pinned . includes ( draggedName ) !== pinned . includes ( col . key ) ) {
2045- if ( dropTargetColumnNameRef . current !== null ) setDropTargetColumnName ( null )
2046- return
2047- }
2048- const midX = left + groupWidth / 2
2049- const side = cursorX < midX ? 'left' : 'right'
2050- if ( col . key !== dropTargetColumnNameRef . current || side !== dropSideRef . current ) {
2051- setDropTargetColumnName ( col . key )
2052- setDropSide ( side )
2053- }
2054- return
2055- }
2056- left += groupWidth
2057- i += groupSize
2055+ startColumnDragAutoScroll ( e . clientX , e . clientY )
20582056 }
2057+ updateColumnDropTargetAtPoint ( e . clientX , e . clientY )
20592058 }
20602059
20612060 function handleScrollDrop ( e : React . DragEvent ) {
@@ -4422,6 +4421,7 @@ export function TableGrid({
44224421 return (
44234422 < th
44244423 key = { `meta-${ g . startColIndex } ` }
4424+ data-column-drag-target = { firstCol ?. key }
44254425 className = { cn (
44264426 'border-[var(--border)] border-b bg-[var(--bg)] px-2 py-[5px]' ,
44274427 stickyLeft !== undefined && 'z-[11]' ,
0 commit comments