Drag-and-drop and keyboard swapping (Feature 14) #120
@@ -1035,13 +1035,14 @@ describe.each([
|
|||||||
const float = () => screen.queryByTestId('drag-float')
|
const float = () => screen.queryByTestId('drag-float')
|
||||||
|
|
||||||
// press, move and release drive pointer 3 on the draggable: down at
|
// 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) {
|
function press(draggable: Element) {
|
||||||
fireEvent.pointerDown(draggable, { pointerId: 3, isPrimary: true, button: 0, clientX: 100, clientY: 100 })
|
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
|
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) {
|
function release(draggable: Element) {
|
||||||
fireEvent.pointerUp(draggable, { pointerId: 3, isPrimary: true, button: 0, clientX: 160, clientY: 140 })
|
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).boxShadow).toBe('var(--shadow-lg)')
|
||||||
expect(declaredStyle(copy).transform).toBe('rotate(-3deg)')
|
expect(declaredStyle(copy).transform).toBe('rotate(-3deg)')
|
||||||
expect(declaredStyle(copy).pointerEvents).toBe('none')
|
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.')
|
expect(announcer()).toHaveTextContent('Picked up Lemon orzo.')
|
||||||
const target = within(column('Friday')).getByText('Drop to swap').parentElement!
|
const target = within(column('Friday')).getByText('Drop to swap').parentElement!
|
||||||
expect(target).toHaveTextContent('Drop to swapReplaces Pizza night')
|
expect(target).toHaveTextContent('Drop to swapReplaces Pizza night')
|
||||||
@@ -1110,6 +1115,23 @@ describe.each([
|
|||||||
expect(puts(fetchMock)).toHaveLength(1)
|
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 () => {
|
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 })
|
const fetchMock = stubServer({ [`GET ${THIS_WEEK}`]: () => jsonResponse(200, planBody('draft')), ...dragLists })
|
||||||
await renderPage()
|
await renderPage()
|
||||||
|
|||||||
@@ -208,8 +208,9 @@
|
|||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The dragged copy floating under the pointer: lifted with the large
|
/* The dragged copy floating below right of the pointer: lifted with the
|
||||||
shadow and tilted -3deg. It lets the pointer through to the day under it. */
|
large shadow and tilted -3deg. It lets the pointer through to the day
|
||||||
|
under it. */
|
||||||
.floating {
|
.floating {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function Grip() {
|
|||||||
// Draggable is a dinner of the panel or the strip, dragged onto a day to
|
// 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
|
// 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
|
// 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({
|
function Draggable({
|
||||||
dinner,
|
dinner,
|
||||||
fromQuestLog,
|
fromQuestLog,
|
||||||
|
|||||||
+24
-7
@@ -195,14 +195,32 @@ function dateAt(x: number, y: number): string | null {
|
|||||||
// not one.
|
// not one.
|
||||||
const DRAG_THRESHOLD = 4
|
const DRAG_THRESHOLD = 4
|
||||||
|
|
||||||
// Press is a pointer held on a draggable: its id, where it went down, how
|
// The dragged copy floats this far below and to the right of the pointer,
|
||||||
// far that was from the draggable's top left, and whether it has become a
|
// as a drag image does, so it stays clear of the "Drop to swap" text
|
||||||
// drag.
|
// centred in the day the pointer is over. Where it would leave the viewport it flips to the
|
||||||
type Press = { pointerId: number; x: number; y: number; dx: number; dy: number; width: number; dragging: boolean }
|
// 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.
|
// Float is where the dragged copy of a draggable is drawn, and how wide.
|
||||||
export type Float = { left: number; top: number; width: number }
|
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
|
// 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
|
// 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
|
// 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,
|
pointerId: event.pointerId,
|
||||||
x: event.clientX,
|
x: event.clientX,
|
||||||
y: event.clientY,
|
y: event.clientY,
|
||||||
dx: event.clientX - rect.left,
|
|
||||||
dy: event.clientY - rect.top,
|
|
||||||
width: rect.width,
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,7 +278,7 @@ export function useDraggable(pickup: Pickup) {
|
|||||||
}
|
}
|
||||||
held.dragging = true
|
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))
|
plan.over(dateAt(event.clientX, event.clientY))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user