diff --git a/src/back/routes/common/fullscreen-menu/fullscreen-menu.css b/src/back/routes/common/fullscreen-menu/fullscreen-menu.css new file mode 100644 index 0000000..f8f2ada --- /dev/null +++ b/src/back/routes/common/fullscreen-menu/fullscreen-menu.css @@ -0,0 +1,57 @@ +.fullscreen-menu { + --fullscreen-menu-padding: 1rem; + --fullscreen-menu-height: 100vh; + --fulscreen-menu-inner-height: calc( + var(--fullscreen-menu-height) - 2 * var(--fullscreen-menu-padding) + ); + position: absolute; + background-color: white; + padding: var(--fullscreen-menu-padding); + padding-right: 0; /* for scrollbar to be close to the screen edge */ + height: var(--fullscreen-menu-height); + overflow: hidden; + width: 100vw; + left: 0; + top: 0; + border: 0; + box-sizing: border-box; + display: grid; + grid-template-rows: minmax(var(--fulscreen-menu-inner-height), 1fr); + grid-template-columns: 1fr; + + /* These need to be overwritten upon activating the dialog */ + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: scale(0.9); + + transition: opacity 200ms, transform 200ms, visibility 0ms 200ms; + box-shadow: 1px 1px 11px #0000006e; +} + +.fullscreen-menu__content { + grid-row: 1/2; + grid-column: 1/2; + display: flex; +} + +.fullscreen-menu__panel-container { + display: grid; +} + +.fullscreen-menu__panel { + opacity: 0; + pointer-events: none; + visibility: hidden; + transform: translateX(50vw); + transition: opacity 200ms, transform 200ms, visibility 0ms 200ms; + + grid-row: 1/2; + grid-column: 1/2; + + max-height: calc(var(--fulscreen-menu-inner-height) - 3rem); + overflow-y: auto; + width: 100%; + padding-right: var(--fullscreen-menu-padding); + box-sizing: border-box; +} diff --git a/src/back/routes/common/fullscreen-menu/fullscreen-menu.tsx b/src/back/routes/common/fullscreen-menu/fullscreen-menu.tsx new file mode 100644 index 0000000..be7cd02 --- /dev/null +++ b/src/back/routes/common/fullscreen-menu/fullscreen-menu.tsx @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/consistent-type-assertions */ +import type { FlatTemplatable } from "tempstream"; +import { TempstreamJSX } from "tempstream"; + +type Styles = { + menu_open: string; + menu_closed: string; + body_when_open: string; + body_when_closed: string; + panel_open: string; + panel_closed: string; +}; + +const default_styles: Styles = { + menu_open: `opacity: 1; visibility: visible; pointer-events: all; transform: scale(1); transition: opacity 200ms, transform 200ms, visibility 0ms 0ms;`, + menu_closed: "", + body_when_open: "overflow: hidden;", + body_when_closed: "", + panel_open: + "opacity: 1; transform: translateX(0); visibility: visible; pointer-events: all; transition: opacity 200ms, transform 200ms, visibility 0ms 0ms;", + panel_closed: "", +}; + +export function fullscreenMenu({ + id: menu_id, + panels, + default_panel, + styles = {}, + inner_wrapper = (menu_id, content) => ( +
+
+ +
+
{content}
+
+ ), + outer_wrapper = (_menu_id: string, content: FlatTemplatable) => content, +}: { + id: string; + panels: Record; + default_panel: string; + styles?: Partial; + outer_wrapper?: (menu_id: string, inner: FlatTemplatable) => FlatTemplatable; + inner_wrapper?: (menu_id: string, inner: FlatTemplatable) => FlatTemplatable; +}) { + const full_styles = { ...default_styles, ...styles }; + return outer_wrapper( + menu_id, +
+ + { + /* HTML */ `` + } +
+ {Object.keys(panels).map((panel_id) => ( + + + {``} + + ))} +
+
+ {inner_wrapper( + menu_id, +
+ {Object.entries(panels).map(([panel_id, panel_content]) => ( +
+ {panel_content} +
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/back/routes/fullscreen-menu-demo.page.tsx b/src/back/routes/fullscreen-menu-demo.page.tsx new file mode 100644 index 0000000..053dfd1 --- /dev/null +++ b/src/back/routes/fullscreen-menu-demo.page.tsx @@ -0,0 +1,88 @@ +import type { Context } from "koa"; +import { TempstreamJSX } from "tempstream"; +import { Page } from "@sealcode/sealgen"; +import html from "../html.js"; +import { fullscreenMenu } from "./common/fullscreen-menu/fullscreen-menu.js"; + +export const actionName = "FullscreenMenuDemo"; + +export default new (class FullscreenMenuDemoPage extends Page { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async canAccess(_: Context) { + return { canAccess: true, message: "" }; + } + + async render(ctx: Context) { + return html( + ctx, + "FullscreenMenuDemo", +
+ { + /* HTML */ `` + } + {fullscreenMenu({ + id: "demo", + panels: { + panel_1: ( +
+ PANEL 1. Click{" "} + to go to + panel 2
And here's some description
+ Go to{" "} + +
+ ), + panel_2: ( +
+ PANEL 2 +
+ +
+
+ ), + panel_3: ( +
+ + {[ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, + ].map((n) => ( +
{n}
+ ))} +
+ ), + }, + default_panel: "panel_1", + })} +
+ + +
+
+ This page is taaaaall - for testing reasons +
+
+ ); + } +})(); diff --git a/src/back/routes/fullscreen-menu-demo.test.ts b/src/back/routes/fullscreen-menu-demo.test.ts new file mode 100644 index 0000000..1ca3532 --- /dev/null +++ b/src/back/routes/fullscreen-menu-demo.test.ts @@ -0,0 +1,40 @@ +import { withProdApp } from "../test_utils/with-prod-app.js"; +import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js"; +import { FullscreenMenuDemoURL } from "./urls.js"; +import { getBrowser } from "../test_utils/browser-creator.js"; +import type { Browser, BrowserContext, Page } from "@playwright/test"; + +describe("FullscreenMenuDemo webhint", () => { + it("doesn't crash", async function () { + return withProdApp(async ({ base_url, rest_api }) => { + await rest_api.get(FullscreenMenuDemoURL); + await webhintURL(base_url + FullscreenMenuDemoURL); + // alternatively you can use webhintHTML for faster but less precise scans + // or for scanning responses of requests that use some form of authorization: + // const response = await rest_api.get(FullscreenMenuDemoURL); + // await webhintHTML(response); + }); + }).timeout(VERY_LONG_TEST_TIMEOUT); +}); + +describe("FullscreenMenuDemo", () => { + let page: Page; + let browser: Browser; + let context: BrowserContext; + + beforeEach(async () => { + browser = await getBrowser(); + context = await browser.newContext(); + page = await context.newPage(); + }); + + afterEach(async () => { + await context.close(); + }); + + it("works as expected", async function () { + return withProdApp(async ({ base_url }) => { + await page.goto(base_url + FullscreenMenuDemoURL); + }); + }).timeout(VERY_LONG_TEST_TIMEOUT); +});