Skip to content

Commit

Permalink
chore(form): replace react-sortable-hoc with dndkit (#3961)
Browse files Browse the repository at this point in the history
* chore(form): replace react-sortable-hoc with dndkit

* fix(form): set sorting strategy for list axis

* fix(form): change easing function for sortable lists
  • Loading branch information
bjoerge committed Dec 16, 2022
1 parent 13389f5 commit 551c4d5
Show file tree
Hide file tree
Showing 11 changed files with 296 additions and 170 deletions.
5 changes: 4 additions & 1 deletion packages/sanity/package.json
Expand Up @@ -96,11 +96,14 @@
"write:version": "node -r esbuild-register scripts/writeVersion.ts"
},
"dependencies": {
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/modifiers": "^6.0.0",
"@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
"@juggle/resize-observer": "^3.3.1",
"@portabletext/react": "^1.0.6",
"@portabletext/types": "^1.0.3",
"@rexxars/react-json-inspector": "^8.0.1",
"@rexxars/react-sortable-hoc": "^2.0.0",
"@sanity/asset-utils": "^1.2.5",
"@sanity/bifur-client": "^0.3.0",
"@sanity/block-tools": "3.0.6",
Expand Down
@@ -1,6 +1,6 @@
/* eslint-disable react/jsx-handler-names */
import {Card, Stack, Text} from '@sanity/ui'
import React, {useCallback} from 'react'
import React, {useCallback, useMemo} from 'react'
import {Item, List} from '../../common/list'
import {ArrayOfObjectsInputProps, ObjectItem, ObjectItemProps} from '../../../../types'
import {ArrayOfObjectsItem} from '../../../../members'
Expand Down Expand Up @@ -51,6 +51,8 @@ export function GridArrayInput<Item extends ObjectItem>(props: ArrayOfObjectsInp
return <GridItem {...itemProps} />
}, [])

const memberKeys = useMemo(() => members.map((member) => member.key), [members])

return (
<Stack space={3}>
<UploadTargetCard
Expand All @@ -71,17 +73,16 @@ export function GridArrayInput<Item extends ObjectItem>(props: ArrayOfObjectsInp
{members?.length > 0 && (
<Card border radius={1}>
<List
axis="xy"
lockAxis="xy"
columns={[2, 3, 4]}
gap={3}
padding={1}
margin={1}
items={memberKeys}
onItemMove={onItemMove}
sortable={sortable}
>
{members.map((member, index) => (
<Item key={member.key} sortable={sortable} index={index} flex={1}>
{members.map((member) => (
<Item key={member.key} sortable={sortable} id={member.key} flex={1}>
{member.kind === 'item' && (
<ArrayOfObjectsItem
member={member}
Expand Down
@@ -1,6 +1,6 @@
/* eslint-disable react/jsx-handler-names */
import {Card, Stack, Text} from '@sanity/ui'
import React, {useCallback} from 'react'
import React, {useCallback, useMemo} from 'react'
import {Item, List} from '../../common/list'
import {ArrayOfObjectsInputProps, ObjectItem} from '../../../../types'
import {ArrayOfObjectsItem} from '../../../../members'
Expand Down Expand Up @@ -46,7 +46,7 @@ export function ListArrayInput<Item extends ObjectItem>(props: ArrayOfObjectsInp
)

const sortable = schemaType.options?.sortable !== false

const memberKeys = useMemo(() => members.map((member) => member.key), [members])
return (
<Stack space={3}>
<UploadTargetCard
Expand All @@ -65,9 +65,16 @@ export function ListArrayInput<Item extends ObjectItem>(props: ArrayOfObjectsInp
</Card>
) : (
<Card border radius={1}>
<List gap={1} paddingY={1} onItemMove={onItemMove} sortable={sortable}>
{members.map((member, index) => (
<Item key={member.key} sortable={sortable} index={index}>
<List
axis="y"
gap={1}
paddingY={1}
items={memberKeys}
onItemMove={onItemMove}
sortable={sortable}
>
{members.map((member) => (
<Item key={member.key} sortable={sortable} id={member.key}>
{member.kind === 'item' && (
<ArrayOfObjectsItem
member={member}
Expand Down
Expand Up @@ -127,6 +127,13 @@ export class ArrayOfPrimitivesInput extends React.PureComponent<ArrayOfPrimitive

const isSortable = !readOnly && get(schemaType, 'options.sortable') !== false

// Note: we need this in order to generate new id's when items are moved around in the list
// without it, dndkit will restore focus on the original index of the dragged item
const membersWithSortIds = members.map((member) => ({
id: `${member.key}-${member.kind === 'item' ? member.item.value : 'error'}`,
member: member,
}))

return (
<Stack space={3} data-testid="array-primitives-input">
<UploadTargetCard
Expand All @@ -137,18 +144,23 @@ export class ArrayOfPrimitivesInput extends React.PureComponent<ArrayOfPrimitive
tabIndex={0}
>
<Stack space={1}>
{members.length === 0 ? (
{membersWithSortIds.length === 0 ? (
<Card padding={3} border style={{borderStyle: 'dashed'}} radius={2}>
<Text align="center" muted size={1}>
{schemaType.placeholder || <>No items</>}
</Text>
</Card>
) : (
<Card padding={1} border>
<List onItemMove={this.handleSortEnd} sortable={isSortable} gap={1}>
{members.map((member, index) => {
<List
onItemMove={this.handleSortEnd}
items={membersWithSortIds.map((m) => m.id)}
sortable={isSortable}
gap={1}
>
{membersWithSortIds.map(({member, id}, index) => {
return (
<Item key={member.key} sortable={isSortable} index={index}>
<Item key={member.key} id={id} sortable={isSortable} disableTransition>
{member.kind === 'item' && (
<ArrayOfPrimitivesItem
member={member}
Expand Down
16 changes: 10 additions & 6 deletions packages/sanity/src/core/form/inputs/arrays/common/DragHandle.tsx
@@ -1,27 +1,31 @@
import styled from 'styled-components'
import {Button, ButtonProps} from '@sanity/ui'
import React from 'react'
import React, {useContext} from 'react'
import {DragHandleIcon} from '@sanity/icons'
import {DRAG_HANDLE_ATTRIBUTE, sortableHandle} from './sortable'
import {useSortable} from '@dnd-kit/sortable'

const DragHandleButton = styled(Button)<{grid?: boolean}>`
cursor: ${(props) => (props.grid ? 'move' : 'ns-resize')};
`

const DRAG_HANDLE_PROPS = {[DRAG_HANDLE_ATTRIBUTE]: true}
export const SortableItemIdContext = React.createContext<string | null>(null)

export const DragHandle = sortableHandle(function DragHandle(
export const DragHandle = function DragHandle(
props: {
grid?: boolean
} & ButtonProps
) {
const id = useContext(SortableItemIdContext)!
const {listeners, attributes} = useSortable({id})

return (
<DragHandleButton
icon={DragHandleIcon}
mode="bleed"
data-ui="DragHandleButton"
{...attributes}
{...props}
{...DRAG_HANDLE_PROPS}
{...listeners}
/>
)
})
}
@@ -0,0 +1,45 @@
import type {ClientRect, Modifier} from '@dnd-kit/core'
import type {Transform} from '@dnd-kit/utilities'

function restrictToBoundingRect(
transform: Transform,
rect: ClientRect,
boundingRect: ClientRect,
margins: Margins
): Transform {
const value = {
...transform,
}

const marginY = margins.y || 0
const marginX = margins.x || 0

if (rect.top + value.y <= boundingRect.top + marginY) {
value.y = boundingRect.top - rect.top + marginY
} else if (rect.bottom + value.y >= boundingRect.top + boundingRect.height - marginY) {
value.y = boundingRect.top + boundingRect.height - rect.bottom - marginY
}

if (rect.left + value.x <= boundingRect.left - marginX) {
value.x = boundingRect.left - rect.left + marginX
} else if (rect.right + value.x >= boundingRect.left + boundingRect.width + marginX) {
value.x = boundingRect.left + boundingRect.width - rect.right + marginX
}

return value
}

interface Margins {
x?: number
y?: number
}

export const restrictToParentElementWithMargins: (margins: Margins) => Modifier =
(margins: Margins) =>
({containerNodeRect, draggingNodeRect, transform}) => {
if (!draggingNodeRect || !containerNodeRect) {
return transform
}

return restrictToBoundingRect(transform, draggingNodeRect, containerNodeRect, margins)
}

1 comment on commit 551c4d5

@vercel
Copy link

@vercel vercel bot commented on 551c4d5 Dec 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

test-studio – ./

test-studio.sanity.build
test-studio-git-next.sanity.build

Please sign in to comment.