feat(mobile/tab_switcher): scroll to active tab

This commit is contained in:
Elian Doran 2026-01-31 18:25:01 +02:00
parent 740b1093d7
commit 2a38af5db6
No known key found for this signature in database

View File

@ -2,10 +2,12 @@ import "./TabSwitcher.css";
import clsx from "clsx"; import clsx from "clsx";
import { createPortal } from "preact/compat"; import { createPortal } from "preact/compat";
import { useCallback, useState } from "preact/hooks"; import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context"; import appContext from "../../components/app_context";
import NoteContext from "../../components/note_context"; import NoteContext from "../../components/note_context";
import { getHue, parseColor } from "../../services/css_class_manager";
import froca from "../../services/froca";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import { NoteContent } from "../collections/legacy/ListOrGridView"; import { NoteContent } from "../collections/legacy/ListOrGridView";
import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets";
@ -32,6 +34,7 @@ function TabBarModal({ shown, setShown }: {
shown: boolean; shown: boolean;
setShown: (newValue: boolean) => void; setShown: (newValue: boolean) => void;
}) { }) {
const [ fullyShown, setFullyShown ] = useState(false);
const selectTab = useCallback((noteContextToActivate: NoteContext) => { const selectTab = useCallback((noteContextToActivate: NoteContext) => {
appContext.tabManager.activateNoteContext(noteContextToActivate.ntxId); appContext.tabManager.activateNoteContext(noteContextToActivate.ntxId);
setShown(false); setShown(false);
@ -43,18 +46,33 @@ function TabBarModal({ shown, setShown }: {
size="xl" size="xl"
title="Tabs" title="Tabs"
show={shown} show={shown}
onHidden={() => setShown(false)} onShown={() => setFullyShown(true)}
onHidden={() => {
setShown(false);
setFullyShown(false);
}}
> >
<TabBarModelContent selectTab={selectTab} /> <TabBarModelContent selectTab={selectTab} shown={fullyShown} />
</Modal> </Modal>
); );
} }
function TabBarModelContent({ selectTab }: { function TabBarModelContent({ selectTab, shown }: {
shown: boolean;
selectTab: (noteContextToActivate: NoteContext) => void; selectTab: (noteContextToActivate: NoteContext) => void;
}) { }) {
const mainNoteContexts = useMainNoteContexts(); const mainNoteContexts = useMainNoteContexts();
const activeNoteContext = useActiveNoteContext(); const activeNoteContext = useActiveNoteContext();
const tabRefs = useRef<Record<string, HTMLDivElement | null>>({});
// Scroll to active tab.
useEffect(() => {
if (!shown || !activeNoteContext?.ntxId) return;
const correspondingEl = tabRefs.current[activeNoteContext.ntxId];
requestAnimationFrame(() => {
correspondingEl?.scrollIntoView();
});
}, [ activeNoteContext, shown ]);
return ( return (
<div className="tabs"> <div className="tabs">
@ -64,13 +82,15 @@ function TabBarModelContent({ selectTab }: {
noteContext={noteContext} noteContext={noteContext}
activeNtxId={activeNoteContext.ntxId} activeNtxId={activeNoteContext.ntxId}
selectTab={selectTab} selectTab={selectTab}
containerRef={el => (tabRefs.current[noteContext.ntxId ?? ""] = el)}
/> />
))} ))}
</div> </div>
); );
} }
function Tab({ noteContext, selectTab, activeNtxId }: { function Tab({ noteContext, containerRef, selectTab, activeNtxId }: {
containerRef: (el: HTMLDivElement | null) => void;
noteContext: NoteContext; noteContext: NoteContext;
selectTab: (noteContextToActivate: NoteContext) => void; selectTab: (noteContextToActivate: NoteContext) => void;
activeNtxId: string | null | undefined; activeNtxId: string | null | undefined;
@ -78,15 +98,21 @@ function Tab({ noteContext, selectTab, activeNtxId }: {
const { note } = noteContext; const { note } = noteContext;
const iconClass = useNoteIcon(note); const iconClass = useNoteIcon(note);
const colorClass = note?.getColorClass() || ''; const colorClass = note?.getColorClass() || '';
const workspaceTabBackgroundColorHue = getWorkspaceTabBackgroundColorHue(noteContext);
return ( return (
<div <div
class={clsx("tab-card", colorClass, { ref={containerRef}
active: noteContext.ntxId === activeNtxId class={clsx("tab-card", {
active: noteContext.ntxId === activeNtxId,
"with-hue": workspaceTabBackgroundColorHue !== undefined
})} })}
onClick={() => selectTab(noteContext)} onClick={() => selectTab(noteContext)}
style={{
"--bg-hue": workspaceTabBackgroundColorHue
}}
> >
<header> <header className={colorClass}>
<Icon icon={iconClass} /> <Icon icon={iconClass} />
<span className="title">{noteContext.note?.title ?? t("tab_row.new_tab")}</span> <span className="title">{noteContext.note?.title ?? t("tab_row.new_tab")}</span>
</header> </header>
@ -102,6 +128,23 @@ function Tab({ noteContext, selectTab, activeNtxId }: {
); );
} }
function getWorkspaceTabBackgroundColorHue(noteContext: NoteContext) {
if (!noteContext.hoistedNoteId) return;
const hoistedNote = froca.getNoteFromCache(noteContext.hoistedNoteId);
if (!hoistedNote) return;
const workspaceTabBackgroundColor = hoistedNote.getWorkspaceTabBackgroundColor();
if (!workspaceTabBackgroundColor) return;
try {
const parsedColor = parseColor(workspaceTabBackgroundColor);
if (!parsedColor) return;
return getHue(parsedColor);
} catch (e) {
// Colors are non-critical, simply ignore.
}
}
function useMainNoteContexts() { function useMainNoteContexts() {
const [ noteContexts, setNoteContexts ] = useState(appContext.tabManager.getMainNoteContexts()); const [ noteContexts, setNoteContexts ] = useState(appContext.tabManager.getMainNoteContexts());