FIX - stabilize homescreen app dragging

This commit is contained in:
DerEchteAlec
2026-08-17 00:12:33 +02:00
parent 0475831b15
commit 181206f3cf
8 changed files with 150 additions and 9 deletions
@@ -0,0 +1,45 @@
import { describe, expect, it, vi } from 'vitest'
import { bindPointerDragSession } from '@/utils/pointerDragSession'
function pointerEvent(type: string, pointerId: number): PointerEvent {
const event = new Event(type)
Object.defineProperty(event, 'pointerId', { value: pointerId })
return event as PointerEvent
}
describe('pointer drag session', () => {
it('keeps tracking the active pointer at window scope until pointerup', () => {
const target = new EventTarget()
const move = vi.fn()
const up = vi.fn()
const cancel = vi.fn()
bindPointerDragSession(target as Window, 7, { cancel, move, up })
target.dispatchEvent(pointerEvent('pointermove', 8))
target.dispatchEvent(pointerEvent('pointermove', 7))
target.dispatchEvent(new Event('lostpointercapture'))
target.dispatchEvent(pointerEvent('pointerup', 7))
target.dispatchEvent(pointerEvent('pointermove', 7))
expect(move).toHaveBeenCalledTimes(1)
expect(up).toHaveBeenCalledTimes(1)
expect(cancel).not.toHaveBeenCalled()
})
it('cancels and removes listeners when the NUI window loses focus', () => {
const target = new EventTarget()
const move = vi.fn()
const up = vi.fn()
const cancel = vi.fn()
bindPointerDragSession(target as Window, 3, { cancel, move, up })
target.dispatchEvent(new Event('blur'))
target.dispatchEvent(pointerEvent('pointermove', 3))
target.dispatchEvent(pointerEvent('pointerup', 3))
expect(cancel).toHaveBeenCalledTimes(1)
expect(move).not.toHaveBeenCalled()
expect(up).not.toHaveBeenCalled()
})
})
+54
View File
@@ -0,0 +1,54 @@
type PointerDragSessionTarget = Pick<
Window,
'addEventListener' | 'removeEventListener'
>
type PointerDragSessionHandlers = {
cancel: () => void
move: (event: PointerEvent) => void
up: (event: PointerEvent) => void
}
export function bindPointerDragSession(
target: PointerDragSessionTarget,
pointerId: number,
handlers: PointerDragSessionHandlers,
): () => void {
let active = true
const cleanup = (): void => {
if (!active) return
active = false
target.removeEventListener('pointermove', onPointerMove, true)
target.removeEventListener('pointerup', onPointerUp, true)
target.removeEventListener('pointercancel', onPointerCancel, true)
target.removeEventListener('blur', onBlur, true)
}
const matchesPointer = (event: PointerEvent): boolean =>
active && event.pointerId === pointerId
const onPointerMove = (event: PointerEvent): void => {
if (matchesPointer(event)) handlers.move(event)
}
const onPointerUp = (event: PointerEvent): void => {
if (!matchesPointer(event)) return
cleanup()
handlers.up(event)
}
const onPointerCancel = (event: PointerEvent): void => {
if (!matchesPointer(event)) return
cleanup()
handlers.cancel()
}
const onBlur = (): void => {
if (!active) return
cleanup()
handlers.cancel()
}
target.addEventListener('pointermove', onPointerMove, true)
target.addEventListener('pointerup', onPointerUp, true)
target.addEventListener('pointercancel', onPointerCancel, true)
target.addEventListener('blur', onBlur, true)
return cleanup
}
@@ -113,4 +113,23 @@ describe('springboard app page layout', () => {
targetPage: 2,
})
})
it('does not reserve a visible slot for an unavailable shortcut', () => {
const grid = new Array<HomeSlot>(HOME_GRID_PAGE_SIZE).fill(null)
grid[0] = app('available-one')
grid[1] = app('unavailable')
grid[2] = app('available-two')
const pages = layoutSpringboardHomePages(
grid,
new Map(),
1,
(item) => typeof item !== 'string' || item !== 'unavailable',
)
expect(
pages[0]?.cells.slice(0, 3).map((cell) => cell?.item ?? null),
).toEqual(['available-one', 'available-two', null])
expect(pages[0]?.cells[1]).toMatchObject({ sourceIndex: 2 })
})
})
+2 -1
View File
@@ -38,6 +38,7 @@ export function layoutSpringboardHomePages(
grid: readonly HomeSlot[],
occupiedByPage: ReadonlyMap<number, ReadonlySet<number>>,
minimumPageCount: number,
isRenderable: (item: HomeItem) => boolean = () => true,
): SpringboardHomePage[] {
const pageCount = Math.max(
1,
@@ -55,7 +56,7 @@ export function layoutSpringboardHomePages(
for (let offset = 0; offset < HOME_GRID_PAGE_SIZE; offset += 1) {
const sourceIndex = pageStart + offset
const item = grid[sourceIndex]
if (!item) continue
if (!item || !isRenderable(item)) continue
pending.push({
item,
renderKey: homeItemRenderKey(item, appOccurrences),