Fix season browser focus: land on first episode on down, ignore parked-cursor hover
- Add data-nav-entry-col-from-above so moving down from the season selector always focuses the first episode instead of the tile visually closest to the selected season poster; moving back up still returns to the current season. - Require recent real mouse movement before treating mouseover as mouse intent: scroll/re-render under a stationary cursor no longer activates mouse mode, so sideways season browsing no longer loses focus to the episode tile under the parked pointer.
This commit is contained in:
1 parent
9041061e86
commit
931f00f647
3 files changed
+33
-1
No files matched your search
@@ -548,6 +548,9 @@ function getEpisodeNavAttrs(seasonIndex: number, episodeIndex: number) {
|
||||
...navAttrs(coords.row, coords.col),
|
||||
"data-season-index": seasonIndex,
|
||||
"data-episode-index": episodeIndex,
|
||||
// Entering the episode grid from above (the season selector) always
|
||||
// lands on the first episode instead of the visually closest tile.
|
||||
...(episodeIndex === 0 ? { "data-nav-entry-col-from-above": "0" } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ type InputModality = "mouse" | "keyboard" | "gamepad"
|
||||
const MOUSE_IDLE_MS = 1400
|
||||
const MOUSE_INTENT_DISTANCE_PX = 28
|
||||
const MOUSE_INTENT_WINDOW_MS = 700
|
||||
const MOUSE_OVER_INTENT_RECENCY_MS = 500
|
||||
const MOUSE_INTENT_SELECTOR = [
|
||||
"[data-nav-focusable]",
|
||||
"button",
|
||||
@@ -28,6 +29,7 @@ let mouseIdleTimer: number | null = null
|
||||
let pointerVisible = false
|
||||
let mouseTravelPx = 0
|
||||
let lastMouseMoveAt = 0
|
||||
let lastAnyMouseMoveAt = 0
|
||||
|
||||
function clearMouseIdleTimer() {
|
||||
if (mouseIdleTimer !== null) {
|
||||
@@ -95,6 +97,7 @@ function registerMouseIntentTravel(event: MouseEvent): boolean {
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
reportUserActivity()
|
||||
showPointerFromMotion()
|
||||
lastAnyMouseMoveAt = performance.now()
|
||||
|
||||
if (modality === "mouse") {
|
||||
applyInputState(true)
|
||||
@@ -111,6 +114,13 @@ function handleMouseOver(event: MouseEvent) {
|
||||
if (modality === "mouse") return
|
||||
if (!isMouseIntentTarget(event.target)) return
|
||||
|
||||
// Browsers fire mouseover/mouseenter when scrolling or re-rendering moves
|
||||
// content under a stationary cursor (e.g. sideways season browsing while
|
||||
// the pointer happens to rest over the episode grid). Without recent real
|
||||
// pointer motion this is not mouse intent — activating mouse input here
|
||||
// would let hover handlers steal keyboard/gamepad focus.
|
||||
if (performance.now() - lastAnyMouseMoveAt > MOUSE_OVER_INTENT_RECENCY_MS) return
|
||||
|
||||
// Entering an interactive target indicates likely mouse intent.
|
||||
activateMouseInput()
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ const FOCUSABLE_ATTR = "data-nav-focusable"
|
||||
const ROW_ATTR = "data-nav-row"
|
||||
const COL_ATTR = "data-nav-col"
|
||||
const ENTRY_COL_ATTR = "data-nav-entry-col"
|
||||
// Like ENTRY_COL_ATTR, but only applies when entering the row from above
|
||||
// (ArrowDown). Lets a row declare a fixed landing column for downward entry
|
||||
// without hijacking upward or horizontal moves.
|
||||
const ENTRY_COL_FROM_ABOVE_ATTR = "data-nav-entry-col-from-above"
|
||||
const SYNC_SCROLL_ROW_ATTR = "data-sync-scroll-row"
|
||||
const SYNC_SCROLL_GROUP_ATTR = "data-sync-scroll-group"
|
||||
const DEFAULT_SYNC_SCROLL_GROUP = "browse"
|
||||
@@ -672,8 +676,23 @@ function findNextElement(
|
||||
desiredCol.value = currentCol
|
||||
}
|
||||
|
||||
const entryTarget = findElementAt(targetRow, targetCol, true)
|
||||
const targetRowElements = byRow.get(targetRow) ?? []
|
||||
|
||||
if (direction === "down") {
|
||||
for (const el of targetRowElements) {
|
||||
const fromAboveCol = el.element.getAttribute(ENTRY_COL_FROM_ABOVE_ATTR)
|
||||
if (fromAboveCol === null) continue
|
||||
const fromAboveTarget = targetRowElements.find(
|
||||
(e) => e.col === parseInt(fromAboveCol, 10),
|
||||
)
|
||||
if (fromAboveTarget) {
|
||||
desiredCol.value = fromAboveTarget.col
|
||||
return fromAboveTarget.element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entryTarget = findElementAt(targetRow, targetCol, true)
|
||||
const hasEntryOverride = targetRowElements.some((el) => el.element.hasAttribute(ENTRY_COL_ATTR))
|
||||
if (hasEntryOverride) {
|
||||
return entryTarget?.element || null
|
||||
|
||||
Reference in new issue
Block a user