From 3b7c349a74232870fa03e3e45981aa3b0e84b2b5 Mon Sep 17 00:00:00 2001 From: HighImKevin Date: Fri, 9 Oct 2026 17:30:40 -0400 Subject: [PATCH] Prevent Home Category rail from stealing and trapping focus on TV display mode --- scripts/homeScreen.css | 29 ++++++++- scripts/homeScreen3.js | 144 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 163 insertions(+), 10 deletions(-) diff --git a/scripts/homeScreen.css b/scripts/homeScreen.css index 44d578d..9931772 100644 --- a/scripts/homeScreen.css +++ b/scripts/homeScreen.css @@ -151,6 +151,32 @@ div#discovery-loading-indicator:not(.show)::after { justify-content: start; } +/* Chevron mode: clickable Discover More (desktop optional; TV focus stop) */ +#indexPage[data-discovery-ready]:not([data-discovery-exhausted]) #discovery-loading-indicator:not(.show) { + cursor: pointer; +} + +.layout-tv #discovery-loading-indicator.discovery-load-more-focusable { + outline: none; + min-height: 3em; + padding: 0.5em 1em; + box-sizing: border-box; +} + +.layout-tv #discovery-loading-indicator.discovery-load-more-focusable.show-focus:focus { + background-color: var(--theme-primary-color, #00a4dc) !important; + background: var(--theme-primary-color, #00a4dc) !important; + color: #fff !important; + outline: none; + box-shadow: none; + border-radius: 8px; +} + +.layout-tv #discovery-loading-indicator.discovery-load-more-focusable.show-focus:focus::before, +.layout-tv #discovery-loading-indicator.discovery-load-more-focusable.show-focus:focus::after { + color: #fff !important; +} + .discovery-loading-indicator .spinner { width: 30px; height: 30px; @@ -447,9 +473,6 @@ html:not([data-active-page="home"]) #kefin-home-create-section-btn { :where(#kefin-home-category-rail) .kefin-home-category-rail:hover, :where(#kefin-home-category-rail) .kefin-home-category-rail:focus-within { opacity: 1; - background: var(--jf-palette-AppBar-darkBg, var(--AppBar-background)) !important; - border-radius: 0; - box-shadow: 1px 1px 1px 1px #fff !important; } .kefin-home-category-btn-label { display: block; diff --git a/scripts/homeScreen3.js b/scripts/homeScreen3.js index 1ed4906..85d1f68 100644 --- a/scripts/homeScreen3.js +++ b/scripts/homeScreen3.js @@ -154,6 +154,8 @@ container.parentNode.appendChild(loadingDiv); } } + bindDiscoveryLoadMoreControl(loadingDiv); + syncDiscoveryLoadMoreFocus(loadingDiv); return loadingDiv; } @@ -175,7 +177,8 @@ container.parentNode.appendChild(loadingDiv); } LOG('Created discovery loading indicator as sibling of sections container'); - + bindDiscoveryLoadMoreControl(loadingDiv); + syncDiscoveryLoadMoreFocus(loadingDiv); return loadingDiv; } @@ -183,6 +186,74 @@ document.querySelectorAll('#discovery-loading-indicator').forEach((el) => el.remove()); } + function isDiscoveryLoadMoreInteractive() { + if (state.discoveryMode !== 'chevron') return false; + if (state.discoverySectionsRemain === false) return false; + if (state.isRenderingDiscovery) return false; + const page = getDiscoveryHomePageEl(); + if (page?.dataset?.discoveryExhausted === 'true') return false; + const indicator = document.querySelector('#discovery-loading-indicator'); + if (indicator?.classList.contains('show')) return false; + return true; + } + + function syncDiscoveryLoadMoreFocus(indicator) { + const el = indicator + || document.querySelector('.homePage:not(.hide) #discovery-loading-indicator') + || document.querySelector('.libraryPage:not(.hide) #discovery-loading-indicator') + || document.querySelector('#discovery-loading-indicator'); + if (!el) return; + + const tv = document.documentElement.classList.contains('layout-tv'); + const interactive = isDiscoveryLoadMoreInteractive(); + + if (tv && interactive) { + el.setAttribute('role', 'button'); + el.setAttribute('tabindex', '0'); + el.setAttribute('aria-label', 'Discover More'); + el.classList.add('show-focus'); + el.classList.add('discovery-load-more-focusable'); + } else { + el.setAttribute('tabindex', '-1'); + el.removeAttribute('role'); + el.classList.remove('show-focus'); + el.classList.remove('discovery-load-more-focusable'); + if (state.discoverySectionsRemain === false || getDiscoveryHomePageEl()?.dataset?.discoveryExhausted === 'true') { + el.setAttribute('aria-label', 'No More Content to Discover'); + } else { + el.removeAttribute('aria-label'); + } + } + } + + function activateDiscoveryLoadMore() { + if (!isDiscoveryLoadMoreInteractive()) return false; + if (!isDiscoveryHomeContext()) return false; + LOG('Discover More activated; loading next discovery group...'); + renderNextDiscoveryGroup(); + return true; + } + + function bindDiscoveryLoadMoreControl(indicator) { + if (!indicator || indicator.dataset.loadMoreBound === '1') return; + indicator.dataset.loadMoreBound = '1'; + + indicator.addEventListener('click', (e) => { + if (!isDiscoveryLoadMoreInteractive()) return; + e.preventDefault(); + e.stopPropagation(); + activateDiscoveryLoadMore(); + }); + + indicator.addEventListener('keydown', (e) => { + if (e.key !== 'Enter' && e.key !== ' ') return; + if (!isDiscoveryLoadMoreInteractive()) return; + e.preventDefault(); + e.stopPropagation(); + activateDiscoveryLoadMore(); + }); + } + function getDiscoveryHomePageEl() { return document.querySelector('#indexPage') || document.querySelector('.homePage:not(.hide)') @@ -198,6 +269,7 @@ else delete page.dataset.infiniteScroll; if (exhausted) page.dataset.discoveryExhausted = 'true'; else delete page.dataset.discoveryExhausted; + syncDiscoveryLoadMoreFocus(); } function showDiscoveryLoadingIndicator() { @@ -208,6 +280,7 @@ loadingIndicator.classList.add('show'); loadingIndicator.style.visibility = 'visible'; loadingIndicator.style.display = 'flex'; + syncDiscoveryLoadMoreFocus(loadingIndicator); } function hideDiscoveryLoadingIndicator() { @@ -222,6 +295,7 @@ loadingIndicator.style.visibility = 'hidden'; loadingIndicator.style.display = 'none'; } + syncDiscoveryLoadMoreFocus(loadingIndicator); } function isDiscoveryHomeContext() { @@ -271,6 +345,7 @@ loadingIndicator.classList.remove('show'); loadingIndicator.style.visibility = ''; loadingIndicator.style.display = ''; + syncDiscoveryLoadMoreFocus(loadingIndicator); } LOG('Discovery exhausted — No More Content to Discover'); } @@ -1491,6 +1566,7 @@ container.dataset.loadingDiscovery = 'true'; container.classList.add('loading-discovery'); showDiscoveryLoadingIndicator(); + syncDiscoveryLoadMoreFocus(); // Chevron mode: nudge scroll so newly appended sections start entering the viewport if (state.discoveryMode === 'chevron') { @@ -1825,7 +1901,11 @@ window.addEventListener('wheel', handleWheel, { passive: true }); window.addEventListener('touchstart', handleTouchStart, { passive: true }); window.addEventListener('touchmove', handleTouchMove, { passive: true }); - LOG('Chevron Discover More enabled for discovery (scroll-again at bottom).'); + + const indicator = createDiscoveryLoadingIndicator(target); + bindDiscoveryLoadMoreControl(indicator); + syncDiscoveryLoadMoreFocus(indicator); + LOG('Chevron Discover More enabled for discovery (scroll-again at bottom / TV Enter).'); } function isInSeasonalPeriod(start, end) { @@ -2392,6 +2472,11 @@ return false; } + // Undrilled spotlight metadata is a leftmost content stop (Left → category rail) + if (el.classList?.contains('metadata-container') && el.dataset?.metadataDrilled !== 'true') { + return true; + } + const row = el.closest?.('.focuscontainer-x, .itemsContainer-tv, .sectionTitleContainer, .spotlight-section-title-container'); if (row) { const focusables = Array.from( @@ -2445,15 +2530,60 @@ return true; } + function isVisibleFocusTarget(el) { + if (!el || !document.contains(el)) return false; + const style = window.getComputedStyle(el); + if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) { + return false; + } + const rect = el.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + } + + /** Live spotlight target after slide change (inactive/drilled nodes go stale). */ + function getActiveSpotlightReturnFocus() { + const activeSlide = document.querySelector( + '.homeSectionsContainer .spotlight-item[data-active="true"], .sections .spotlight-item[data-active="true"]', + ); + if (!activeSlide) return null; + const meta = activeSlide.querySelector('.metadata-container'); + if (meta && isVisibleFocusTarget(meta)) { + if (meta.dataset.metadataDrilled === 'true') { + meta.dataset.metadataDrilled = 'false'; + meta.setAttribute('tabindex', '0'); + meta.querySelectorAll( + 'a.spotlight-genre-link, a.spotlight-person-link, a.spotlight-studio-link', + ).forEach((link) => link.setAttribute('tabindex', '-1')); + } + return meta; + } + const header = activeSlide.closest('.spotlight-section')?.querySelector('.spotlight-header-container') + || document.querySelector('.spotlight-section .spotlight-header-container'); + if (header) { + const focusable = Array.from( + header.querySelectorAll( + 'button:not([disabled]):not([tabindex="-1"]), a[href]:not([tabindex="-1"]), [tabindex]:not([tabindex="-1"])', + ), + ).find((node) => isVisibleFocusTarget(node)); + if (focusable) return focusable; + } + return null; + } + function exitCategoryRailToContent() { const buttons = getRailCategoryButtons(); - const returnEl = - (homeChromeRailReturnFocus && document.contains(homeChromeRailReturnFocus) && homeChromeRailReturnFocus) || - document.querySelector( + let returnEl = homeChromeRailReturnFocus; + homeChromeRailReturnFocus = null; + + if (!isVisibleFocusTarget(returnEl)) { + returnEl = getActiveSpotlightReturnFocus(); + } + if (!isVisibleFocusTarget(returnEl)) { + returnEl = document.querySelector( '.homeSectionsContainer .card.itemAction, .homeSectionsContainer a.sectionTitle-link, .sections .card.itemAction', ); - homeChromeRailReturnFocus = null; - if (returnEl && typeof returnEl.focus === 'function') { + } + if (returnEl && typeof returnEl.focus === 'function' && isVisibleFocusTarget(returnEl)) { try { returnEl.focus({ preventScroll: true }); } catch (_) {