Highlight every dropzone on the page while dragging

Palette blueprints, searched nodes and moving blocks shared one engine
but only the single current target line was visible, so there was no way
to see where a dragged item could land. While a drag is active the
overlay now outlines every valid dropzone with the engine's own
predicates (accepts / acceptsInSlot, blueprint refusal, self-exclusion):
containers in green, fitting slots in blue with a name tag, refused slots
dimmed red with a name tag so they read as "exists but not for this".

Tests: palette fit + slot-fallback, blueprint container/refused-slot,
node fit/refuse on the same slot, move self-exclusion; typecheck clean.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
mats committed 2026-11-09 09:19:59 +01:00
1 parent 5c5c34d25a
commit a798bc1ad3
3 files changed
+154

No files matched your search

+81
View File
@@ -399,3 +399,84 @@ describe('sorting root nodes (snap between siblings)', () => {
expect(kids('l1')).toEqual(['x2', 'x1'])
})
})
describe('dropzone highlights', () => {
const zones = () => qa('.cms-dropzone:not(.cms-dropzone--slot)').map((el) => el.getAttribute('data-dropzone'))
const slots = () => qa('.cms-dropzone--slot')
const stop = async () => {
await act(async () => void h.editor.ctrl.cancelDrag())
await h.settle()
}
it('a palette drag outlines every container the type fits, and clears on drop/cancel', async () => {
h = await setup()
await h.settle()
expect(qa('.cms-dropzone').length).toBe(0) // idle: no highlights
h.editor.ctrl.beginPaletteDrag({ type: 'text', label: 'Text' })
await h.settle()
const ids = zones()
expect(ids).toContain('s1') // section takes text as a plain child
expect(ids).toContain('s2')
expect(ids).toContain('l1') // layout takes text too
expect(ids).not.toContain('t1') // leaves are never dropzones
expect(slots().length).toBe(0) // the fixture page has no tagged slots
await stop()
expect(qa('.cms-dropzone').length).toBe(0)
})
it('a type that only fits through a slot still lights up the container', async () => {
h = await setup()
await h.settle()
expect(h.editor.ctrl.accepts('s1', 'footer')).toBe(false) // not a plain child ...
expect(h.editor.ctrl.handleForDrop('s1', 'footer')).toBe('sidebar') // ... but fits the slot
h.editor.ctrl.beginPaletteDrag({ type: 'footer', label: 'Footer' })
await h.settle()
expect(zones()).toContain('s1') // the drop lands in its slot, so it glows
await stop()
})
it('a blueprint drag lights up containers and marks every slot refused', async () => {
h = await setup()
await h.settle()
byId('s1').insertAdjacentHTML('beforeend', '<div data-cms-slot="sidebar" data-cms-slot-parent="s1"></div>')
h.editor.ctrl.scan()
h.editor.ctrl.beginPaletteDrag({ blueprint: 'hero', label: 'Hero' })
await h.settle()
expect(zones()).toContain('s1') // typeless: every container fits
const s = slots()
expect(s.map((el) => el.getAttribute('data-dropzone'))).toEqual(['slot:s1:sidebar'])
expect(s[0].className).toContain('is-refused') // blueprints cannot land in slots
expect(s[0].querySelector('.cms-dropzone__tag')!.textContent).toBe('sidebar')
await stop()
})
it('a node drag accepts a fitting slot and refuses one that does not take the type', async () => {
h = await setup()
await h.settle()
byId('s1').insertAdjacentHTML('beforeend', '<div data-cms-slot="sidebar" data-cms-slot-parent="s1"></div>')
h.editor.ctrl.scan()
h.editor.ctrl.beginExistingDrag({ id: 'orphan1', type: 'text', label: 'sparrows' })
await h.settle()
expect(slots().length).toBe(1)
expect(slots()[0].className).not.toContain('is-refused') // sidebar takes text
await stop()
h.editor.ctrl.beginExistingDrag({ id: 'orphan1', type: 'heading', label: 'headline' })
await h.settle()
expect(slots().length).toBe(1)
expect(slots()[0].className).toContain('is-refused') // sidebar takes text/button/footer
expect(zones()).toContain('s1') // ... while the section itself still takes it
await stop()
})
it('moving a block excludes itself (and never highlights leaves)', async () => {
h = await setup()
await h.settle()
h.editor.ctrl.beginDrag('s1')
await h.settle()
const ids = zones()
expect(ids).not.toContain('s1') // cannot drop into itself
expect(ids).toEqual(expect.arrayContaining(['s2', 'l1'])) // sections nest, so siblings still take it
expect(ids).not.toContain('h1') // leaves are never dropzones
await stop()
})
})