Refine movie detail cast rail and nav behavior
This commit is contained in:
@@ -92,7 +92,8 @@
|
|||||||
:best="index === 0"
|
:best="index === 0"
|
||||||
:selectable="!!version.playable_file"
|
:selectable="!!version.playable_file"
|
||||||
:disabled="!version.playable_file"
|
:disabled="!version.playable_file"
|
||||||
v-bind="navAttrs(2, index, index === 0 ? 0 : undefined)"
|
data-nav-release-item="true"
|
||||||
|
v-bind="navAttrs(2 + index, 0, 0)"
|
||||||
@activate="handleVersionActivate(version, $event)"
|
@activate="handleVersionActivate(version, $event)"
|
||||||
@keydown="handleVersionShortcutKeydown($event, version)"
|
@keydown="handleVersionShortcutKeydown($event, version)"
|
||||||
@contextmenu="handleVersionContextMenu($event, version)"
|
@contextmenu="handleVersionContextMenu($event, version)"
|
||||||
@@ -105,12 +106,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="limitedMovieCast.length > 0" class="cast-list" data-sync-scroll-row="true">
|
<div
|
||||||
|
v-if="limitedMovieCast.length > 0"
|
||||||
|
class="cast-list"
|
||||||
|
data-sync-scroll-row="true"
|
||||||
|
data-nav-cast-row="true"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
v-for="(castMember, castIndex) in limitedMovieCast"
|
v-for="(castMember, castIndex) in limitedMovieCast"
|
||||||
:key="`${castMember.name}-${castMember.character || ''}`"
|
:key="`${castMember.name}-${castMember.character || ''}`"
|
||||||
class="cast-card media-card"
|
class="cast-card media-card"
|
||||||
v-bind="navAttrs(2, movieVersions.length + castIndex)"
|
data-nav-cast-item="true"
|
||||||
|
v-bind="navAttrs(2 + movieVersions.length, castIndex)"
|
||||||
:href="`#/?q=${encodeURIComponent(castMember.name)}`"
|
:href="`#/?q=${encodeURIComponent(castMember.name)}`"
|
||||||
:title="`Search for ${castMember.name}`"
|
:title="`Search for ${castMember.name}`"
|
||||||
@click.prevent="handleCastSelect(castMember.name)"
|
@click.prevent="handleCastSelect(castMember.name)"
|
||||||
@@ -208,7 +215,11 @@ import castPlaceholderMaleUrl from "../assets/cast-placeholder-male.svg"
|
|||||||
import SeriesFullView from "./SeriesFullView.vue"
|
import SeriesFullView from "./SeriesFullView.vue"
|
||||||
import ReleaseVersionCard from "./ReleaseVersionCard.vue"
|
import ReleaseVersionCard from "./ReleaseVersionCard.vue"
|
||||||
import ReleaseActionMenu from "./ReleaseActionMenu.vue"
|
import ReleaseActionMenu from "./ReleaseActionMenu.vue"
|
||||||
import { navAttrs } from "../composables/useKeyboardNavigation"
|
import {
|
||||||
|
navAttrs,
|
||||||
|
registerOutOfBoundsNavigationHandler,
|
||||||
|
FOCUSABLE_ATTR,
|
||||||
|
} from "../composables/useKeyboardNavigation"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
item: MediaItem
|
item: MediaItem
|
||||||
@@ -230,6 +241,64 @@ const videoStates = ref<string[]>([])
|
|||||||
const COLLAGE_SLOT_COUNT = 5
|
const COLLAGE_SLOT_COUNT = 5
|
||||||
const safariAutoplay = isSafariBrowser()
|
const safariAutoplay = isSafariBrowser()
|
||||||
const COLLAGE_START_OFFSETS_SECONDS = [0, 8, 6, 4, 2]
|
const COLLAGE_START_OFFSETS_SECONDS = [0, 8, 6, 4, 2]
|
||||||
|
const DESKTOP_NAV_SHORTCUT_MIN_WIDTH = 900
|
||||||
|
|
||||||
|
let disposeOutOfBoundsHandler: (() => void) | null = null
|
||||||
|
let lastReleaseShortcutRow: number | null = null
|
||||||
|
|
||||||
|
function getDesktopCastFirstItem(): HTMLElement | null {
|
||||||
|
const castRow = document.querySelector<HTMLElement>('[data-nav-cast-row="true"]')
|
||||||
|
if (!castRow) return null
|
||||||
|
return castRow.querySelector<HTMLElement>(`[data-nav-cast-item="true"][${FOCUSABLE_ATTR}="true"]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReleaseAtRow(row: number): HTMLElement | null {
|
||||||
|
return document.querySelector<HTMLElement>(
|
||||||
|
`[data-nav-release-item="true"][data-nav-row="${row}"][data-nav-col="0"][${FOCUSABLE_ATTR}="true"]`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastReleaseRowBefore(castRow: number): number | null {
|
||||||
|
const releases = Array.from(document.querySelectorAll<HTMLElement>('[data-nav-release-item="true"]'))
|
||||||
|
let best: number | null = null
|
||||||
|
for (const release of releases) {
|
||||||
|
const row = parseInt(release.getAttribute("data-nav-row") || "", 10)
|
||||||
|
if (!Number.isFinite(row) || row >= castRow) continue
|
||||||
|
if (best === null || row > best) best = row
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerMovieOutOfBoundsShortcut() {
|
||||||
|
disposeOutOfBoundsHandler?.()
|
||||||
|
disposeOutOfBoundsHandler = registerOutOfBoundsNavigationHandler((context) => {
|
||||||
|
if (window.innerWidth <= DESKTOP_NAV_SHORTCUT_MIN_WIDTH) return null
|
||||||
|
if (props.item.type !== "movies") return null
|
||||||
|
|
||||||
|
const { current, direction, currentRow, currentCol } = context
|
||||||
|
|
||||||
|
if (direction === "right" && current.hasAttribute("data-nav-release-item")) {
|
||||||
|
const firstCast = getDesktopCastFirstItem()
|
||||||
|
if (firstCast) {
|
||||||
|
lastReleaseShortcutRow = currentRow
|
||||||
|
return firstCast
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
direction === "left" &&
|
||||||
|
current.hasAttribute("data-nav-cast-item") &&
|
||||||
|
currentCol === 0
|
||||||
|
) {
|
||||||
|
const targetRow = lastReleaseShortcutRow ?? getLastReleaseRowBefore(currentRow)
|
||||||
|
if (targetRow === null) return null
|
||||||
|
return getReleaseAtRow(targetRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function setVideoRef(el: HTMLVideoElement | null, index: number) {
|
function setVideoRef(el: HTMLVideoElement | null, index: number) {
|
||||||
videoRefs.value[index] = el
|
videoRefs.value[index] = el
|
||||||
@@ -340,6 +409,8 @@ onMounted(() => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
startStaggeredPlayback()
|
startStaggeredPlayback()
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
|
registerMovieOutOfBoundsShortcut()
|
||||||
})
|
})
|
||||||
|
|
||||||
const showreelSourceSets = computed((): string[][] | null => {
|
const showreelSourceSets = computed((): string[][] | null => {
|
||||||
@@ -648,6 +719,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener("keydown", handleMovieMenuKeydown, true)
|
document.removeEventListener("keydown", handleMovieMenuKeydown, true)
|
||||||
|
disposeOutOfBoundsHandler?.()
|
||||||
|
disposeOutOfBoundsHandler = null
|
||||||
|
lastReleaseShortcutRow = null
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -736,10 +810,14 @@ onUnmounted(() => {
|
|||||||
.content-main {
|
.content-main {
|
||||||
grid-area: main;
|
grid-area: main;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-sidebar {
|
.content-sidebar {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-left {
|
.sidebar-left {
|
||||||
@@ -955,16 +1033,18 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cast-list {
|
.cast-list {
|
||||||
|
--sync-row-tail: 0px;
|
||||||
|
--cast-safe-start: 40vw;
|
||||||
|
--cast-safe-end: 32px;
|
||||||
|
--sync-row-right-deadzone: 32px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(-1 * (var(--header-height) + 30px));
|
top: calc(-1 * (var(--header-height) + 30px));
|
||||||
left: calc(-50vw + 50% + 40vw - 0.8rem);
|
left: calc(-50vw + 50%);
|
||||||
width: calc(100vw - (40vw - 0.8rem));
|
width: 100vw;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-top: 4px;
|
padding: 4px calc(var(--cast-safe-end) + var(--sync-row-tail)) 8px
|
||||||
padding-right: 32px;
|
calc(var(--cast-safe-start) + var(--sync-row-tail));
|
||||||
padding-bottom: 8px;
|
z-index: 0;
|
||||||
padding-left: 0;
|
|
||||||
z-index: 2;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -1093,14 +1173,15 @@ html:not(.mouse-active) .cast-card.nav-focused::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cast-list {
|
.cast-list {
|
||||||
position: static;
|
--cast-safe-start: 32px;
|
||||||
|
--cast-safe-end: 32px;
|
||||||
|
--sync-row-right-deadzone: 32px;
|
||||||
|
position: relative;
|
||||||
top: auto;
|
top: auto;
|
||||||
left: auto;
|
left: -32px;
|
||||||
width: auto;
|
width: calc(100% + 64px);
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,20 @@ export interface FocusableElement {
|
|||||||
col: number
|
col: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NavDirection = "up" | "down" | "left" | "right"
|
||||||
|
|
||||||
|
export interface OutOfBoundsNavigationContext {
|
||||||
|
current: HTMLElement
|
||||||
|
direction: NavDirection
|
||||||
|
currentRow: number
|
||||||
|
currentCol: number
|
||||||
|
byRow: Map<number, FocusableElement[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OutOfBoundsNavigationHandler = (
|
||||||
|
context: OutOfBoundsNavigationContext,
|
||||||
|
) => HTMLElement | null | undefined
|
||||||
|
|
||||||
// Global focus state
|
// Global focus state
|
||||||
const focusedElement = ref<HTMLElement | null>(null)
|
const focusedElement = ref<HTMLElement | null>(null)
|
||||||
const isNavigating = ref(false)
|
const isNavigating = ref(false)
|
||||||
@@ -25,6 +39,7 @@ const SYNC_SCROLL_FIRST_CONTENT_ROW = 2
|
|||||||
const SYNC_SCROLL_DEADZONE_RATIO = 0.18
|
const SYNC_SCROLL_DEADZONE_RATIO = 0.18
|
||||||
const SYNC_SCROLL_EASING_MS = 220
|
const SYNC_SCROLL_EASING_MS = 220
|
||||||
const SYNC_SCROLL_TAIL_VAR = "--sync-row-tail"
|
const SYNC_SCROLL_TAIL_VAR = "--sync-row-tail"
|
||||||
|
const SYNC_SCROLL_RIGHT_DEADZONE_VAR = "--sync-row-right-deadzone"
|
||||||
|
|
||||||
let syncedRowsFrame: number | null = null
|
let syncedRowsFrame: number | null = null
|
||||||
let syncedRowsCurrentOffset = 0
|
let syncedRowsCurrentOffset = 0
|
||||||
@@ -32,6 +47,18 @@ let syncedRowsTargetOffset = 0
|
|||||||
let syncedRowsTailPx = 0
|
let syncedRowsTailPx = 0
|
||||||
let lastSyncedAnchorCol: number | null = null
|
let lastSyncedAnchorCol: number | null = null
|
||||||
let lastSyncedRowsAnimationAt: number | null = null
|
let lastSyncedRowsAnimationAt: number | null = null
|
||||||
|
const outOfBoundsHandlers = new Set<OutOfBoundsNavigationHandler>()
|
||||||
|
|
||||||
|
function resolveOutOfBoundsNavigation(
|
||||||
|
context: OutOfBoundsNavigationContext,
|
||||||
|
): HTMLElement | null {
|
||||||
|
const handlers = Array.from(outOfBoundsHandlers)
|
||||||
|
for (let i = handlers.length - 1; i >= 0; i--) {
|
||||||
|
const result = handlers[i]?.(context)
|
||||||
|
if (result) return result
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function getSyncedRows(): HTMLElement[] {
|
function getSyncedRows(): HTMLElement[] {
|
||||||
return Array.from(document.querySelectorAll<HTMLElement>(`[${SYNC_SCROLL_ROW_ATTR}="true"]`))
|
return Array.from(document.querySelectorAll<HTMLElement>(`[${SYNC_SCROLL_ROW_ATTR}="true"]`))
|
||||||
@@ -48,6 +75,8 @@ function getSyncedRowMetrics(rows: HTMLElement[]) {
|
|||||||
|
|
||||||
const rowStyle = window.getComputedStyle(row)
|
const rowStyle = window.getComputedStyle(row)
|
||||||
const paddingLeft = parseFloat(rowStyle.paddingLeft || "0")
|
const paddingLeft = parseFloat(rowStyle.paddingLeft || "0")
|
||||||
|
const rightDeadzoneRaw = rowStyle.getPropertyValue(SYNC_SCROLL_RIGHT_DEADZONE_VAR).trim()
|
||||||
|
const rightDeadzoneOverride = rightDeadzoneRaw ? parseFloat(rightDeadzoneRaw) : NaN
|
||||||
let gap = parseFloat(rowStyle.columnGap || rowStyle.gap || "0")
|
let gap = parseFloat(rowStyle.columnGap || rowStyle.gap || "0")
|
||||||
|
|
||||||
if (cards.length > 1) {
|
if (cards.length > 1) {
|
||||||
@@ -60,6 +89,9 @@ function getSyncedRowMetrics(rows: HTMLElement[]) {
|
|||||||
stride: cardWidth + gap,
|
stride: cardWidth + gap,
|
||||||
paddingLeft,
|
paddingLeft,
|
||||||
viewportWidth: row.clientWidth,
|
viewportWidth: row.clientWidth,
|
||||||
|
rightDeadzoneOverride: Number.isFinite(rightDeadzoneOverride)
|
||||||
|
? Math.max(0, rightDeadzoneOverride)
|
||||||
|
: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,10 +212,11 @@ function updateSyncedRowTarget(anchorCol: number, anchorRow: HTMLElement | null
|
|||||||
metrics.paddingLeft,
|
metrics.paddingLeft,
|
||||||
(metrics.viewportWidth - metrics.cardWidth) * SYNC_SCROLL_DEADZONE_RATIO,
|
(metrics.viewportWidth - metrics.cardWidth) * SYNC_SCROLL_DEADZONE_RATIO,
|
||||||
)
|
)
|
||||||
|
const rightDeadzone = metrics.rightDeadzoneOverride ?? deadzoneInset
|
||||||
const minVisibleLeft = deadzoneInset
|
const minVisibleLeft = deadzoneInset
|
||||||
const maxVisibleLeft = Math.max(
|
const maxVisibleLeft = Math.max(
|
||||||
minVisibleLeft,
|
minVisibleLeft,
|
||||||
metrics.viewportWidth - metrics.cardWidth - deadzoneInset,
|
metrics.viewportWidth - metrics.cardWidth - rightDeadzone,
|
||||||
)
|
)
|
||||||
const itemLeft = metrics.paddingLeft + anchorCol * metrics.stride
|
const itemLeft = metrics.paddingLeft + anchorCol * metrics.stride
|
||||||
const viewportLeft = itemLeft - effectiveAnchorOffset
|
const viewportLeft = itemLeft - effectiveAnchorOffset
|
||||||
@@ -226,6 +259,30 @@ function updateSyncedRowTarget(anchorCol: number, anchorRow: HTMLElement | null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLocalSyncedRowCol(
|
||||||
|
anchorRow: HTMLElement,
|
||||||
|
element: HTMLElement,
|
||||||
|
requestedCol: number,
|
||||||
|
): number {
|
||||||
|
const cards = Array.from(anchorRow.querySelectorAll<HTMLElement>(`.media-card[${FOCUSABLE_ATTR}]`))
|
||||||
|
if (cards.length === 0) return Math.max(0, requestedCol)
|
||||||
|
|
||||||
|
const cardCols = cards
|
||||||
|
.map((card) => parseInt(card.getAttribute(COL_ATTR) || "", 10))
|
||||||
|
.filter((col) => Number.isFinite(col))
|
||||||
|
|
||||||
|
if (cardCols.length > 0) {
|
||||||
|
const minCol = Math.min(...cardCols)
|
||||||
|
const localFromRequested = requestedCol - minCol
|
||||||
|
return Math.max(0, Math.min(localFromRequested, cards.length - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackIndex = cards.indexOf(element)
|
||||||
|
if (fallbackIndex >= 0) return fallbackIndex
|
||||||
|
|
||||||
|
return Math.max(0, Math.min(requestedCol, cards.length - 1))
|
||||||
|
}
|
||||||
|
|
||||||
function syncRowsToElement(element: HTMLElement) {
|
function syncRowsToElement(element: HTMLElement) {
|
||||||
const row = parseInt(element.getAttribute(ROW_ATTR) || "0", 10)
|
const row = parseInt(element.getAttribute(ROW_ATTR) || "0", 10)
|
||||||
if (row < SYNC_SCROLL_FIRST_CONTENT_ROW) {
|
if (row < SYNC_SCROLL_FIRST_CONTENT_ROW) {
|
||||||
@@ -234,8 +291,14 @@ function syncRowsToElement(element: HTMLElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentCol = parseInt(element.getAttribute(COL_ATTR) || "0", 10)
|
const currentCol = parseInt(element.getAttribute(COL_ATTR) || "0", 10)
|
||||||
const anchorCol = desiredCol.value ?? currentCol
|
|
||||||
const anchorRow = element.closest<HTMLElement>(`[${SYNC_SCROLL_ROW_ATTR}="true"]`)
|
const anchorRow = element.closest<HTMLElement>(`[${SYNC_SCROLL_ROW_ATTR}="true"]`)
|
||||||
|
if (!anchorRow) {
|
||||||
|
resetSyncedRows()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedCol = desiredCol.value ?? currentCol
|
||||||
|
const anchorCol = getLocalSyncedRowCol(anchorRow, element, requestedCol)
|
||||||
updateSyncedRowTarget(anchorCol, anchorRow)
|
updateSyncedRowTarget(anchorCol, anchorRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +477,7 @@ function findElementClosestToLogicalViewportX(
|
|||||||
*/
|
*/
|
||||||
function findNextElement(
|
function findNextElement(
|
||||||
current: HTMLElement,
|
current: HTMLElement,
|
||||||
direction: "up" | "down" | "left" | "right",
|
direction: NavDirection,
|
||||||
): HTMLElement | null {
|
): HTMLElement | null {
|
||||||
const currentRow = parseInt(current.getAttribute(ROW_ATTR) || "0", 10)
|
const currentRow = parseInt(current.getAttribute(ROW_ATTR) || "0", 10)
|
||||||
const currentCol = parseInt(current.getAttribute(COL_ATTR) || "0", 10)
|
const currentCol = parseInt(current.getAttribute(COL_ATTR) || "0", 10)
|
||||||
@@ -432,7 +495,15 @@ function findNextElement(
|
|||||||
|
|
||||||
// Find element with target col in this row
|
// Find element with target col in this row
|
||||||
const target = rowElements.find((e) => e.col === targetCol)
|
const target = rowElements.find((e) => e.col === targetCol)
|
||||||
return target?.element || null
|
if (target?.element) return target.element
|
||||||
|
|
||||||
|
return resolveOutOfBoundsNavigation({
|
||||||
|
current,
|
||||||
|
direction,
|
||||||
|
currentRow,
|
||||||
|
currentCol,
|
||||||
|
byRow,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// Vertical: move to adjacent row, try to maintain column
|
// Vertical: move to adjacent row, try to maintain column
|
||||||
const sortedRows = Array.from(byRow.keys()).sort((a, b) => a - b)
|
const sortedRows = Array.from(byRow.keys()).sort((a, b) => a - b)
|
||||||
@@ -443,7 +514,15 @@ function findNextElement(
|
|||||||
const delta = direction === "down" ? 1 : -1
|
const delta = direction === "down" ? 1 : -1
|
||||||
const targetRowIdx = currentRowIdx + delta
|
const targetRowIdx = currentRowIdx + delta
|
||||||
|
|
||||||
if (targetRowIdx < 0 || targetRowIdx >= sortedRows.length) return null
|
if (targetRowIdx < 0 || targetRowIdx >= sortedRows.length) {
|
||||||
|
return resolveOutOfBoundsNavigation({
|
||||||
|
current,
|
||||||
|
direction,
|
||||||
|
currentRow,
|
||||||
|
currentCol,
|
||||||
|
byRow,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const targetRow = sortedRows[targetRowIdx]
|
const targetRow = sortedRows[targetRowIdx]
|
||||||
|
|
||||||
@@ -480,7 +559,16 @@ function findNextElement(
|
|||||||
targetCol,
|
targetCol,
|
||||||
metrics,
|
metrics,
|
||||||
)
|
)
|
||||||
return closestByViewport?.element || entryTarget?.element || null
|
const resolved = closestByViewport?.element || entryTarget?.element || null
|
||||||
|
if (resolved) return resolved
|
||||||
|
|
||||||
|
return resolveOutOfBoundsNavigation({
|
||||||
|
current,
|
||||||
|
direction,
|
||||||
|
currentRow,
|
||||||
|
currentCol,
|
||||||
|
byRow,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,4 +802,11 @@ export function navAttrs(row: number, col: number, entryCol?: number) {
|
|||||||
return attrs
|
return attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerOutOfBoundsNavigationHandler(handler: OutOfBoundsNavigationHandler) {
|
||||||
|
outOfBoundsHandlers.add(handler)
|
||||||
|
return () => {
|
||||||
|
outOfBoundsHandlers.delete(handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export { FOCUSABLE_ATTR, ROW_ATTR, COL_ATTR, ENTRY_COL_ATTR }
|
export { FOCUSABLE_ATTR, ROW_ATTR, COL_ATTR, ENTRY_COL_ATTR }
|
||||||
|
|||||||
Reference in New Issue
Block a user