editor: explain slot refusals on-page; auto-scroll while dragging\n\nDrag state now records the tagged slot under the pointer. When the\ndragged type does not fit it, a badge on the zone names the accepted\ntypes (from the slot schema) and the pointer chip turns refused -\npreviously a refused slot silently fell back to a container drop.\nHolding the pointer near a viewport edge scrolls the page (ramped\nrAF loop, re-targeting after each step) so long pages are droppable\nend to end. Helpers are unit-tested; harness covers refusal badge,\naccept path and the scroll step.\n\n🤖 Generated with Codebuff\nCo-Authored-By: Codebuff <noreply@codebuff.com>

This commit is contained in:
mats committed 2026-11-07 11:53:47 +01:00
1 parent 8e8823421d
commit 40b33f89a4
6 files changed
+154 -12

No files matched your search

+61
View File
@@ -199,6 +199,67 @@ describe('block palette and drag & drop', () => {
await h.settle()
expect(kids('s1')).toEqual(['b1', 'h1', 't1'])
})
it('a slot that refuses the type names what it takes (drop still falls back to the container)', async () => {
h = await setup()
await h.settle()
place('s1', 0, 0, 600, 300)
// The section schema declares a `sidebar` slot taking text/button/footer; the template marks it.
byId('s1').insertAdjacentHTML('beforeend', '<div data-cms-slot="sidebar" data-cms-slot-parent="s1"></div>')
const zone = byId('s1').querySelector<HTMLElement>('[data-cms-slot]')!
zone.getBoundingClientRect = () => ({ x: 10, y: 190, left: 10, top: 190, right: 590, bottom: 250, width: 580, height: 60, toJSON() {} }) as DOMRect
h.editor.ctrl.scan()
h.editor.ctrl.beginPaletteDrag({ type: 'heading', label: 'heading' })
await ptr('pointermove', 80, 200)
expect(h.editor.ctrl.store.get().drag).toMatchObject({ slot: { parentId: 's1', handle: 'sidebar' }, target: { parentId: 's1' } })
expect(h.shadow.querySelector('.cms-slotref')?.textContent).toContain('sidebar takes: text, button, footer')
expect(h.shadow.querySelector('.cms-dragchip')?.className).toContain('is-refused')
await ptr('pointerup', 80, 200)
await h.settle()
expect(h.editor.ctrl.store.get().drag).toBeNull()
})
it('a slot that accepts the type shows no refusal', async () => {
h = await setup()
await h.settle()
place('s1', 0, 0, 600, 300)
byId('s1').insertAdjacentHTML('beforeend', '<div data-cms-slot="sidebar" data-cms-slot-parent="s1"></div>')
const zone = byId('s1').querySelector<HTMLElement>('[data-cms-slot]')!
zone.getBoundingClientRect = () => ({ x: 10, y: 190, left: 10, top: 190, right: 590, bottom: 250, width: 580, height: 60, toJSON() {} }) as DOMRect
h.editor.ctrl.scan()
h.editor.ctrl.beginPaletteDrag({ type: 'button', label: 'button' })
await ptr('pointermove', 80, 200)
expect(h.editor.ctrl.store.get().drag).toMatchObject({
slot: { parentId: 's1', handle: 'sidebar' },
target: { parentId: 's1', handle: 'sidebar' },
})
expect(h.shadow.querySelector('.cms-slotref')).toBeNull()
await ptr('pointerup', 80, 200)
await h.settle()
})
it('holding the pointer at the viewport edge scrolls the page while dragging', async () => {
h = await setup()
await h.settle()
place('s1', 0, 0, 600, 300)
h.editor.ctrl.scan()
const scrolled: Array<[number, number]> = []
const realScrollBy = window.scrollBy
window.scrollBy = ((dx: number, dy: number) => void scrolled.push([dx, dy])) as typeof window.scrollBy
try {
h.editor.ctrl.beginPaletteDrag({ type: 'heading', label: 'heading' })
await ptr('pointermove', 400, window.innerHeight - 4)
await act(async () => void (await new Promise((r) => setTimeout(r, 40)))) // let the rAF autoscroll step run
expect(scrolled.length).toBeGreaterThan(0)
expect(scrolled[0]).toEqual([0, expect.any(Number)])
expect(scrolled[0][1]).toBeGreaterThan(0)
await ptr('pointerup', 400, window.innerHeight - 4)
await h.settle()
} finally {
window.scrollBy = realScrollBy
}
expect(h.editor.ctrl.store.get().drag).toBeNull()
})
})
describe('insertion slots (virtual "+" elements)', () => {