Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions apps/app/src/components/layout/AppLayoutSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ vi.mock("@/components/sidebar/AppSidebar", async () => {
isBodyHidden,
renderRail,
alternateBody,
railFooter,
}: {
isBodyHidden: boolean;
renderRail: (customize: {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
}) => ReactNode;
alternateBody: ReactNode;
railFooter: ReactNode;
}) => {
useEffect(() => {
mountCounts.appSidebar += 1;
Expand All @@ -54,26 +56,27 @@ vi.mock("@/components/sidebar/AppSidebar", async () => {
App sidebar
</div>
{alternateBody}
{railFooter}
</Sidebar>
);
},
};
});

vi.mock("@/components/sidebar/AppNavRail", () => ({
AppNavRail: ({
isAppMode,
NavRailSettingsButton: ({
isSettingsActive,
}: {
isAppMode: boolean;
isSettingsActive: boolean;
}) => (
<div
data-testid="app-nav-rail"
data-app-mode={isAppMode}
<button
data-testid="settings-footer"
data-settings-active={isSettingsActive}
/>
),
AppNavRail: ({ isAppMode }: { isAppMode: boolean }) => (
<div data-testid="app-nav-rail" data-app-mode={isAppMode} />
),
}));

vi.mock("@/components/settings/SettingsSidebar", () => ({
Expand Down Expand Up @@ -193,7 +196,9 @@ describe("AppLayoutSidebar", () => {

expect(getMobilePanel()).toBe(panel);
expect(rail.dataset.appMode).toBe("false");
expect(rail.dataset.settingsActive).toBe("true");
expect(screen.getByTestId("settings-footer").dataset.settingsActive).toBe(
"true",
);
expect(getAppSidebarBody().hidden).toBe(false);
expect(screen.queryByTestId("settings-sidebar-body")).toBeNull();
expect(getShelfRevealTranslate()).toBe("0px");
Expand All @@ -203,7 +208,9 @@ describe("AppLayoutSidebar", () => {
expect(getMobilePanel()).toBe(panel);
expect(panel.dataset.state).toBe("closed");
expect(getRail()).toBe(rail);
expect(rail.dataset.settingsActive).toBe("true");
expect(screen.getByTestId("settings-footer").dataset.settingsActive).toBe(
"true",
);
expect(getAppSidebarBody().hidden).toBe(true);
expect(screen.getByTestId("settings-sidebar-body").textContent).toBe(
"Settings sidebar",
Expand Down Expand Up @@ -266,7 +273,9 @@ describe("AppLayoutSidebar", () => {
expect(getMobilePanel()).toBe(panel);
expect(panel.dataset.state).toBe("open");
expect(getRail()).toBe(rail);
expect(rail.dataset.settingsActive).toBe("true");
expect(screen.getByTestId("settings-footer").dataset.settingsActive).toBe(
"true",
);
expect(getAppSidebarBody().hidden).toBe(true);
expect(screen.getByTestId("settings-sidebar-body")).toBeTruthy();
});
Expand All @@ -282,23 +291,29 @@ describe("AppLayoutSidebar", () => {

const rail = getRail();
expect(rail.dataset.appMode).toBe("true");
expect(rail.dataset.settingsActive).toBe("false");
expect(screen.getByTestId("settings-footer").dataset.settingsActive).toBe(
"false",
);
expect(getAppSidebarBody().hidden).toBe(false);

fireEvent.click(
screen.getByRole("button", { name: "Change route without closing" }),
);
expect(getRail()).toBe(rail);
expect(rail.dataset.appMode).toBe("false");
expect(rail.dataset.settingsActive).toBe("true");
expect(screen.getByTestId("settings-footer").dataset.settingsActive).toBe(
"true",
);
expect(getAppSidebarBody().hidden).toBe(true);
expect(screen.getByTestId("settings-sidebar-body")).toBeTruthy();

fireEvent.click(
screen.getByRole("button", { name: "Navigate to plugins" }),
);
expect(getRail()).toBe(rail);
expect(rail.dataset.settingsActive).toBe("false");
expect(screen.getByTestId("settings-footer").dataset.settingsActive).toBe(
"false",
);
expect(screen.queryByTestId("settings-sidebar-body")).toBeNull();
expect(screen.getByTestId("plugins-sidebar-body")).toBeTruthy();

Expand Down
14 changes: 9 additions & 5 deletions apps/app/src/components/layout/AppLayoutSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { useState, type MouseEvent as ReactMouseEvent } from "react";
import { AppNavRail } from "@/components/sidebar/AppNavRail";
import {
AppNavRail,
NavRailSettingsButton,
} from "@/components/sidebar/AppNavRail";
import { AppSidebar } from "@/components/sidebar/AppSidebar";
import { SettingsSidebar } from "@/components/settings/SettingsSidebar";
import { ResourceSidebar } from "@/components/tools/ResourceSidebar";
Expand Down Expand Up @@ -36,13 +39,14 @@ export function AppLayoutSidebar({
isResizing={isResizing}
isBodyHidden={renderedMode !== "app"}
renderRail={(customize) => (
<AppNavRail
isAppMode={mode === "app"}
<AppNavRail isAppMode={mode === "app"} customize={customize} />
)}
railFooter={
<NavRailSettingsButton
isSettingsActive={mode === "settings"}
settingsRoutePath={settingsRoutePath}
customize={customize}
/>
)}
}
alternateBody={
renderedMode === "settings" ? (
<SettingsSidebar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ function renderAppSidebar() {
isResizing={false}
isBodyHidden={false}
renderRail={() => null}
railFooter={null}
alternateBody={null}
/>
</SidebarProvider>
Expand Down
69 changes: 36 additions & 33 deletions apps/app/src/components/sidebar/AppNavRail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ import {
} from "@/lib/route-paths";
import { makePluginRegistrationSet as registrationSet } from "@/test/fixtures/plugins";
import { SidebarUpdatesBadge } from "./SidebarUpdatesBadge";
import { AppNavRail, NavRailNewThreadButton } from "./AppNavRail";
import {
AppNavRail,
NavRailNewThreadButton,
NavRailSettingsButton,
} from "./AppNavRail";
import { SidebarVisibilityCustomize } from "./SidebarVisibilityControls";
import { SidebarNavigationModelProvider } from "./SidebarNavigationModel";

Expand All @@ -76,12 +80,19 @@ vi.mock("@/views/useMobileRecentsThreadReveal", () => ({
vi.mock("@/components/sidebar/AppSidebar", () => ({
AppSidebar: ({
renderRail,
railFooter,
}: {
railFooter: ReactNode;
renderRail: (customize: {
isOpen: boolean;
onOpenChange: () => void;
}) => ReactNode;
}) => renderRail({ isOpen: false, onOpenChange: () => {} }),
}) => (
<>
{renderRail({ isOpen: false, onOpenChange: () => {} })}
{railFooter}
</>
),
}));

vi.mock("@/components/settings/SettingsSidebar", () => ({
Expand Down Expand Up @@ -147,12 +158,16 @@ function RailHarness({ useLayout = false }: { useLayout?: boolean }) {
<SidebarUpdatesBadge onNavigate={closeMobileSidebar} />
</>
) : (
<AppNavRail
isAppMode={isAppMode}
isSettingsActive={isSettings}
settingsRoutePath={SETTINGS_ROUTE_PATH}
customize={{ isOpen: isCustomizing, onOpenChange: setCustomizing }}
/>
<>
<AppNavRail
isAppMode={isAppMode}
customize={{ isOpen: isCustomizing, onOpenChange: setCustomizing }}
/>
<NavRailSettingsButton
isSettingsActive={isSettings}
settingsRoutePath={SETTINGS_ROUTE_PATH}
/>
</>
)}
<NavRailNewThreadButton />
<output data-testid="pathname">{location.pathname}</output>
Expand Down Expand Up @@ -196,22 +211,29 @@ function rail(): HTMLElement {
}

function railButton(name: string): HTMLElement {
const button = Array.from(rail().querySelectorAll("button")).find(
(candidate) => candidate.getAttribute("aria-label") === name,
);
const button = Array.from(
document.querySelectorAll<HTMLButtonElement>(
'[data-testid="app-nav-rail"] button, button[aria-label="Settings"]',
),
).find((candidate) => candidate.getAttribute("aria-label") === name);
if (!button) throw new Error(`Expected a rail button named ${name}`);
return button;
}

function railLabels(): (string | null)[] {
return Array.from(rail().querySelectorAll("button"), (button) =>
button.getAttribute("aria-label"),
return Array.from(
document.querySelectorAll<HTMLButtonElement>(
'[data-testid="app-nav-rail"] button, button[aria-label="Settings"]',
),
(button) => button.getAttribute("aria-label"),
);
}

function currentRailLabels(): (string | null)[] {
return Array.from(
rail().querySelectorAll('button[aria-current="page"]'),
document.querySelectorAll<HTMLButtonElement>(
'[data-testid="app-nav-rail"] button[aria-current="page"], button[aria-label="Settings"][aria-current="page"]',
),
(button) => button.getAttribute("aria-label"),
);
}
Expand Down Expand Up @@ -268,25 +290,6 @@ describe("AppNavRail", () => {
expect(currentRailLabels()).toEqual(["Home"]);
});

it("rests Settings on the window card's bottom edge in the framed shell, and keeps its own bottom padding otherwise", () => {
const framed = renderRail(THREAD_PATH, { isFramed: true });
const framedNav = screen.getByRole("navigation", {
name: "Primary navigation",
});
expect(framedNav.lastElementChild).toBe(
screen.getByRole("button", { name: /^Settings/ }),
);
expect(framedNav.classList.contains("pb-2.5")).toBe(false);
framed.unmount();

renderRail(THREAD_PATH);
expect(
screen
.getByRole("navigation", { name: "Primary navigation" })
.classList.contains("pb-2.5"),
).toBe(true);
});

it("moves the highlight from Home to a plugin panel and to Settings as the route changes", () => {
renderRail(THREAD_PATH);

Expand Down
51 changes: 29 additions & 22 deletions apps/app/src/components/sidebar/AppNavRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,9 @@ function RailMoreMenu({

export function AppNavRail({
isAppMode,
isSettingsActive,
settingsRoutePath,
customize,
}: {
isAppMode: boolean;
isSettingsActive: boolean;
settingsRoutePath: string;
customize: NavRailCustomizeState;
}) {
const { items, activeItemId } = useSidebarNavigation();
Expand All @@ -277,7 +273,6 @@ export function AppNavRail({
const customizeAfterMenuCloseRef = useRef(false);
const location = useLocation();
const navigate = useNavigate();
const settingsShortcut = useAppCommandShortcut("settings.open");
const [desktopInfo] = useState(getBbDesktopInfo);
const isFramed = useIsSidebarFramed();
const isCompactViewport = useIsCompactViewport();
Expand Down Expand Up @@ -316,7 +311,7 @@ export function AppNavRail({
"relative z-10 flex shrink-0 flex-col",
NAV_RAIL_WIDTH_CLASS,
!isFramed &&
"-mt-[env(safe-area-inset-top)] -mb-[env(safe-area-inset-bottom)] bg-surface-recessed pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]",
"-mt-[env(safe-area-inset-top)] bg-surface-recessed pt-[env(safe-area-inset-top)]",
)}
>
{isFramed ? null : (
Expand All @@ -332,8 +327,8 @@ export function AppNavRail({
<nav
aria-label="Primary navigation"
className={cn(
"flex min-h-0 flex-1 flex-col items-center gap-2.5",
isFramed ? "pt-2" : "pb-2.5",
"flex min-h-0 flex-1 flex-col items-center gap-2.5 pb-2",
isFramed && "pt-2",
)}
>
<div className="flex min-h-0 w-full flex-1 flex-col items-center gap-2.5 overflow-y-auto py-0.5 [scrollbar-width:none]">
Expand Down Expand Up @@ -391,25 +386,37 @@ export function AppNavRail({
</PopoverContent>
</Popover>
</div>
<RailButton
label={
settingsShortcut
? `Settings (${settingsShortcut.label})`
: "Settings"
}
active={isSettingsActive}
aria-keyshortcuts={settingsShortcut?.ariaKeyshortcuts}
onClick={() => {
if (!isSettingsActive) void navigate(settingsRoutePath);
}}
>
<Icon name="Settings" aria-hidden="true" />
</RailButton>
</nav>
</div>
);
}

export function NavRailSettingsButton({
isSettingsActive,
settingsRoutePath,
}: {
isSettingsActive: boolean;
settingsRoutePath: string;
}) {
const navigate = useNavigate();
const settingsShortcut = useAppCommandShortcut("settings.open");
return (
<RailButton
label={
settingsShortcut ? `Settings (${settingsShortcut.label})` : "Settings"
}
active={isSettingsActive}
className="h-8 max-md:pointer-coarse:h-9"
aria-keyshortcuts={settingsShortcut?.ariaKeyshortcuts}
onClick={() => {
if (!isSettingsActive) void navigate(settingsRoutePath);
}}
>
<Icon name="Settings" aria-hidden="true" />
</RailButton>
);
}

export function NavRailNewThreadButton() {
const { items, actions, isShortcutModifierHeld } = useSidebarNavigation();
const split = useSidebarNavigationSplit(NEW_THREAD_NAVIGATION_ITEM_ID);
Expand Down
Loading
Loading