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.reload();
|
||||||
await touchPage.getByRole("button", { name: "Show sidebar", exact: true }).waitFor();
|
await touchPage.getByRole("button", { name: "Show sidebar", exact: true }).waitFor();
|
||||||
assertEquals(await touchPage.evaluate(() => localStorage.getItem("yoi.sidebar.mode.v1")), "pinned");
|
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.
|
// A wide touchscreen still has a temporary-open control in hover mode.
|
||||||
await touchPage.setViewportSize({ width: 1024, height: 600 });
|
await touchPage.setViewportSize({ width: 1024, height: 600 });
|
||||||
await touchPage.getByRole("button", { name: "Unpin sidebar", exact: true }).tap();
|
await touchPage.getByRole("button", { name: "Unpin sidebar", exact: true }).tap();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<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 Bevel from '#lib/workspace/ui/Bevel.svelte';
|
||||||
import SidebarToggleIcon from './SidebarToggleIcon.svelte';
|
import SidebarToggleIcon from './SidebarToggleIcon.svelte';
|
||||||
import './sidebar.css';
|
import './sidebar.css';
|
||||||
@@ -77,6 +78,19 @@
|
|||||||
focusInside = false;
|
focusInside = false;
|
||||||
clearCloseTimer();
|
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);
|
onDestroy(clearCloseTimer);
|
||||||
</script>
|
</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