Holding a navigation key on the remote repeats it, like holding a key on a keyboard. Applies to the arrow keys, Tab, Backspace, Delete, Page Up, and Page Down.
{state.telemetry.available ? state.telemetry.consent === "undecided" ? "No choice recorded yet. Nothing is sent unless you choose Share diagnostics." : state.telemetry.consent === "enabled" ? "Consent recorded. You can turn this off at any time to delete queued reports." : "Opted out. No diagnostic reports are stored or sent." : "Diagnostic reporting is unavailable in this build."} Privacy policy
Holding a navigation key on the remote repeats it, like holding a key on a keyboard. Applies to the arrow keys, Tab, Backspace, Delete, Page Up, and Page Down.
{state.telemetry.available ? state.telemetry.consent === "undecided" ? "No choice recorded yet. Nothing is sent unless you choose Share diagnostics." : state.telemetry.consent === "enabled" ? "Consent recorded. You can turn this off at any time to delete queued reports." : "Opted out. No diagnostic reports are stored or sent." : "Diagnostic reporting is unavailable in this build."} Privacy policy
;
}
diff --git a/src/settings/Tabs.tsx b/src/settings/Tabs.tsx
new file mode 100644
index 00000000..5b84470f
--- /dev/null
+++ b/src/settings/Tabs.tsx
@@ -0,0 +1,64 @@
+import { useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from "react";
+
+export type TabDefinition = { id: string; label: string };
+
+export function tabId(id: string) { return `settings-tab-${id}`; }
+export function panelId(id: string) { return `settings-panel-${id}`; }
+
+const TABBABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
+
+export function Tabs({ tabs, active, onSelect, label }: { tabs: readonly TabDefinition[]; active: string; onSelect: (id: string) => void; label: string }) {
+ const listRef = useRef(null);
+ // The tab stop stays on the last tab that held focus, so arrowing to a tab and
+ // then tabbing away and back returns to it rather than to the selected tab.
+ const [focused, setFocused] = useState(null);
+ const tabStop = tabs.some((tab) => tab.id === focused) ? focused : active;
+ // Selecting a tab — by click or programmatically, as the update banner does —
+ // moves the stop back onto the selection, so it can never strand the tab stop
+ // on a tab that is no longer selected.
+ useEffect(() => { setFocused(null); }, [active]);
+
+ const focusTab = (index: number) => {
+ const buttons = listRef.current?.querySelectorAll('[role="tab"]');
+ buttons?.[(index + tabs.length) % tabs.length]?.focus();
+ };
+
+ const onKeyDown = (event: KeyboardEvent, index: number) => {
+ if (event.key === "ArrowRight") focusTab(index + 1);
+ else if (event.key === "ArrowLeft") focusTab(index - 1);
+ else if (event.key === "Home") focusTab(0);
+ else if (event.key === "End") focusTab(tabs.length - 1);
+ else return;
+ event.preventDefault();
+ };
+
+ return
+ {tabs.map((tab, index) => )}
+
;
+}
+
+export function TabPanel({ id, children }: { id: string; children: ReactNode }) {
+ const ref = useRef(null);
+ // A panel is only its own tab stop when nothing inside it can take focus,
+ // which the Updates panel hits while a check or install is in flight.
+ const [tabbable, setTabbable] = useState(false);
+ useEffect(() => {
+ // Keep the stop while the panel itself holds focus: removing tabIndex from
+ // the focused element would drop focus to the document body.
+ setTabbable((current) => !ref.current?.querySelector(TABBABLE) || (current && ref.current === document.activeElement));
+ });
+ return
{children}
;
+}
diff --git a/src/settings/UpdatesSection.tsx b/src/settings/UpdatesSection.tsx
index eedd21e3..2494df55 100644
--- a/src/settings/UpdatesSection.tsx
+++ b/src/settings/UpdatesSection.tsx
@@ -1,5 +1,7 @@
import { Download, RefreshCw, X } from "lucide-react";
-import type { UpdateState } from "../types";
+import type { Ref } from "react";
+import type { AppState, UpdateState } from "../types";
+import { SettingGroup } from "./controls";
export type UpdateAction = "check" | "download" | "install";
@@ -42,3 +44,7 @@ export function UpdateControls({ update, run, cancel }: { update: UpdateState; r