feat: slide sidebar when expanding and folding

This commit is contained in:
2026-10-01 11:48:50 +09:00
parent a07d70b107
commit 3ee1698957
5 changed files with 120 additions and 22 deletions
@@ -1,7 +1,7 @@
/// <reference lib="dom" />
import { assert, assertEquals } from "@std/assert";
import { dirname, fromFileUrl, join } from "@std/path";
import { chromium } from "playwright";
import { chromium, type Page } from "playwright";
const repositoryRoot = join(dirname(fromFileUrl(import.meta.url)), "../../..");
const workspaceRoot = join(repositoryRoot, "web/workspace");
@@ -41,6 +41,54 @@ async function historyRequestCount(baseUrl: string): Promise<number> {
return state.history_requests;
}
async function checkSidebarSlide(page: Page, mobile: boolean, opening: boolean): Promise<void> {
const toggle = page.locator(mobile ? ".app-shell__mobile-sidebar-toggle" : ".sidebar-fold-button");
const result = await toggle.evaluate(async (button, mobile) => {
const frame = document.querySelector<HTMLElement>(".sidebar-frame")!;
const content = frame.querySelector<HTMLElement>(".sidebar-frame-content")!;
const main = document.querySelector<HTMLElement>(".app-shell__main")!;
const moving = mobile ? frame : content;
const sample = () => ({
x: moving.getBoundingClientRect().x,
width: moving.getBoundingClientRect().width,
frameWidth: frame.getBoundingClientRect().width,
mainWidth: main.getBoundingClientRect().width,
mainHeight: main.getBoundingClientRect().height,
});
const before = sample();
(button as HTMLButtonElement).click();
await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
const animations = frame.getAnimations({ subtree: true });
const slides = animations.filter((animation) => animation instanceof CSSTransition &&
["width", "transform"].includes(animation.transitionProperty));
const durations = slides.map((animation) => animation.effect!.getTiming().duration);
for (const animation of animations) {
animation.pause();
animation.currentTime = 110;
}
const middle = sample();
for (const animation of animations) animation.finish();
await Promise.all(animations.map((animation) => animation.finished));
return { before, middle, after: sample(), durations, contentInert: content.inert, mainInert: main.inert };
}, mobile);
assert(result.durations.length >= (mobile ? 1 : 2));
assert(result.durations.every((duration) => duration === 220));
const { before, middle, after } = result;
assert(opening ? before.x < middle.x && middle.x < after.x : before.x > middle.x && middle.x > after.x,
`sidebar must slide horizontally through an intermediate position: ${JSON.stringify(result)}`);
assertEquals(before.width, after.width, "sidebar contents must slide without reflowing");
assertEquals(result.contentInert, !opening);
assertEquals(result.mainInert, mobile && opening);
if (mobile) {
assertEquals(before.mainWidth, after.mainWidth);
assertEquals(before.mainHeight, after.mainHeight);
assertEquals(after.frameWidth, before.frameWidth);
} else {
assert(opening ? before.frameWidth < middle.frameWidth && middle.frameWidth < after.frameWidth
: before.frameWidth > middle.frameWidth && middle.frameWidth > after.frameWidth);
}
}
async function checkConsoleHistory(viewportHeight: number): Promise<void> {
const build = await new Deno.Command(Deno.execPath(), {
args: ["task", "build"],
@@ -336,17 +384,33 @@ async function checkConsoleHistory(viewportHeight: number): Promise<void> {
assertEquals(new Set(rowIds).size, rowIds.length);
await page.getByRole("button", { name: "Overview", exact: true }).click();
assertEquals(await transcript.getByText("searched 1 time・ran 1 command", { exact: true }).count(), 12);
await page.getByRole("button", { name: "Fold sidebar", exact: true }).click();
await checkSidebarSlide(page, false, false);
await checkSidebarSlide(page, false, true);
await checkSidebarSlide(page, false, false);
assertEquals(await page.evaluate(() => localStorage.getItem("yoi.sidebar.folded.v1")), "true");
await page.reload();
await page.getByRole("button", { name: "Unfold sidebar", exact: true }).waitFor();
assertEquals(await page.locator(".app-shell.sidebar-open").count(), 0);
await page.setViewportSize({ width: 600, height: viewportHeight });
await page.getByRole("button", { name: "Show sidebar", exact: true }).click();
await page.locator(".sidebar-frame").evaluate(async (element) => {
await Promise.all(element.getAnimations({ subtree: true }).map((animation) => animation.finished));
});
await checkSidebarSlide(page, true, true);
await checkSidebarSlide(page, true, false);
await checkSidebarSlide(page, true, true);
assertEquals(await page.evaluate(() => localStorage.getItem("yoi.sidebar.folded.v1")), "false");
await page.reload();
await page.getByRole("button", { name: "Hide sidebar", exact: true }).waitFor();
assertEquals(await page.locator(".app-shell.sidebar-open").count(), 1);
await page.emulateMedia({ reducedMotion: "reduce" });
await page.getByRole("button", { name: "Hide sidebar", exact: true }).click();
const reducedSidebar = await page.locator(".sidebar-frame").evaluate((element) => ({
animations: element.getAnimations({ subtree: true }).length,
right: element.getBoundingClientRect().right,
visibility: getComputedStyle(element).visibility,
}));
assertEquals(reducedSidebar, { animations: 0, right: 0, visibility: "hidden" });
assertEquals(await page.locator(".app-shell__main").evaluate((element) => (element as HTMLElement).inert), false);
assertEquals(errors, []);
await context.close();
} finally {