diff --git a/src/ui/src/components/settings/SettingsDialog.tsx b/src/ui/src/components/settings/SettingsDialog.tsx index 0845dbdb..d0ab49c9 100644 --- a/src/ui/src/components/settings/SettingsDialog.tsx +++ b/src/ui/src/components/settings/SettingsDialog.tsx @@ -214,7 +214,7 @@ export function SettingsDialog() { className="flex-1 min-w-0 h-full overflow-hidden" > -
+
{activeTab?.id === "general" && ( )} diff --git a/src/ui/src/components/settings/SettingsNav.test.tsx b/src/ui/src/components/settings/SettingsNav.test.tsx index ba8e9148..ca16f33d 100644 --- a/src/ui/src/components/settings/SettingsNav.test.tsx +++ b/src/ui/src/components/settings/SettingsNav.test.tsx @@ -118,3 +118,24 @@ describe("SettingsNav (#803) keyboard navigation", () => { expect(buttons[0]).toHaveAttribute("tabindex", "-1"); }); }); + +describe("SettingsNav (#785) scrollable when window height is reduced", () => { + it("renders the tablist inside a scroll-area viewport, not a plain overflow div", () => { + // #785: the nav previously scrolled via a bare `overflow-y-auto` div, whose + // scrollbar is the OS/browser's native one — which can render with zero + // visible width (observed on Linux/GTK themes) or stay hidden until + // scrolled (macOS default), leaving no visible affordance that more tabs + // exist below the fold. The tablist must live inside the shared + // `ScrollArea` (the same styled-scrollbar component the settings panel + // uses) so it gets a visible, cross-OS-consistent scrollbar whenever its + // content overflows. + render(); + + const tablist = screen.getByRole("tablist", { name: "Settings sections" }); + const viewport = tablist.closest('[data-slot="scroll-area-viewport"]'); + expect(viewport).not.toBeNull(); + + const scrollAreaRoot = viewport?.closest('[data-slot="scroll-area"]'); + expect(scrollAreaRoot).not.toBeNull(); + }); +}); diff --git a/src/ui/src/components/settings/SettingsNav.tsx b/src/ui/src/components/settings/SettingsNav.tsx index 6f0e90eb..62e63942 100644 --- a/src/ui/src/components/settings/SettingsNav.tsx +++ b/src/ui/src/components/settings/SettingsNav.tsx @@ -1,4 +1,5 @@ import { type KeyboardEvent, useEffect, useRef, useState } from "react"; +import { ScrollArea } from "../ui/scroll-area"; import { SettingsNavHealthIndicator } from "./SettingsNavHealthIndicator"; import type { SettingsHealthMap } from "./settings-health"; import { nextRovingIndex } from "./settings-nav-keys"; @@ -23,7 +24,7 @@ interface SettingsNavProps { * Keyboard-accessible Settings options list. * * Fixes #803: - * - `min-h-0` lets the nav shrink inside its flex row so `overflow-y-auto` + * - `min-h-0` lets the nav shrink inside its flex row so the scroll container * actually engages and every tab (e.g. "Account") stays reachable instead of * overflowing and being clipped. * - Roving-tabindex + Up/Down/Home/End arrow navigation (the list previously @@ -35,6 +36,16 @@ interface SettingsNavProps { * was vetoed by the unsaved-changes guard (where `activeIndex` doesn't change * and the resync effect below wouldn't fire). A later Arrow press then steps * from where the user visibly is, not a stale keyboard position. + * + * Fixes #785: + * - The list previously scrolled via plain `overflow-y-auto`, which relies on + * the OS/browser's native scrollbar. That scrollbar can render with zero + * visible width (seen on Linux/GTK themes, and macOS's default + * hidden-until-scroll setting), leaving no visual affordance that the list + * has more content below the fold even though wheel/trackpad scrolling + * mechanically works. Wrapping the tablist in the same `ScrollArea` used by + * the settings panel gives it the app's styled, always-visible scrollbar + * thumb consistently across OSes, matching AC3/AC4/AC5. */ export function SettingsNav({ tabs, activeTabId, panelId, onSelect, tabHealth }: SettingsNavProps) { const activeIndex = Math.max( @@ -61,40 +72,42 @@ export function SettingsNav({ tabs, activeTabId, panelId, onSelect, tabHealth }: }; return ( -
- {tabs.map((tab, index) => { - const isActive = tab.id === activeTabId; - const health = tabHealth?.[tab.id]; - return ( - - ); - })} -
+ +
+ {tabs.map((tab, index) => { + const isActive = tab.id === activeTabId; + const health = tabHealth?.[tab.id]; + return ( + + ); + })} +
+
); }