Cheaper, clearer dropzones: memoize, hatch refused, label slots as words
DropZones re-ran the accept predicates and a DOM walk on every overlay render (pointermove-driven). Matching now runs once per drag via matchZones(); scroll/resize ticks only re-measure the cached rects (new test pins the predicate count across ticks). Refused slots get a diagonal hatch on top of the red tint so the state is not color-only, slot tags show handles as words (`3d-accessories` -> "3d accessories", raw key kept in title), and the ghost pop honours prefers-reduced-motion. The mock section template now marks its sidebar slot like a real template, so slot tests use the fixture instead of hand-injected DOM (the old palette test asserting "no tagged slots" now expects both sections' slots). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
1 parent
a798bc1ad3
commit
7362e4730f
5 files changed
+114
-52
No files matched your search
+45
-12
@@ -3,6 +3,7 @@ import { act } from 'react'
|
||||
import { byId, click, setup } from './harness'
|
||||
import type { Harness } from './harness'
|
||||
import { Icon, TypeIcon, registerTypeIcon } from '../src/ui/Icon'
|
||||
import { slotLabel } from '../src/ui/Overlay'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
let h: Harness
|
||||
@@ -204,8 +205,7 @@ describe('block palette and drag & drop', () => {
|
||||
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>')
|
||||
// The section schema declares a `sidebar` slot taking text/button/footer; the fixture template marks it.
|
||||
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()
|
||||
@@ -223,7 +223,6 @@ describe('block palette and drag & drop', () => {
|
||||
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()
|
||||
@@ -267,7 +266,7 @@ describe('insertion slots (virtual "+" elements)', () => {
|
||||
h = await setup()
|
||||
await h.settle()
|
||||
await select('s1')
|
||||
expect(qa('.cms-slot').map((s) => s.getAttribute('aria-label'))).toEqual(['Add block at position 1', 'Add block at position 2', 'Add block at position 3', 'Add block at position 4'])
|
||||
expect(qa('.cms-slot').map((s) => s.getAttribute('aria-label'))).toEqual(['Add block at position 1', 'Add block at position 2', 'Add block at position 3', 'Add block at position 4', 'Add to sidebar'])
|
||||
await act(async () => void qa('.cms-slot')[1].click())
|
||||
await h.settle()
|
||||
expect([...h.shadow.querySelectorAll('.cms-dialog h4')].map((x) => x.textContent)).toEqual(['Content', 'Layout', 'Blueprints']) // grouped by x-graph.group
|
||||
@@ -419,7 +418,9 @@ describe('dropzone highlights', () => {
|
||||
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
|
||||
const s = slots()
|
||||
expect(s.map((el) => el.getAttribute('data-dropzone'))).toEqual(['slot:s1:sidebar', 'slot:s2:sidebar']) // both sections mark their sidebar slot
|
||||
expect(s.every((el) => !el.className.includes('is-refused'))).toBe(true) // sidebar takes text
|
||||
await stop()
|
||||
expect(qa('.cms-dropzone').length).toBe(0)
|
||||
})
|
||||
@@ -438,13 +439,12 @@ describe('dropzone highlights', () => {
|
||||
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.map((el) => el.getAttribute('data-dropzone'))).toEqual(['slot:s1:sidebar', 'slot:s2: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()
|
||||
@@ -453,17 +453,16 @@ describe('dropzone highlights', () => {
|
||||
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
|
||||
expect(slots()).toHaveLength(2)
|
||||
expect(slots().every((el) => !el.className.includes('is-refused'))).toBe(true) // 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(slots()).toHaveLength(2)
|
||||
expect(slots().every((el) => el.className.includes('is-refused'))).toBe(true) // sidebar takes text/button/footer
|
||||
expect(zones()).toContain('s1') // ... while the section itself still takes it
|
||||
await stop()
|
||||
})
|
||||
@@ -479,4 +478,38 @@ describe('dropzone highlights', () => {
|
||||
expect(ids).not.toContain('h1') // leaves are never dropzones
|
||||
await stop()
|
||||
})
|
||||
|
||||
it('zones match once per drag: scroll ticks re-measure but do not re-match', async () => {
|
||||
h = await setup()
|
||||
await h.settle()
|
||||
const ctrl = h.editor.ctrl
|
||||
let n = 0
|
||||
const orig = ctrl.accepts
|
||||
ctrl.accepts = (...args: [string, string]) => {
|
||||
n++
|
||||
return orig.call(ctrl, ...args)
|
||||
}
|
||||
try {
|
||||
ctrl.beginPaletteDrag({ type: 'text', label: 'Text' })
|
||||
await h.settle()
|
||||
expect(n).toBeGreaterThan(0) // matching ran: every container checked
|
||||
expect(qa('.cms-dropzone').length).toBeGreaterThan(0)
|
||||
ctrl.store.set({ tick: ctrl.store.get().tick + 1 }) // scroll/resize/mutation tick
|
||||
await h.settle()
|
||||
expect(qa('.cms-dropzone').length).toBeGreaterThan(0) // still drawn, rects re-measured ...
|
||||
const after = n
|
||||
ctrl.store.set({ tick: ctrl.store.get().tick + 1 })
|
||||
await h.settle()
|
||||
expect(n).toBe(after) // ... without re-running the accept predicates
|
||||
await stop()
|
||||
} finally {
|
||||
ctrl.accepts = orig
|
||||
}
|
||||
})
|
||||
|
||||
it('slot tags show handles as words', () => {
|
||||
expect(slotLabel('sidebar')).toBe('sidebar')
|
||||
expect(slotLabel('3d-accessories')).toBe('3d accessories')
|
||||
expect(slotLabel('hero_slot')).toBe('hero slot')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user