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

View File

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

View File

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

View File

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