diff --git a/app/ui/Header.jsx b/app/ui/Header.jsx index 07f83b6..c5b7ee6 100644 --- a/app/ui/Header.jsx +++ b/app/ui/Header.jsx @@ -4,7 +4,7 @@ import '../assets/Inter-Regular.ttf'; import { jsx, jsxs } from 'react/jsx-runtime'; -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { focusEnd, useLocale, useT } from '../i18n/index.js'; import { NO_FILTERS, activeCount, genreLabel, genres, getGame } from '../state/catalog.js'; import { useIsImmersive, useNavActions, useNavState } from '../state/nav.jsx'; @@ -26,12 +26,20 @@ import { FocusScope, Text, View } from '@ps5-react/core'; function Header({ children }) { const t2 = useT(); const { rtl } = useLocale(); - const { section, filters, on, step, keyOf, browseFocus } = useSections(); + const { section, shown, filters, on, step, keyOf, browseFocus, leaveBrowse } = useSections(); const wishlist = useWishlist(); const favorites = wishlist.filter((id) => getGame(id)).length; const nav = useNavActions(); const { top } = useNavState(); const [browsing, setBrowsing] = (0, useState)(false); + const browsingNow = (0, useRef)(false); + browsingNow.current = browsing; + // A closed menu returns focus a frame later: only then can it tell whether focus left Browse. + (0, useEffect)(() => { + if (browsing) return void 0; + const id = setTimeout(leaveBrowse, 50); + return () => clearTimeout(id); + }, [browsing]); const [choosingLanguage, setChoosingLanguage] = (0, useState)(false); const immersive = useIsImmersive(); const [layouts, track] = useNavLayouts(); @@ -61,7 +69,7 @@ import { FocusScope, Text, View } from '@ps5-react/core'; /* @__PURE__ */ (0, jsx)(DownloadToast, { actionRef: toastAction }), choosingLanguage ? /* @__PURE__ */ (0, jsx)(LanguageSheet, { onClose: () => setChoosingLanguage(false) }) : null, /* @__PURE__ */ (0, jsx)(View, { style: immersive ? { position: "absolute", left: 0, right: 0, top: 0, zIndex: 40, display: "none" } : { position: "absolute", left: 0, right: 0, top: 0, zIndex: 40 }, children: /* @__PURE__ */ (0, jsx)(FocusScope, { focusKey: "header", inert: immersive, children: /* @__PURE__ */ (0, jsxs)(View, { onLayout: track("header"), style: { height: 96, flexDirection: "row", alignItems: "center", justifyContent: "space-between", backgroundColor: "#000000", paddingHorizontal: 48 }, children: [ - /* @__PURE__ */ (0, jsx)(NavPill, { section, layouts }), + /* @__PURE__ */ (0, jsx)(NavPill, { section: shown, layouts }), /* @__PURE__ */ (0, jsxs)(View, { style: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 24 }, children: [ /* @__PURE__ */ (0, jsx)(View, { style: rtl ? { marginRight: -24 } : { marginLeft: -24 }, children: /* @__PURE__ */ (0, jsx)( NavItem, @@ -69,7 +77,7 @@ import { FocusScope, Text, View } from '@ps5-react/core'; focusKey: keyOf("search"), icon: "search", label: t2("nav.search"), - current: section === "search", + current: shown === "search", onPress: on.search, onLayout: track("search"), nextFocusDown: down @@ -82,7 +90,7 @@ import { FocusScope, Text, View } from '@ps5-react/core'; { focusKey: keyOf("discover"), label: t2("nav.discover"), - current: section === "discover", + current: shown === "discover", onPress: on.discover, onLayout: track("discover"), nextFocusDown: down @@ -95,11 +103,15 @@ import { FocusScope, Text, View } from '@ps5-react/core'; focusKey: keyOf("catalog"), label: t2("nav.browse"), trailing: browsing ? "chevronUp" : "chevronDown", - current: section === "catalog", + current: shown === "catalog", onPress: () => setBrowsing((open) => !open), onLayout: track("catalog"), nextFocusDown: down, - ...browseFocus + onFocus: browseFocus.onFocus, + onBlur: () => { + browseFocus.onBlur(); + if (!browsingNow.current) leaveBrowse(); + } } ), /* @__PURE__ */ (0, jsxs)(Menu, { open: browsing, onClose: () => setBrowsing(false), rows: genres.length + 2, children: [ @@ -140,7 +152,7 @@ import { FocusScope, Text, View } from '@ps5-react/core'; { focusKey: keyOf("top"), label: t2("nav.top"), - current: section === "top", + current: shown === "top", onPress: on.top, onLayout: track("top"), nextFocusDown: down @@ -151,7 +163,7 @@ import { FocusScope, Text, View } from '@ps5-react/core'; { focusKey: keyOf("new"), label: t2("nav.new"), - current: section === "new", + current: shown === "new", onPress: on.new, onLayout: track("new"), nextFocusDown: down @@ -165,7 +177,7 @@ import { FocusScope, Text, View } from '@ps5-react/core'; focusKey: keyOf("favorites"), icon: "heart", label: favorites ? String(favorites) : null, - current: section === "favorites", + current: shown === "favorites", onPress: on.favorites, onLayout: track("favorites"), nextFocusDown: down, @@ -177,7 +189,7 @@ import { FocusScope, Text, View } from '@ps5-react/core'; { focusKey: keyOf("downloads"), icon: "download", - current: section === "downloads", + current: shown === "downloads", onPress: on.downloads, onLayout: track("downloads"), nextFocusDown: down, @@ -189,7 +201,7 @@ import { FocusScope, Text, View } from '@ps5-react/core'; { focusKey: keyOf("files"), icon: "folder", - current: section === "files", + current: shown === "files", onPress: on.files, onLayout: track("files"), nextFocusDown: down diff --git a/app/ui/useSections.js b/app/ui/useSections.js index 5f7b3a6..1906721 100644 --- a/app/ui/useSections.js +++ b/app/ui/useSections.js @@ -55,6 +55,9 @@ import { useQueryActive, useStoreActions, useStoreState } from '../state/store.j const section = top.name === "search" ? "search" : top.name === "game" ? null : underneath; const topName = (0, useRef)(top.name); topName.current = top.name; + // L1/R1 stopped on Browse: the pill and the item show it as the current section, like any other + // tab, while the page stays where it was until ✕ opens the menu and a choice there shows the library. + const [stopped, setStopped] = (0, useState)(false); const [on] = (0, useState)(() => ({ show, search: () => topName.current !== "search" && nav.push({ name: "search" }, "nav:search"), @@ -70,9 +73,11 @@ import { useQueryActive, useStoreActions, useStoreState } from '../state/store.j const next = TABS[TABS.indexOf(current) + (forward ? 1 : -1)]; if (!next) return; if (next === "catalog") { + setStopped(true); nav.focusLater(next === section ? "nav:current" : KEYS2.catalog); return; } + setStopped(false); if (next === "downloads" || next === "files") { store.setQuery(""); nav.home(); @@ -87,7 +92,10 @@ import { useQueryActive, useStoreActions, useStoreState } from '../state/store.j // The page's top elements go up to `nav:current`, the current section's item, like a PS5 tab // row; the scope's own memory would be stale after L1/R1 or a closed menu. keyOf: (name) => name === section ? "nav:current" : KEYS2[name], - browseFocus: { onFocus: () => onBrowse = true, onBlur: () => onBrowse = false } + shown: stopped ? "catalog" : section, + browseFocus: { onFocus: () => onBrowse = true, onBlur: () => onBrowse = false }, + // The stop ends once focus is off Browse and its menu is closed. + leaveBrowse: () => onBrowse || setStopped(false) }; }