diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index d57a4da1c2f0..4a4d54bf9456 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -2187,31 +2187,33 @@ export function GeneralSettingsPanel() { } /> - - updateSettings({ - confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive, - }) + {settings.legacySidebarEnabled ? ( + + updateSettings({ + confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive, + }) + } + /> + ) : null + } + control={ + + updateSettings({ confirmThreadArchive: Boolean(checked) }) } + aria-label="Confirm thread archiving" /> - ) : null - } - control={ - - updateSettings({ confirmThreadArchive: Boolean(checked) }) - } - aria-label="Confirm thread archiving" - /> - } - /> + } + /> + ) : null} (null); const [query, setQuery] = useState(""); const [activeResultIndex, setActiveResultIndex] = useState(0); - const results = useMemo(() => searchSettings(query), [query]); + const legacySidebarEnabled = useLegacySidebarEnabled(); + const results = useMemo( + () => searchSettings(query, visibleSettingsSearchItems(legacySidebarEnabled)), + [legacySidebarEnabled, query], + ); + const resolvedActiveResultIndex = clampSettingsSearchResultIndex( + activeResultIndex, + results.length, + ); const isSearching = query.trim().length > 0; const hasResults = results.length > 0; useEffect(() => { - const result = results[activeResultIndex]; + const result = results[resolvedActiveResultIndex]; if (!result) return; document .getElementById(`settings-search-result-${result.id}`) ?.scrollIntoView({ block: "nearest" }); - }, [activeResultIndex, results]); + }, [resolvedActiveResultIndex, results]); useEffect(() => { const handleKeyDown = (event: globalThis.KeyboardEvent) => { @@ -160,21 +171,21 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { if (results.length === 0) return; if (event.key === "ArrowDown") { event.preventDefault(); - setActiveResultIndex((index) => (index + 1) % results.length); + setActiveResultIndex((resolvedActiveResultIndex + 1) % results.length); return; } if (event.key === "ArrowUp") { event.preventDefault(); - setActiveResultIndex((index) => (index - 1 + results.length) % results.length); + setActiveResultIndex((resolvedActiveResultIndex - 1 + results.length) % results.length); return; } if (event.key === "Enter") { event.preventDefault(); - const result = results[activeResultIndex]; + const result = results[resolvedActiveResultIndex]; if (result) handleSearchResultClick(result); } }, - [activeResultIndex, clearSearch, handleSearchResultClick, isSearching, results], + [clearSearch, handleSearchResultClick, isSearching, resolvedActiveResultIndex, results], ); const handleBackClick = useCallback(() => { if (isMobile) { @@ -211,8 +222,8 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { aria-expanded={isSearching && hasResults} aria-controls={isSearching && hasResults ? "settings-search-results" : undefined} aria-activedescendant={ - isSearching && results[activeResultIndex] - ? `settings-search-result-${results[activeResultIndex].id}` + isSearching && results[resolvedActiveResultIndex] + ? `settings-search-result-${results[resolvedActiveResultIndex].id}` : undefined } className="min-w-0 flex-1 [&_[data-slot=input]]:h-auto [&_[data-slot=input]]:p-0 [&_[data-slot=input]]:leading-normal [&_[data-slot=input]]:text-sm [&_[data-slot=input]]:font-medium [&_[data-slot=input]]:text-sidebar-foreground [&_[data-slot=input]]:placeholder:text-sidebar-muted-foreground" @@ -255,10 +266,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { setActiveResultIndex(index)} onClick={() => handleSearchResultClick(item)} diff --git a/apps/web/src/components/settings/settingsSearch.test.ts b/apps/web/src/components/settings/settingsSearch.test.ts index 09fd7a9a6a0b..a13c2cb38337 100644 --- a/apps/web/src/components/settings/settingsSearch.test.ts +++ b/apps/web/src/components/settings/settingsSearch.test.ts @@ -1,10 +1,12 @@ import { describe, expect, it } from "vite-plus/test"; import { + clampSettingsSearchResultIndex, searchableSetting, searchSettings, SETTINGS_SEARCH_ITEMS, type SettingsSearchItem, + visibleSettingsSearchItems, } from "./settingsSearch"; const ITEMS: ReadonlyArray = [ @@ -70,6 +72,21 @@ describe("searchSettings", () => { expect(new Set(ids).size).toBe(ids.length); }); + it("only exposes archive confirmation for the legacy sidebar", () => { + expect( + visibleSettingsSearchItems(false).some((item) => item.id === "archive-confirmation"), + ).toBe(false); + expect( + visibleSettingsSearchItems(true).some((item) => item.id === "archive-confirmation"), + ).toBe(true); + }); + + it("clamps the active result after visible settings shrink", () => { + expect(clampSettingsSearchResultIndex(1, 1)).toBe(0); + expect(clampSettingsSearchResultIndex(1, 2)).toBe(1); + expect(clampSettingsSearchResultIndex(0, 0)).toBe(0); + }); + it("serves anchor props to panels from the catalog", () => { expect(searchableSetting("word-wrap")).toEqual({ id: "word-wrap", title: "Word wrap" }); expect(searchableSetting("archive")).toEqual({ id: "archive", title: "Archived threads" }); diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index e3aef6705665..4014b75619c8 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -14,6 +14,7 @@ export interface SettingsSearchItem { readonly title: string; readonly to: SettingsPath; readonly targetId?: string; + readonly requiresLegacySidebar?: boolean; // Its row only renders in the desktop app, so a browser result would land on // an anchor that isn't there. readonly desktopOnly?: boolean; @@ -148,6 +149,7 @@ export const SETTINGS_SEARCH_ITEMS = [ id: "archive-confirmation", title: "Archive confirmation", to: "/settings/general", + requiresLegacySidebar: true, }, { id: "delete-confirmation", @@ -240,6 +242,10 @@ function normalizeSearchText(value: string): string { .trim(); } +export function clampSettingsSearchResultIndex(index: number, resultCount: number): number { + return Math.max(0, Math.min(index, resultCount - 1)); +} + export function searchSettings( query: string, items: ReadonlyArray = SETTINGS_SEARCH_ITEMS, @@ -253,3 +259,11 @@ export function searchSettings( normalizeSearchText(item.title).includes(normalizedQuery), ); } + +export function visibleSettingsSearchItems( + legacySidebarEnabled: boolean, + items: ReadonlyArray = SETTINGS_SEARCH_ITEMS, +): ReadonlyArray { + if (legacySidebarEnabled) return items; + return items.filter((item) => item.requiresLegacySidebar !== true); +}