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