Drag-and-drop and keyboard swapping (Feature 14) #120

4 changed files with 53 additions and 13 deletions
Showing only changes of commit 621fdef42b - Show all commits
+25 -3
View File
@@ -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()
+3 -2
View File
@@ -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;
+1 -1
View File
@@ -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
View File
@@ -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))
}