Drag-and-drop and keyboard swapping (Feature 14) #120
@@ -1035,13 +1035,14 @@ describe.each([
|
||||
const float = () => screen.queryByTestId('drag-float')
|
||||
|
||||
// press, move and release drive pointer 3 on the draggable: down at
|
||||
// (100, 100), moved over an element (or none), and let go there.
|
||||
// (100, 100), moved over an element (or none) at (160, 140) or the given
|
||||
// point, and let go there.
|
||||
function press(draggable: Element) {
|
||||
fireEvent.pointerDown(draggable, { pointerId: 3, isPrimary: true, button: 0, clientX: 100, clientY: 100 })
|
||||
}
|
||||
function move(draggable: Element, over: Element | null) {
|
||||
function move(draggable: Element, over: Element | null, [clientX, clientY] = [160, 140]) {
|
||||
hovered = over
|
||||
fireEvent.pointerMove(draggable, { pointerId: 3, isPrimary: true, clientX: 160, clientY: 140 })
|
||||
fireEvent.pointerMove(draggable, { pointerId: 3, isPrimary: true, clientX, clientY })
|
||||
}
|
||||
function release(draggable: Element) {
|
||||
fireEvent.pointerUp(draggable, { pointerId: 3, isPrimary: true, button: 0, clientX: 160, clientY: 140 })
|
||||
@@ -1079,6 +1080,10 @@ describe.each([
|
||||
expect(declaredStyle(copy).boxShadow).toBe('var(--shadow-lg)')
|
||||
expect(declaredStyle(copy).transform).toBe('rotate(-3deg)')
|
||||
expect(declaredStyle(copy).pointerEvents).toBe('none')
|
||||
// Offset 48 px below right of the pointer at (160, 140), clear of the
|
||||
// day's text under it.
|
||||
expect(copy.style.left).toBe('208px')
|
||||
expect(copy.style.top).toBe('188px')
|
||||
expect(announcer()).toHaveTextContent('Picked up Lemon orzo.')
|
||||
const target = within(column('Friday')).getByText('Drop to swap').parentElement!
|
||||
expect(target).toHaveTextContent('Drop to swapReplaces Pizza night')
|
||||
@@ -1110,6 +1115,23 @@ describe.each([
|
||||
expect(puts(fetchMock)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('flips the floating copy to the left of and above the pointer where it would leave the viewport', async () => {
|
||||
stubServer({ [`GET ${THIS_WEEK}`]: () => jsonResponse(200, planBody('draft')), ...dragLists })
|
||||
await renderPage()
|
||||
await screen.findByText('Pizza night')
|
||||
const orzo = await quest('Lemon orzo')
|
||||
vi.spyOn(orzo, 'getBoundingClientRect').mockReturnValue(DOMRect.fromRect({ x: 90, y: 90, width: 240, height: 60 }))
|
||||
|
||||
press(orzo)
|
||||
const [x, y] = [window.innerWidth - 100, window.innerHeight - 40]
|
||||
move(orzo, document.body, [x, y])
|
||||
const copy = float()!
|
||||
expect(copy.style.width).toBe('240px')
|
||||
expect(copy.style.left).toBe(`${x - 48 - 240}px`)
|
||||
expect(copy.style.top).toBe(`${y - 48 - 60}px`)
|
||||
release(orzo)
|
||||
})
|
||||
|
||||
it('cancels a drag let go over no day, or cancelled, with no swap', async () => {
|
||||
const fetchMock = stubServer({ [`GET ${THIS_WEEK}`]: () => jsonResponse(200, planBody('draft')), ...dragLists })
|
||||
await renderPage()
|
||||
|
||||
@@ -208,8 +208,9 @@
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* The dragged copy floating under the pointer: lifted with the large
|
||||
shadow and tilted -3deg. It lets the pointer through to the day under it. */
|
||||
/* The dragged copy floating below right of the pointer: lifted with the
|
||||
large shadow and tilted -3deg. It lets the pointer through to the day
|
||||
under it. */
|
||||
.floating {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
|
||||
@@ -60,7 +60,7 @@ function Grip() {
|
||||
// Draggable is a dinner of the panel or the strip, dragged onto a day to
|
||||
// swap it in: a button whose content is the row or the chip. While it is
|
||||
// dragged it is a dashed ghost, and with a pointer a copy of it floats
|
||||
// under the pointer, lifted and tilted.
|
||||
// below right of the pointer, lifted and tilted.
|
||||
function Draggable({
|
||||
dinner,
|
||||
fromQuestLog,
|
||||
|
||||
+24
-7
@@ -195,14 +195,32 @@ function dateAt(x: number, y: number): string | null {
|
||||
// not one.
|
||||
const DRAG_THRESHOLD = 4
|
||||
|
||||
// Press is a pointer held on a draggable: its id, where it went down, how
|
||||
// far that was from the draggable's top left, and whether it has become a
|
||||
// drag.
|
||||
type Press = { pointerId: number; x: number; y: number; dx: number; dy: number; width: number; dragging: boolean }
|
||||
// The dragged copy floats this far below and to the right of the pointer,
|
||||
// as a drag image does, so it stays clear of the "Drop to swap" text
|
||||
// centred in the day the pointer is over. Where it would leave the viewport it flips to the
|
||||
// left of the pointer, or above it.
|
||||
const FLOAT_OFFSET = 48
|
||||
|
||||
// Press is a pointer held on a draggable: its id, where it went down, the
|
||||
// draggable's size, and whether it has become a drag.
|
||||
type Press = { pointerId: number; x: number; y: number; width: number; height: number; dragging: boolean }
|
||||
|
||||
// Float is where the dragged copy of a draggable is drawn, and how wide.
|
||||
export type Float = { left: number; top: number; width: number }
|
||||
|
||||
// floatAt is where the dragged copy of the held draggable is drawn for the
|
||||
// pointer at (x, y): offset below right of it, flipped where that would
|
||||
// leave the viewport.
|
||||
function floatAt(held: Press, x: number, y: number): Float {
|
||||
const right = x + FLOAT_OFFSET
|
||||
const below = y + FLOAT_OFFSET
|
||||
return {
|
||||
left: right + held.width > window.innerWidth ? x - FLOAT_OFFSET - held.width : right,
|
||||
top: below + held.height > window.innerHeight ? y - FLOAT_OFFSET - held.height : below,
|
||||
width: held.width,
|
||||
}
|
||||
}
|
||||
|
||||
// useDraggable makes a panel row or a strip chip a draggable dinner. With a
|
||||
// pointer it is pressed, captured and moved past DRAG_THRESHOLD to pick it
|
||||
// up, its copy floats with the pointer, and letting go over a day drops it
|
||||
@@ -243,9 +261,8 @@ export function useDraggable(pickup: Pickup) {
|
||||
pointerId: event.pointerId,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
dx: event.clientX - rect.left,
|
||||
dy: event.clientY - rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
dragging: false,
|
||||
}
|
||||
}
|
||||
@@ -261,7 +278,7 @@ export function useDraggable(pickup: Pickup) {
|
||||
}
|
||||
held.dragging = true
|
||||
}
|
||||
setFloat({ left: event.clientX - held.dx, top: event.clientY - held.dy, width: held.width })
|
||||
setFloat(floatAt(held, event.clientX, event.clientY))
|
||||
plan.over(dateAt(event.clientX, event.clientY))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user