Add fullscreen-menu helpers

This commit is contained in:
Kuba Orlik 2024-05-11 16:42:02 +02:00
parent cd1260e390
commit 4491f53f32
4 changed files with 292 additions and 0 deletions

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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>
);
}
})();

View File

@ -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);
});