Add fullscreen-menu helpers
This commit is contained in:
parent
cd1260e390
commit
4491f53f32
57
src/back/routes/common/fullscreen-menu/fullscreen-menu.css
Normal file
57
src/back/routes/common/fullscreen-menu/fullscreen-menu.css
Normal 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;
|
||||||
|
}
|
107
src/back/routes/common/fullscreen-menu/fullscreen-menu.tsx
Normal file
107
src/back/routes/common/fullscreen-menu/fullscreen-menu.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
88
src/back/routes/fullscreen-menu-demo.page.tsx
Normal file
88
src/back/routes/fullscreen-menu-demo.page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
40
src/back/routes/fullscreen-menu-demo.test.ts
Normal file
40
src/back/routes/fullscreen-menu-demo.test.ts
Normal 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);
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user