fix: close mobile sidebar after navigation
This commit is contained in:
@@ -596,6 +596,16 @@ async function checkConsoleHistory(viewportHeight: number): Promise<void> {
|
||||
await touchPage.reload();
|
||||
await touchPage.getByRole("button", { name: "Show sidebar", exact: true }).waitFor();
|
||||
assertEquals(await touchPage.evaluate(() => localStorage.getItem("yoi.sidebar.mode.v1")), "pinned");
|
||||
// Different-route and same-page links both dismiss the mobile overlay.
|
||||
for (let visit = 0; visit < 2; visit++) {
|
||||
await touchPage.getByRole("button", { name: "Show sidebar", exact: true }).tap();
|
||||
await touchPage.getByRole("link", { name: "Workspace home", exact: true }).tap();
|
||||
await touchPage.waitForURL(`**/w/${workspaceId}`);
|
||||
await touchPage.getByRole("button", { name: "Show sidebar", exact: true }).waitFor();
|
||||
assertEquals(await touchPage.locator(".app-shell__main").evaluate((element) => (element as HTMLElement).inert), false);
|
||||
assertEquals(await touchPage.evaluate(() => localStorage.getItem("yoi.sidebar.mode.v1")), "pinned");
|
||||
}
|
||||
await touchPage.goto(page.url());
|
||||
// A wide touchscreen still has a temporary-open control in hover mode.
|
||||
await touchPage.setViewportSize({ width: 1024, height: 600 });
|
||||
await touchPage.getByRole("button", { name: "Unpin sidebar", exact: true }).tap();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, type Snippet } from 'svelte';
|
||||
import { onDestroy, onMount, type Snippet } from 'svelte';
|
||||
import { on } from 'svelte/events';
|
||||
import Bevel from '#lib/workspace/ui/Bevel.svelte';
|
||||
import SidebarToggleIcon from './SidebarToggleIcon.svelte';
|
||||
import './sidebar.css';
|
||||
@@ -77,6 +78,19 @@
|
||||
focusInside = false;
|
||||
clearCloseTimer();
|
||||
});
|
||||
function closeAfterNavigation(event: MouseEvent) {
|
||||
if (!mobile || !open || event.defaultPrevented || event.button !== 0 ||
|
||||
event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
||||
const link = event.target instanceof Element ? event.target.closest('a[href]') : null;
|
||||
if (!(link instanceof HTMLAnchorElement) || !frame.contains(link) ||
|
||||
link.hasAttribute('download') || (link.target && link.target !== '_self')) return;
|
||||
clearCloseTimer();
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
// Handle nested navigation links, including activation of the current page.
|
||||
// Svelte's listener preserves ordering with delegated child click handlers.
|
||||
onMount(() => on(frame, 'click', closeAfterNavigation));
|
||||
onDestroy(clearCloseTimer);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { cleanup, fireEvent, render } from "@testing-library/svelte";
|
||||
import { createRawSnippet } from "svelte";
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||
import SidebarFrame from "./SidebarFrame.svelte";
|
||||
|
||||
// Stop happy-dom navigation after the component has handled the bubbling click.
|
||||
const preventBrowserNavigation = (event: MouseEvent) => event.preventDefault();
|
||||
beforeEach(() => document.addEventListener("click", preventBrowserNavigation));
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
document.removeEventListener("click", preventBrowserNavigation);
|
||||
});
|
||||
|
||||
function mount(mobile = true, mode: "pinned" | "hover" = "pinned") {
|
||||
const onOpenChange = vi.fn();
|
||||
const onModeChange = vi.fn();
|
||||
const children = createRawSnippet(() => ({
|
||||
render: () => `<nav>
|
||||
<a href="/w/example"><span>Workspace home</span></a>
|
||||
<a href="${location.href}">Current page</a>
|
||||
<a href="#section">Section</a>
|
||||
<a href="/account" target="_blank">New tab</a>
|
||||
<a href="/export" download>Download</a>
|
||||
<button type="button">Expand group</button>
|
||||
</nav>`,
|
||||
}));
|
||||
return { ...render(SidebarFrame, {
|
||||
mobile, mode, open: true, children, onOpenChange, onModeChange,
|
||||
}), onOpenChange, onModeChange };
|
||||
}
|
||||
|
||||
test.each(["pinned", "hover"] as const)("mobile %s closes on nested, current-page and hash links", async (mode) => {
|
||||
const view = mount(true, mode);
|
||||
for (const label of ["Workspace home", "Current page", "Section"]) {
|
||||
view.onOpenChange.mockClear();
|
||||
await fireEvent.click(view.getByText(label));
|
||||
expect(view.onOpenChange).toHaveBeenCalledExactlyOnceWith(false);
|
||||
expect(view.onModeChange).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
test("mobile ignores disclosure buttons, modified clicks, new tabs, downloads and cancelled navigation", async () => {
|
||||
const view = mount();
|
||||
for (const label of ["Expand group", "New tab", "Download"]) {
|
||||
await fireEvent.click(view.getByText(label));
|
||||
}
|
||||
const link = view.getByText("Workspace home");
|
||||
for (const options of [{ ctrlKey: true }, { metaKey: true }, { shiftKey: true }, { altKey: true }, { button: 1 }]) {
|
||||
await fireEvent.click(link, options);
|
||||
}
|
||||
link.addEventListener("click", (event) => event.preventDefault(), { once: true });
|
||||
await fireEvent.click(link);
|
||||
expect(view.onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each(["pinned", "hover"] as const)("desktop %s navigation does not change sidebar mode or open state", async (mode) => {
|
||||
const view = mount(false, mode);
|
||||
await fireEvent.click(view.getByText("Workspace home"));
|
||||
expect(view.onOpenChange).not.toHaveBeenCalled();
|
||||
expect(view.onModeChange).not.toHaveBeenCalled();
|
||||
});
|
||||
Reference in New Issue
Block a user