diff --git a/src/back/routes/common/tabs/tabs.css b/src/back/routes/common/tabs/tabs.css new file mode 100644 index 0000000..214cf66 --- /dev/null +++ b/src/back/routes/common/tabs/tabs.css @@ -0,0 +1,22 @@ +.tabs { + & > nav input[type="radio"] { + display: none; + } + + nav { + display: flex; + flex-flow: row nowrap; + gap: 8px; + + .tabs__tab-label { + label { + cursor: pointer; + font-size: 16px; + user-select: none; + } + } + } + .tabs__tab { + display: none; + } +} diff --git a/src/back/routes/common/tabs/tabs.tsx b/src/back/routes/common/tabs/tabs.tsx new file mode 100644 index 0000000..71bb2ff --- /dev/null +++ b/src/back/routes/common/tabs/tabs.tsx @@ -0,0 +1,70 @@ +import type { FlatTemplatable } from "tempstream"; +import { TempstreamJSX } from "tempstream"; + +const ids = (function* () { + let i = 0; + while (true) { + yield i++; + } +})(); + +export function tabs({ + tabs, + default_tab, + tab_bar, + remember_tab = false, + active_navbar_tab_style = "", +}: { + tabs: { id: string; label?: string; content: FlatTemplatable }[]; + default_tab: string; + tab_bar?: FlatTemplatable; + remember_tab?: boolean; + active_navbar_tab_style?: string; +}) { + const tab_section_id = ids.next().value; + return ( +
+ {tab_bar || ( + + )} + {tabs.map(({ id, content }) => { + const tab_id = `tabs__${tab_section_id}__tab__${id}`; + return ( +
+ { + /* HTML */ `` + } + {content} +
+ ); + })} +
+ ); +} diff --git a/src/back/routes/tabs-demo.page.tsx b/src/back/routes/tabs-demo.page.tsx new file mode 100644 index 0000000..1cf8da4 --- /dev/null +++ b/src/back/routes/tabs-demo.page.tsx @@ -0,0 +1,48 @@ +import type { Context } from "koa"; +import { TempstreamJSX } from "tempstream"; +import { Page } from "@sealcode/sealgen"; +import html from "../html.js"; +import { tabs } from "./common/tabs/tabs.js"; + +export const actionName = "TabsDemo"; + +export default new (class TabsDemoPage 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, + "TabsDemo", +
+ {tabs({ + default_tab: "tab1", + active_navbar_tab_style: + "font-weight: bold; text-decoration: underline", + tabs: [ + { + id: "tab1", + label: "First tab", + content: ( +
+

This is tab 1

+
+ ), + }, + { + id: "tab2", + label: "Second tab", + content: ( +
+

And this is tab 2

+
+ ), + }, + ], + })} +
+ ); + } +})(); diff --git a/src/back/routes/tabs-demo.test.ts b/src/back/routes/tabs-demo.test.ts new file mode 100644 index 0000000..7413bc7 --- /dev/null +++ b/src/back/routes/tabs-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 { TabsDemoURL } from "./urls.js"; +import { getBrowser } from "../test_utils/browser-creator.js"; +import type { Browser, BrowserContext, Page } from "@playwright/test"; + +describe("TabsDemo webhint", () => { + it("doesn't crash", async function () { + return withProdApp(async ({ base_url, rest_api }) => { + await rest_api.get(TabsDemoURL); + await webhintURL(base_url + TabsDemoURL); + // 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(TabsDemoURL); + // await webhintHTML(response); + }); + }).timeout(VERY_LONG_TEST_TIMEOUT); +}); + +describe("TabsDemo", () => { + 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 + TabsDemoURL); + }); + }).timeout(VERY_LONG_TEST_TIMEOUT); +});