Skip to content

Commit 2c7ef3f

Browse files
authored
fix(ui): drag and drop not working for sortable hasMany fields (#15845)
### What Fixes drag-and-drop sorting for relationship and select fields with `hasMany` and `admin.isSortable: true`. ### Why In v3.78.0, commit 418bb92 replaced `PointerSensor` with `MouseSensor` + `TouchSensor`. The `MultiValue` component was overriding the `onMouseDown` handler from `listeners`, preventing the `MouseSensor` from detecting drag start events. ### How Extract `onMouseDown` from `listeners` and call it explicitly before the custom `stopPropagation` logic. Touch and keyboard handlers remain unaffected. Fixes #15843
1 parent 74799ea commit 2c7ef3f

3 files changed

Lines changed: 477 additions & 85 deletions

File tree

packages/ui/src/elements/ReactSelect/MultiValue/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ export const MultiValue: React.FC<MultiValueProps<Option>> = (props) => {
4040
.filter(Boolean)
4141
.join(' ')
4242

43+
// Extract onMouseDown from listeners to preserve the MouseSensor handler
44+
const { onMouseDown: listenersMouseDown, ...restListeners } = listeners || {}
45+
4346
return (
4447
<React.Fragment>
4548
<SelectComponents.MultiValue
@@ -49,11 +52,16 @@ export const MultiValue: React.FC<MultiValueProps<Option>> = (props) => {
4952
...(isSortable
5053
? {
5154
...attributes,
52-
...listeners,
55+
...restListeners,
5356
}
5457
: {}),
5558
...innerProps,
5659
onMouseDown: (e) => {
60+
// Call the MouseSensor's handler first to enable mouse dragging
61+
if (isSortable && listenersMouseDown) {
62+
listenersMouseDown(e)
63+
}
64+
5765
if (!disableMouseDown) {
5866
// we need to prevent the dropdown from opening when clicking on the drag handle, but not when a modal is open (i.e. the 'Relationship' field component)
5967
e.stopPropagation()

test/fields/collections/Select/e2e.spec.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ import { fileURLToPath } from 'url'
77
import type { PayloadTestSDK } from '../../../__helpers/shared/sdk/index.js'
88
import type { Config } from '../../payload-types.js'
99

10+
import { checkFocusIndicators } from '../../../__helpers/e2e/checkFocusIndicators.js'
1011
import {
1112
ensureCompilationIsDone,
1213
initPageConsoleErrorCatch,
1314
saveDocAndAssert,
1415
waitForFormReady,
1516
} from '../../../__helpers/e2e/helpers.js'
16-
import { AdminUrlUtil } from '../../../__helpers/shared/adminUrlUtil.js'
17-
import { checkFocusIndicators } from '../../../__helpers/e2e/checkFocusIndicators.js'
1817
import { runAxeScan } from '../../../__helpers/e2e/runAxeScan.js'
19-
import { initPayloadE2ENoConfig } from '../../../__helpers/shared/initPayloadE2ENoConfig.js'
18+
import { AdminUrlUtil } from '../../../__helpers/shared/adminUrlUtil.js'
2019
import { reInitializeDB } from '../../../__helpers/shared/clearAndSeed/reInitializeDB.js'
20+
import { initPayloadE2ENoConfig } from '../../../__helpers/shared/initPayloadE2ENoConfig.js'
2121
import { RESTClient } from '../../../__helpers/shared/rest.js'
2222
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
2323
import { selectFieldsSlug } from '../../slugs.js'
@@ -130,6 +130,67 @@ describe('Select', () => {
130130
await expect(options.locator('text=Two')).toBeHidden()
131131
})
132132

133+
test('should allow reordering hasMany select values with drag and drop', async () => {
134+
await page.goto(url.create)
135+
await waitForFormReady(page)
136+
137+
const field = page.locator('#field-selectHasMany')
138+
139+
// Select multiple options in order: one, two, three
140+
await field.click({ delay: 100 })
141+
await page.locator('.rs__option:has-text("Value One")').click()
142+
143+
await field.click({ delay: 100 })
144+
await page.locator('.rs__option:has-text("Value Two")').click()
145+
146+
await field.click({ delay: 100 })
147+
await page.locator('.rs__option:has-text("Value Three")').click()
148+
149+
// Verify initial order
150+
const valueContainer = field.locator('.rs__value-container')
151+
const pills = valueContainer.locator('.rs__multi-value')
152+
await expect(pills).toHaveCount(3)
153+
await expect(pills.nth(0)).toContainText('Value One')
154+
await expect(pills.nth(1)).toContainText('Value Two')
155+
await expect(pills.nth(2)).toContainText('Value Three')
156+
157+
// Get bounding boxes for drag operation
158+
const firstPill = pills.nth(0)
159+
const thirdPill = pills.nth(2)
160+
161+
const firstBox = (await firstPill.boundingBox())!
162+
const thirdBox = (await thirdPill.boundingBox())!
163+
164+
// Drag first pill to the position of the third pill
165+
await page.mouse.move(firstBox.x + firstBox.width / 2, firstBox.y + firstBox.height / 2)
166+
await page.mouse.down()
167+
await page.mouse.move(thirdBox.x + thirdBox.width / 2, thirdBox.y + thirdBox.height / 2, {
168+
steps: 10,
169+
})
170+
await page.mouse.up()
171+
172+
// Verify the order changed - first item should now be last
173+
const updatedPills = valueContainer.locator('.rs__multi-value')
174+
await expect(updatedPills.nth(0)).toContainText('Value Two')
175+
await expect(updatedPills.nth(1)).toContainText('Value Three')
176+
await expect(updatedPills.nth(2)).toContainText('Value One')
177+
178+
// Save and verify the order is persisted
179+
await saveDocAndAssert(page)
180+
181+
const currentUrl = page.url()
182+
const id = currentUrl.split('/').pop()!
183+
184+
// Reload the page to verify order persists
185+
await page.goto(url.edit(id))
186+
await waitForFormReady(page)
187+
188+
const reloadedPills = page.locator('#field-selectHasMany .rs__value-container .rs__multi-value')
189+
await expect(reloadedPills.nth(0)).toContainText('Value Two')
190+
await expect(reloadedPills.nth(1)).toContainText('Value Three')
191+
await expect(reloadedPills.nth(2)).toContainText('Value One')
192+
})
193+
133194
describe('A11y', () => {
134195
test.fixme('Create view should have no accessibility violations', async ({}, testInfo) => {
135196
await page.goto(url.create)

0 commit comments

Comments
 (0)