fix(ui): polish board→rack recall drag and its gesture interplay
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 52s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m13s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 52s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m13s
- The rack now animates the opening drop gap for a board-tile recall drag too
(the reorder transition was gated on a rack-source drag only).
- While a board tile is dragged over the rack (a recall, gap shown), the ✅
confirm and its preview caption are hidden so they don't sit over the gap;
they return if the tile is dragged back out over the board.
- A recall drop lands off the boardwrap, so its pointerup never reached the
boardwrap handler and left a stale id in the active-pointer set — the next
board swipe then read as multi-touch and the shuffle / history pull went dead.
Reconcile the pointer when a drag ends or cancels. Regression e2e added.
This commit is contained in:
@@ -43,6 +43,29 @@ test('a swipe-down on the zoom-out board opens the history', async ({ page }) =>
|
||||
await expect(page.locator('.boardwrap.slid')).toBeVisible();
|
||||
});
|
||||
|
||||
test('a recall drag onto the rack does not break the next board swipe', async ({ page }) => {
|
||||
await openGame(page);
|
||||
// Place a tile, then drag it back onto the rack — the recall's pointerup lands off the
|
||||
// boardwrap, which once left a stale pointer there so the next swipe read as multi-touch.
|
||||
await page.locator('.rack .tile').first().click();
|
||||
await page.locator('[data-cell]:not(.filled)').nth(30).click();
|
||||
const pending = page.locator('[data-cell].pending');
|
||||
await expect(pending).toHaveCount(1);
|
||||
const fb = (await pending.first().boundingBox())!;
|
||||
const slot = (await page.locator('[data-rack] .tile').nth(2).boundingBox())!;
|
||||
await page.mouse.move(fb.x + fb.width / 2, fb.y + fb.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(slot.x + 2, slot.y + slot.height / 2, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
await expect(pending).toHaveCount(0);
|
||||
|
||||
// The swipe-down still opens the history (the recall pointer was reconciled).
|
||||
await page.locator('.stage').evaluate((el) => (el.scrollTop = 0));
|
||||
const box = (await page.locator('.boardwrap').boundingBox())!;
|
||||
await touchSwipeDown(page, box.y + 20, box.y + 180);
|
||||
await expect(page.locator('.history')).toBeVisible();
|
||||
});
|
||||
|
||||
test('the history is a per-seat grid with move scores but no names or running total', async ({ page }) => {
|
||||
await openGame(page);
|
||||
await page.locator('.scoreboard').click(); // open the history (gesture is covered separately)
|
||||
|
||||
+12
-2
@@ -378,6 +378,10 @@
|
||||
// While a placed (pending) board tile is dragged to relocate it, draggingPend is its cell —
|
||||
// hidden from the board (the ghost stands in) like a lifted rack tile.
|
||||
let draggingPend = $state<{ row: number; col: number } | null>(null);
|
||||
// A board tile dragged over the rack (a recall in progress, the rack showing a drop gap): the
|
||||
// move's ✅ confirm and its preview caption are hidden so they don't sit over the opening gap;
|
||||
// they return the moment the tile is dragged back out over the board.
|
||||
const recallOverRack = $derived(draggingPend != null && reorderTo != null);
|
||||
|
||||
let dragPointerId = -1;
|
||||
function beginDrag(src: DragSrc, e: PointerEvent) {
|
||||
@@ -398,6 +402,7 @@
|
||||
window.removeEventListener('pointermove', onWinMove);
|
||||
window.removeEventListener('pointerup', onWinUp);
|
||||
window.removeEventListener('pointerdown', onExtraPointer);
|
||||
boardPointers.delete(dragPointerId); // see onWinUp: don't leave a stale boardwrap pointer
|
||||
clearHover();
|
||||
clearReorder();
|
||||
downInfo = null;
|
||||
@@ -516,6 +521,11 @@
|
||||
window.removeEventListener('pointermove', onWinMove);
|
||||
window.removeEventListener('pointerup', onWinUp);
|
||||
window.removeEventListener('pointerdown', onExtraPointer);
|
||||
// A board-tile drag also registered this pointer on the boardwrap (the tile sits inside it),
|
||||
// but it can be released off the boardwrap (e.g. a recall dropped on the rack), where the
|
||||
// boardwrap's own pointerup never fires. Reconcile it here so no stale id is left to make the
|
||||
// next swipe read as multi-touch and kill the shuffle / history-pull gesture.
|
||||
boardPointers.delete(e.pointerId);
|
||||
clearHover();
|
||||
const di = downInfo;
|
||||
downInfo = null;
|
||||
@@ -1205,7 +1215,7 @@
|
||||
<span class="turn-ind">{isMyTurn ? t('game.yourTurn') : turnLabel()}</span>
|
||||
{/if}
|
||||
<span class="scores">
|
||||
{#if preview}{preview.legal ? t('game.previewWords', { words: preview.words.join(', '), n: preview.score }) : t('game.previewIllegal')}{:else if !view.game.multipleWordsPerTurn}<span class="oneword" title={t('game.oneWordRule')}>1️⃣</span>{/if}
|
||||
{#if recallOverRack}{:else if preview}{preview.legal ? t('game.previewWords', { words: preview.words.join(', '), n: preview.score }) : t('game.previewIllegal')}{:else if !view.game.multipleWordsPerTurn}<span class="oneword" title={t('game.oneWordRule')}>1️⃣</span>{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1227,7 +1237,7 @@
|
||||
ondown={onRackDown}
|
||||
/>
|
||||
</div>
|
||||
{#if !gameOver && placement.pending.length > 0}
|
||||
{#if !gameOver && placement.pending.length > 0 && !recallOverRack}
|
||||
<button class="make" onclick={commit} disabled={busy || !isMyTurn || !connection.online || !preview?.legal} aria-label={t('game.makeMove')}>✅</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rack" class:reordering={draggingId != null} class:landscape data-rack>
|
||||
<div class="rack" class:reordering={draggingId != null || dropIndex != null} class:landscape data-rack>
|
||||
{#each shown as slot, i (slot.id)}
|
||||
<button
|
||||
class="tile"
|
||||
|
||||
Reference in New Issue
Block a user