fix(tv): Alphabet-Sidebar per D-Pad/Fernbedienung navigierbar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-03-16 11:15:31 +01:00
parent bce5460fcf
commit 78dd9ebe03

View file

@ -36,9 +36,9 @@ class FocusManager {
el => el.classList.remove("input-editing")); el => el.classList.remove("input-editing"));
} }
if (e.target && e.target.hasAttribute && e.target.hasAttribute("data-focusable")) { if (e.target && e.target.hasAttribute && e.target.hasAttribute("data-focusable")) {
if (!e.target.closest("#tv-nav")) { if (!e.target.closest("#tv-nav") && !e.target.closest(".tv-alpha-sidebar")) {
// Nur echte Content-Elemente merken (nicht Filter/Controls) // Nur echte Content-Elemente merken (nicht Nav/Sidebar)
if (e.target.closest(".tv-grid, .tv-list-compact, .tv-detail-list, .tv-folder-view, .tv-row, .tv-episode-list, .tv-episode-grid, .tv-tabs, .tv-detail-actions, .tv-alpha-sidebar, .tv-view-switch, .tv-filter-bar, .tv-season-actions, .profiles-grid")) { if (e.target.closest(".tv-grid, .tv-list-compact, .tv-detail-list, .tv-folder-view, .tv-row, .tv-episode-list, .tv-episode-grid, .tv-tabs, .tv-detail-actions, .tv-view-switch, .tv-filter-bar, .tv-season-actions, .profiles-grid")) {
this._lastContentFocus = e.target; this._lastContentFocus = e.target;
} }
} }
@ -229,6 +229,70 @@ class FocusManager {
} }
} }
// ===== Alphabet-Sidebar Navigation =====
const inSidebar = active.closest(".tv-alpha-sidebar");
if (inSidebar) {
if (direction === "ArrowLeft") {
// Zurueck zum Content
if (this._lastContentFocus && document.contains(this._lastContentFocus)) {
this._lastContentFocus.focus();
} else {
const firstCard = document.querySelector(".tv-grid [data-focusable], .tv-card[data-focusable]");
if (firstCard) firstCard.focus();
}
e.preventDefault();
return;
}
if (direction === "ArrowUp" || direction === "ArrowDown") {
// Sequentiell durch Buchstaben
const letters = Array.from(document.querySelectorAll(".tv-alpha-letter[data-focusable]"))
.filter(el => el.offsetHeight > 0);
const idx = letters.indexOf(active);
const next = direction === "ArrowDown" ? idx + 1 : idx - 1;
if (next >= 0 && next < letters.length) {
letters[next].focus();
letters[next].scrollIntoView({ block: "nearest", behavior: "smooth" });
}
e.preventDefault();
return;
}
}
// ArrowRight am rechten Grid-Rand -> Sidebar
if (!inNav && !inSidebar && direction === "ArrowRight") {
const sidebar = document.getElementById("alpha-sidebar");
if (sidebar && sidebar.offsetHeight > 0) {
// Pruefen ob es noch ein Element rechts im Content gibt
const contentEls = focusables.filter(el => !el.closest("#tv-nav") && !el.closest(".tv-alpha-sidebar"));
const currentRect_r = active.getBoundingClientRect();
const rightNeighbor = contentEls.some(el => {
const r = el.getBoundingClientRect();
return r.left > currentRect_r.right + 5 && Math.abs(r.top - currentRect_r.top) < 100;
});
if (!rightNeighbor) {
// Kein Content rechts -> zur Sidebar springen
const sidebarLetters = Array.from(sidebar.querySelectorAll("[data-focusable]"))
.filter(el => el.offsetHeight > 0);
if (sidebarLetters.length > 0) {
// Naechstgelegenen Buchstaben vertikal finden
const cy_r = currentRect_r.top + currentRect_r.height / 2;
let best = sidebarLetters[0];
let bestDist_r = Infinity;
sidebarLetters.forEach(l => {
const lr = l.getBoundingClientRect();
const d = Math.abs(lr.top + lr.height / 2 - cy_r);
if (d < bestDist_r) { bestDist_r = d; best = l; }
});
this._lastContentFocus = active;
best.focus();
e.preventDefault();
return;
}
}
}
}
// Naechstes Element in Richtung finden (Nearest-Neighbor) // Naechstes Element in Richtung finden (Nearest-Neighbor)
const currentRect = active.getBoundingClientRect(); const currentRect = active.getBoundingClientRect();
const cx = currentRect.left + currentRect.width / 2; const cx = currentRect.left + currentRect.width / 2;
@ -240,7 +304,7 @@ class FocusManager {
// Nur Elemente im gleichen Bereich (Nav oder Content) bevorzugen // Nur Elemente im gleichen Bereich (Nav oder Content) bevorzugen
const searchEls = inNav const searchEls = inNav
? focusables.filter(el => el.closest("#tv-nav")) ? focusables.filter(el => el.closest("#tv-nav"))
: focusables.filter(el => !el.closest("#tv-nav")); : focusables.filter(el => !el.closest("#tv-nav") && !el.closest(".tv-alpha-sidebar"));
for (const el of searchEls) { for (const el of searchEls) {
if (el === active) continue; if (el === active) continue;