Page MenuHomeSealhub

No OneTemporary

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) => (
+ <div style="width: 100%;">
+ <div>
+ <label for={`${menu_id}--visible`}>Close with checkbox label</label>
+ </div>
+ <div>{content}</div>
+ </div>
+ ),
+ outer_wrapper = (_menu_id: string, content: FlatTemplatable) => content,
+}: {
+ id: string;
+ panels: Record<string, FlatTemplatable>;
+ default_panel: string;
+ styles?: Partial<Styles>;
+ 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,
+ <div class={`fullscreen-menu fullscreen-menu--${menu_id}`} id={menu_id}>
+ <input
+ type="checkbox"
+ id={`${menu_id}--visible`}
+ autocomplete="off"
+ style="display: none"
+ />
+ {
+ /* HTML */ `<style>
+
+ body:has(#${menu_id}--visible:checked) .fullscreen-menu--${menu_id},
+ .fullscreen-menu--${menu_id}[open]{
+ ${full_styles.menu_open}
+ }
+
+ body:has(#${menu_id}--visible:checked),
+ body:has(.fullscreen-menu--${menu_id}[open]) {
+ ${full_styles.body_when_open}
+ }
+ </style>`
+ }
+ <div>
+ {Object.keys(panels).map((panel_id) => (
+ <span>
+ <input
+ type="radio"
+ name={`${menu_id}__active_panel`}
+ value={panel_id}
+ checked={panel_id == default_panel}
+ id={`${menu_id}__activate--${panel_id}`}
+ autocomplete="off"
+ style="display: none"
+ />
+ {`<style>
+ body:has(#${menu_id}--visible:checked) .fullscreen-menu:has(input[value="${panel_id}"]:checked) .fullscreen-menu__panel--${panel_id},
+ body:has(.fullscreen-menu--${menu_id}[open]) .fullscreen-menu:has(input[value="${panel_id}"]:checked) .fullscreen-menu__panel--${panel_id} {
+ ${full_styles.panel_open}
+ }
+ </style>`}
+ </span>
+ ))}
+ </div>
+ <div class="fullscreen-menu__content">
+ {inner_wrapper(
+ menu_id,
+ <div class="fullscreen-menu__panel-container">
+ {Object.entries(panels).map(([panel_id, panel_content]) => (
+ <div
+ class={`fullscreen-menu__panel fullscreen-menu__panel--${panel_id}`}
+ >
+ {panel_content}
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+ );
+}
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",
+ <div>
+ {
+ /* HTML */ `<style>
+ .fullscreen-menu__panel--panel_1 {
+ transform: translateX(-50vw); /* move that one to the left */
+ }
+
+ .fullscreen-menu label {
+ cursor: pointer;
+ font-weight: bold;
+ }
+
+ .fullscreen-menu__panel {
+ font-size: 20px;
+ display: flex;
+ flex-flow: column;
+ row-gap: 20px;
+ }
+ </style>`
+ }
+ {fullscreenMenu({
+ id: "demo",
+ panels: {
+ panel_1: (
+ <div>
+ PANEL 1. Click{" "}
+ <label for="demo__activate--panel_2">here</label> to go to
+ panel 2<div>And here's some description</div>
+ Go to{" "}
+ <label for="demo__activate--panel_3">
+ Very tall panel 3
+ </label>
+ </div>
+ ),
+ panel_2: (
+ <div>
+ PANEL 2
+ <div>
+ <label for="demo__activate--panel_1">←Go back</label>
+ </div>
+ </div>
+ ),
+ panel_3: (
+ <div style="display: flex; flex-flow: column; row-gap: 20px;">
+ <label for="demo__activate--panel_1">←Go back</label>
+ {[
+ 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) => (
+ <div>{n}</div>
+ ))}
+ </div>
+ ),
+ },
+ default_panel: "panel_1",
+ })}
+ <div style="display: flex; flex-flow: column; row-gap: 10px;">
+ <label for="demo--visible">Toggle menu via checkbox label</label>
+ <button onclick="demo.showModal()" type="button">
+ Toggle menu with javascript
+ </button>
+ </div>
+ <div style="height: 150vh">
+ This page is taaaaall - for testing reasons
+ </div>
+ </div>
+ );
+ }
+})();
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);
+});

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 8, 08:38 (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1033175
Default Alt Text
(9 KB)

Event Timeline