refactor(Sortable): improve sortable props, support custom modifiers (#9879)

* refactor(Sortable): improve props, support modifiers

* refactor: update id and index
This commit is contained in:
one 2025-09-07 17:21:55 +08:00 committed by GitHub
parent 4b65dfa6ea
commit 33f8ea5acb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 64 additions and 44 deletions

View File

@ -1,13 +1,16 @@
import { DraggableSyntheticListeners } from '@dnd-kit/core' import { DraggableSyntheticListeners } from '@dnd-kit/core'
import { Transform } from '@dnd-kit/utilities' import { CSS, Transform } from '@dnd-kit/utilities'
import { classNames } from '@renderer/utils' import { classNames } from '@renderer/utils'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { RenderItemType } from './types'
interface ItemRendererProps<T> { interface ItemRendererProps<T> {
ref?: React.Ref<HTMLDivElement> ref?: React.Ref<HTMLDivElement>
index?: number
item: T item: T
renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode renderItem: RenderItemType<T>
dragging?: boolean dragging?: boolean
dragOverlay?: boolean dragOverlay?: boolean
ghost?: boolean ghost?: boolean
@ -18,6 +21,7 @@ interface ItemRendererProps<T> {
export function ItemRenderer<T>({ export function ItemRenderer<T>({
ref, ref,
index,
item, item,
renderItem, renderItem,
dragging, dragging,
@ -42,14 +46,15 @@ export function ItemRenderer<T>({
const wrapperStyle = { const wrapperStyle = {
transition, transition,
'--translate-x': transform ? `${Math.round(transform.x)}px` : undefined, transform: CSS.Transform.toString(transform ?? null)
'--translate-y': transform ? `${Math.round(transform.y)}px` : undefined,
'--scale-x': transform?.scaleX ? `${transform.scaleX}` : undefined,
'--scale-y': transform?.scaleY ? `${transform.scaleY}` : undefined
} as React.CSSProperties } as React.CSSProperties
return ( return (
<ItemWrapper ref={ref} className={classNames({ dragOverlay: dragOverlay })} style={{ ...wrapperStyle }}> <ItemWrapper
ref={ref}
data-index={index}
className={classNames({ dragOverlay: dragOverlay })}
style={{ ...wrapperStyle }}>
<DraggableItem <DraggableItem
className={classNames({ dragging: dragging, dragOverlay: dragOverlay, ghost: ghost })} className={classNames({ dragging: dragging, dragOverlay: dragOverlay, ghost: ghost })}
{...listeners} {...listeners}
@ -62,8 +67,6 @@ export function ItemRenderer<T>({
const ItemWrapper = styled.div` const ItemWrapper = styled.div`
box-sizing: border-box; box-sizing: border-box;
transform: translate3d(var(--translate-x, 0), var(--translate-y, 0), 0) scaleX(var(--scale-x, 1))
scaleY(var(--scale-y, 1));
transform-origin: 0 0; transform-origin: 0 0;
touch-action: manipulation; touch-action: manipulation;

View File

@ -5,13 +5,19 @@ import {
DragOverlay, DragOverlay,
DropAnimation, DropAnimation,
KeyboardSensor, KeyboardSensor,
Modifier,
Over, Over,
TouchSensor, TouchSensor,
UniqueIdentifier, UniqueIdentifier,
useSensor, useSensor,
useSensors useSensors
} from '@dnd-kit/core' } from '@dnd-kit/core'
import { restrictToHorizontalAxis, restrictToVerticalAxis } from '@dnd-kit/modifiers' import {
restrictToFirstScrollableAncestor,
restrictToHorizontalAxis,
restrictToVerticalAxis,
restrictToWindowEdges
} from '@dnd-kit/modifiers'
import { import {
horizontalListSortingStrategy, horizontalListSortingStrategy,
rectSortingStrategy, rectSortingStrategy,
@ -25,6 +31,7 @@ import styled from 'styled-components'
import { ItemRenderer } from './ItemRenderer' import { ItemRenderer } from './ItemRenderer'
import { SortableItem } from './SortableItem' import { SortableItem } from './SortableItem'
import { RenderItemType } from './types'
import { PortalSafePointerSensor } from './utils' import { PortalSafePointerSensor } from './utils'
interface SortableProps<T> { interface SortableProps<T> {
@ -39,7 +46,7 @@ interface SortableProps<T> {
/** Callback when drag ends, will be passed to dnd-kit's onDragEnd */ /** Callback when drag ends, will be passed to dnd-kit's onDragEnd */
onDragEnd?: (event: { over: Over }) => void onDragEnd?: (event: { over: Over }) => void
/** Function to render individual item, receives item data and drag state */ /** Function to render individual item, receives item data and drag state */
renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode renderItem: RenderItemType<T>
/** Layout type - 'list' for vertical/horizontal list, 'grid' for grid layout */ /** Layout type - 'list' for vertical/horizontal list, 'grid' for grid layout */
layout?: 'list' | 'grid' layout?: 'list' | 'grid'
/** Whether sorting is horizontal */ /** Whether sorting is horizontal */
@ -54,10 +61,17 @@ interface SortableProps<T> {
className?: string className?: string
/** Item list style */ /** Item list style */
listStyle?: React.CSSProperties listStyle?: React.CSSProperties
/** Ghost item style */
ghostItemStyle?: React.CSSProperties
/** Item gap */ /** Item gap */
gap?: number | string gap?: number | string
/** Restrictions, shortcuts for some modifiers */
restrictions?: {
/** Add modifier restrictToWindowEdges */
windowEdges?: boolean
/** Add modifier restrictToFirstScrollableAncestor */
scrollableAncestor?: boolean
}
/** Additional modifiers */
modifiers?: Modifier[]
} }
function Sortable<T>({ function Sortable<T>({
@ -73,7 +87,9 @@ function Sortable<T>({
showGhost = false, showGhost = false,
className, className,
listStyle, listStyle,
gap gap,
restrictions,
modifiers: customModifiers
}: SortableProps<T>) { }: SortableProps<T>) {
const sensors = useSensors( const sensors = useSensors(
useSensor(PortalSafePointerSensor, { useSensor(PortalSafePointerSensor, {
@ -132,7 +148,18 @@ function Sortable<T>({
const strategy = const strategy =
layout === 'list' ? (horizontal ? horizontalListSortingStrategy : verticalListSortingStrategy) : rectSortingStrategy layout === 'list' ? (horizontal ? horizontalListSortingStrategy : verticalListSortingStrategy) : rectSortingStrategy
const modifiers = layout === 'list' ? (horizontal ? [restrictToHorizontalAxis] : [restrictToVerticalAxis]) : []
const { windowEdges = false, scrollableAncestor = false } = restrictions ?? {}
const modifiers = useMemo<Modifier[]>(
() => [
...(layout === 'list' ? [horizontal ? restrictToHorizontalAxis : restrictToVerticalAxis] : []),
...(windowEdges ? [restrictToWindowEdges] : []),
...(scrollableAncestor ? [restrictToFirstScrollableAncestor] : []),
...(customModifiers ?? [])
],
[layout, horizontal, windowEdges, scrollableAncestor, customModifiers]
)
const dropAnimation: DropAnimation = useMemo( const dropAnimation: DropAnimation = useMemo(
() => ({ () => ({
@ -162,8 +189,9 @@ function Sortable<T>({
{items.map((item, index) => ( {items.map((item, index) => (
<SortableItem <SortableItem
key={itemIds[index]} key={itemIds[index]}
id={itemIds[index]}
index={index}
item={item} item={item}
getId={getId}
renderItem={renderItem} renderItem={renderItem}
useDragOverlay={useDragOverlay} useDragOverlay={useDragOverlay}
showGhost={showGhost} showGhost={showGhost}
@ -200,14 +228,14 @@ const ListWrapper = styled.div<{ $gap?: number | string }>`
&[data-layout='list'] { &[data-layout='list'] {
display: flex; display: flex;
align-items: center; align-items: center;
}
[data-direction='horizontal'] { &[data-layout='list'][data-direction='horizontal'] {
flex-direction: row; flex-direction: row;
} }
[data-direction='vertical'] { &[data-layout='list'][data-direction='vertical'] {
flex-direction: column; flex-direction: column;
}
} }
` `

View File

@ -1,25 +1,25 @@
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
import React from 'react'
import { ItemRenderer } from './ItemRenderer' import { ItemRenderer } from './ItemRenderer'
import { RenderItemType } from './types'
interface SortableItemProps<T> { interface SortableItemProps<T> {
item: T item: T
getId: (item: T) => string | number id: string | number
renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode index: number
renderItem: RenderItemType<T>
useDragOverlay?: boolean useDragOverlay?: boolean
showGhost?: boolean showGhost?: boolean
} }
export function SortableItem<T>({ export function SortableItem<T>({
item, item,
getId, id,
index,
renderItem, renderItem,
useDragOverlay = true, useDragOverlay = true,
showGhost = true showGhost = true
}: SortableItemProps<T>) { }: SortableItemProps<T>) {
const id = getId(item)
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id id
}) })
@ -28,6 +28,7 @@ export function SortableItem<T>({
<ItemRenderer <ItemRenderer
ref={setNodeRef} ref={setNodeRef}
item={item} item={item}
index={index}
renderItem={renderItem} renderItem={renderItem}
dragging={isDragging} dragging={isDragging}
dragOverlay={!useDragOverlay && isDragging} dragOverlay={!useDragOverlay && isDragging}

View File

@ -0,0 +1 @@
export type RenderItemType<T> = (item: T, props: { dragging: boolean }) => React.ReactNode

View File

@ -1,4 +1,4 @@
import { defaultDropAnimationSideEffects, type DropAnimation, PointerSensor } from '@dnd-kit/core' import { PointerSensor } from '@dnd-kit/core'
export const PORTAL_NO_DND_SELECTORS = [ export const PORTAL_NO_DND_SELECTORS = [
'.ant-dropdown', '.ant-dropdown',
@ -8,20 +8,6 @@ export const PORTAL_NO_DND_SELECTORS = [
'.ant-modal' '.ant-modal'
].join(',') ].join(',')
/**
* Default drop animation config.
* The opacity is set so to match the drag overlay case.
*/
export const dropAnimationConfig: DropAnimation = {
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: {
opacity: '0.25'
}
}
})
}
/** /**
* Prevent drag on elements with specific classes or data-no-dnd attribute * Prevent drag on elements with specific classes or data-no-dnd attribute
*/ */

View File

@ -253,7 +253,8 @@ const McpServersList: FC = () => {
itemKey="id" itemKey="id"
onSortEnd={onSortEnd} onSortEnd={onSortEnd}
layout="grid" layout="grid"
gap={'12px'} gap="12px"
restrictions={{ scrollableAncestor: true }}
useDragOverlay useDragOverlay
showGhost showGhost
renderItem={(server) => ( renderItem={(server) => (