diff --git a/web/src/plan/PlanPage.test.tsx b/web/src/plan/PlanPage.test.tsx index 1205ef9..329dcc2 100644 --- a/web/src/plan/PlanPage.test.tsx +++ b/web/src/plan/PlanPage.test.tsx @@ -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() diff --git a/web/src/plan/PlanPanel.module.css b/web/src/plan/PlanPanel.module.css index 5693571..99eea60 100644 --- a/web/src/plan/PlanPanel.module.css +++ b/web/src/plan/PlanPanel.module.css @@ -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; diff --git a/web/src/plan/PlanPanel.tsx b/web/src/plan/PlanPanel.tsx index a2a4760..8bf035d 100644 --- a/web/src/plan/PlanPanel.tsx +++ b/web/src/plan/PlanPanel.tsx @@ -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, diff --git a/web/src/plan/drag.tsx b/web/src/plan/drag.tsx index deee358..724da6e 100644 --- a/web/src/plan/drag.tsx +++ b/web/src/plan/drag.tsx @@ -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)) }