Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/webui-press/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,13 @@ Generated root and page entries also receive build-time `<link rel="modulepreloa
Plus shadow components used by the chrome itself: `<docs-site-navigation>`,
`<docs-sidebar-navigation>`, `<docs-search>`, and `<docs-theme-toggle>`.

Related tab groups can share a page-local selection with the same `sync`
attribute on `<webui-press-tabs>` and matching `value` attributes on their tabs.
Add `storage="local"` to remember that category across page loads.
Ungrouped controls stay independent. See
[tabbed content](https://microsoft.github.io/webui/reference/webui-press/#tabbed-content)
for initial selection and lifetime behavior.

---

## Markdown features
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<template shadowrootmode="open">
<template shadowrootmode="open" @tab-select="{onTabSelect(e)}">
<div class="tabs-container">
<div class="tab-bar">
<slot name="tab"></slot>
Expand Down
142 changes: 126 additions & 16 deletions crates/webui-press/components/webui-press-tabs/webui-press-tabs.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,143 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { WebUIElement, observable } from "@microsoft/webui-framework";
import { WebUIElement, attr } from "@microsoft/webui-framework";

interface TabSelection {
value: string;
members: Set<WebUIPressTabs>;
chosen?: boolean;
storageKey?: string;
storage?: Storage;
}

const pageSelections = new WeakMap<Document, Map<string, TabSelection>>();

export class WebUIPressTabs extends WebUIElement {
@observable activeIndex = 0;
@attr sync = "";
@attr storage = "";

private activeIndex = 0;
private selection?: TabSelection;

connectedCallback(): void {
super.connectedCallback();

// Listen for tab-select events from child webui-press-tab components
this.addEventListener("tab-select", ((e: CustomEvent) => {
this.onTabSelect(e);
}) as EventListener);

// Find initially active tab
const tabs = [...this.querySelectorAll(":scope > webui-press-tab")];
const activeIdx = tabs.findIndex((t) => t.hasAttribute("active"));
this.activeIndex = activeIdx >= 0 ? activeIdx : 0;
const tabs = this.querySelectorAll(":scope > webui-press-tab");
this.activeIndex = 0;
for (let index = 0; index < tabs.length; index++) {
if (tabs[index].hasAttribute("active")) {
this.activeIndex = index;
break;
}
}
this.syncActive();
this.joinSelection();
}

onTabSelect(e: CustomEvent): void {
const tab = e.detail.tab as Element;
const tabs = [...this.querySelectorAll(":scope > webui-press-tab")];
const idx = tabs.indexOf(tab);
if (idx >= 0) {
disconnectedCallback(): void {
this.selection?.members.delete(this);
this.selection = undefined;
super.disconnectedCallback();
}

onTabSelect(e: CustomEvent<{ tab: Element }>): void {
const tab = e.detail.tab;
if (tab.parentElement !== this) return;
const tabs = this.querySelectorAll(":scope > webui-press-tab");
for (let idx = 0; idx < tabs.length; idx++) {
if (tabs[idx] !== tab) continue;
this.activeIndex = idx;
this.syncActive();
if (this.selection) {
const value = tab.getAttribute("value");
if (!value) {
console.error("[WebUI Press] Synchronized tabs require a nonempty value attribute.");
return;
}
this.selection.value = value;
this.selection.chosen = true;
this.saveSelection(this.selection);
for (const member of this.selection.members) {
if (member !== this) member.selectValue(value);
}
}
return;
}
}

private joinSelection(): void {
if (!this.sync) {
if (this.storage) {
console.error("[WebUI Press] Tab storage requires a sync category.");
}
return;
}
if (this.storage && this.storage !== "local") {
console.error('[WebUI Press] Unsupported tab storage; use storage="local" or omit it.');
}
const value = this.querySelector(":scope > webui-press-tab[active]")?.getAttribute("value");
if (!value) {
console.error("[WebUI Press] Synchronized tabs require a nonempty value attribute.");
return;
}
let groups = pageSelections.get(this.ownerDocument);
if (!groups) {
groups = new Map();
pageSelections.set(this.ownerDocument, groups);
}
let selection = groups.get(this.sync);
if (!selection) {
selection = { value, members: new Set() };
groups.set(this.sync, selection);
}
this.selection = selection;
this.restoreSelection(selection);
selection.members.add(this);
this.selectValue(selection.value);
}

private restoreSelection(selection: TabSelection): void {
if (this.storage !== "local" || selection.storageKey) return;
selection.storageKey = `webui-press:tabs:${this.sync}`;
let stored: string | null;
try {
const storage = this.ownerDocument.defaultView?.localStorage;
if (!storage) {
console.warn("[WebUI Press] Tab preferences cannot be restored without localStorage; selection remains page-local.");
return;
}
stored = storage.getItem(selection.storageKey);
selection.storage = storage;
} catch (error) {
console.warn("[WebUI Press] Cannot restore tab preferences; selection remains page-local.", error);
return;
}
if (stored !== null && !selection.chosen) {
selection.value = stored;
for (const member of selection.members) member.selectValue(stored);
}
if (selection.chosen) this.saveSelection(selection);
}

private saveSelection(selection: TabSelection): void {
if (!selection.storage || !selection.storageKey) return;
try {
selection.storage.setItem(selection.storageKey, selection.value);
} catch (error) {
selection.storage = undefined;
console.warn("[WebUI Press] Cannot save tab preferences; selection remains page-local.", error);
}
}

private selectValue(value: string): void {
const tabs = this.querySelectorAll(":scope > webui-press-tab");
for (let index = 0; index < tabs.length; index++) {
if (tabs[index].getAttribute("value") === value) {
this.activeIndex = index;
this.syncActive();
return;
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions crates/webui-press/template/docs.css
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@ body {
font-size: var(--docs-font-size-s);
}

.doc-content webui-press-tab-panel > code-block:only-child > pre {
margin: 0;
border-radius: 0;
}

.hl-keyword {
color: var(--docs-hl-keyword);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,15 +328,15 @@ test('the real preparation script emits deterministic ignored assets and fails i
const stateBytes = fs.readFileSync(stateFile, 'utf8');
const homeBytes = fs.readFileSync(homeFile, 'utf8');
const downloadBytes = fs.readFileSync(downloadFile, 'utf8');
const state = JSON.parse(stateBytes) as { benchmarks: BenchmarkView };
const home = JSON.parse(homeBytes) as {
previewMetrics: BenchmarkPreview[]; benchmarkNotes: BenchmarkNote[];
const state = JSON.parse(stateBytes) as {
benchmarks: BenchmarkView; benchmarkNotes: BenchmarkNote[];
};
const home = JSON.parse(homeBytes) as { previewMetrics: BenchmarkPreview[] };
assert.equal(state.benchmarks.selectedMetric, DEFAULT_METRIC);
assert.deepEqual(Object.keys(state), ['benchmarks']);
assert.deepEqual(Object.keys(home), ['previewMetrics', 'benchmarkNotes']);
assert.deepEqual(Object.keys(state), ['benchmarks', 'benchmarkNotes']);
assert.deepEqual(Object.keys(home), ['previewMetrics']);
assert.deepEqual(home.previewMetrics, buildBenchmarkPreviews(state.benchmarks));
assert.deepEqual(home.benchmarkNotes, buildBenchmarkNotes(state.benchmarks));
assert.deepEqual(state.benchmarkNotes, buildBenchmarkNotes(state.benchmarks));
assert.ok(Buffer.byteLength(JSON.stringify(home.previewMetrics)) < 4096,
'four three-case previews must stay below 4 KiB; notes are separate server-only state');
assert.deepEqual(JSON.parse(downloadBytes), prepareBenchmarkEvidence(source()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ <h2 id="preview-title">Benchmark snapshot</h2>
>{{metric.label}}</button>
</for>
</div>
<p class="visually-hidden" id="preview-help">The initial RPS chart is available without JavaScript. Enable JavaScript to switch tabs, or open the full data and methodology.</p>
<p class="visually-hidden" id="preview-help">The initial RPS chart is available without JavaScript. Enable JavaScript to switch tabs, or open the full framework comparison and methodology.</p>
<for each="metric in previewMetrics">
<section
key="{{metric.id}}"
Expand All @@ -32,7 +32,7 @@ <h2 id="preview-title">Benchmark snapshot</h2>
<caption class="chart-caption">
<span class="metric-title">
{{metric.caption}} ({{metric.unit}})<sup><a
href="{{base}}#benchmark-note-{{metric.id}}"
href="{{base}}reference/benchmarks/#benchmark-note-{{metric.id}}"
aria-label="{{metric.caption}} measurement notes"
>{{metric.noteNumber}}</a></sup>
</span>
Expand Down Expand Up @@ -73,6 +73,6 @@ <h2 id="preview-title">Benchmark snapshot</h2>
</if>
</section>
</for>
<a class="details-link" href="{{base}}reference/benchmarks/">Full data and methodology</a>
<a class="details-link" href="{{base}}reference/benchmarks/">More frameworks and methodology</a>
</section>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ test('homepage evidence preserves unavailable and measured-zero values', () => {
}
});

test('homepage notes use the chart metric order and preserve sampling and measurement limitations', () => {
test('benchmark notes use the chart metric order and preserve sampling and measurement limitations', () => {
const evidence = prepareBenchmarkEvidence(source());
const previews = buildBenchmarkPreviews(evidence);
const notes = buildBenchmarkNotes(evidence);
Expand Down Expand Up @@ -183,7 +183,8 @@ test('the homepage defaults to RPS and switches four ordered metrics without mou
await expect(preview.locator('.value')).toHaveText(metric.values);
await expect(preview.locator('.chart-caption')).toContainText(metric.caption);
await expect(preview.locator('.direction')).toHaveText(metric.direction);
await expect(page.locator(`#benchmark-note-${metric.id}`)).toContainText(metric.sampling);
await expect(preview.locator('.chart-caption sup a')).toHaveAttribute(
'href', `/webui/reference/benchmarks/#benchmark-note-${metric.id}`);
await expect(preview.locator('.scale')).toContainText(metric.maximum);
const widths = await preview.locator('.bar').evaluateAll(elements =>
elements.map(element => Number(element.getAttribute('width'))));
Expand All @@ -199,23 +200,24 @@ test('the homepage defaults to RPS and switches four ordered metrics without mou
assert.ok(shape.graphics && shape.width > 0 && shape.height > 0, 'Each selected bar must actually paint, not merely carry numeric attributes.');
}
await expect(preview.locator('.details-link')).toHaveAttribute('href', '/webui/reference/benchmarks/');
await expect(preview.getByRole('link', { name: 'Full data and methodology', exact: true })).toBeVisible();
await expect(preview.getByRole('link', { name: 'More frameworks and methodology', exact: true })).toBeVisible();
}
assert.deepEqual(errors, []);
assert.equal(await page.evaluate(() => customElements.get('benchmark-explorer') !== undefined), false);
});

test('metric notes are native document fragment targets with and without JavaScript', async t => {
test('metric notes link to the benchmark page with and without JavaScript', async t => {
const server = await startWebsite();
t.after(() => server.close());
const browser = await chromium.launch({ headless: true });
t.after(() => browser.close());
for (const javaScriptEnabled of [false, true]) {
const page = await browser.newPage({ javaScriptEnabled });
await page.route('https://**', route => route.abort());
await page.goto(`${server.origin}/webui/`);
const preview = page.locator('benchmark-preview');
for (const metric of javaScriptEnabled ? metrics : metrics.slice(0, 1)) {
await page.goto(`${server.origin}/webui/`);
await expect(page.locator('.benchmark-notes')).toHaveCount(0);
if (javaScriptEnabled) {
const tab = preview.getByRole('tab', { name: metric.label, exact: true });
await expect(tab).toBeEnabled();
Expand All @@ -224,7 +226,7 @@ test('metric notes are native document fragment targets with and without JavaScr
const link = preview.locator('.chart-caption sup a');
await expect(link).toHaveAccessibleName(/measurement notes/);
await link.click();
await expect(page).toHaveURL(`${server.origin}/webui/#benchmark-note-${metric.id}`);
await expect(page).toHaveURL(`${server.origin}/webui/reference/benchmarks/#benchmark-note-${metric.id}`);
const note = page.locator(`#benchmark-note-${metric.id}`);
assert.equal(await note.evaluate(element => element.getRootNode() === document), true);
await expect(note).toBeInViewport();
Expand Down
33 changes: 33 additions & 0 deletions docs/.webui-press/components/install-command/install-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ interface InstallOption {

type CopyState = "idle" | "copying" | "copied" | "failed" | "unavailable";

const packageManagerKey = "webui-press:tabs:package-manager";

export class InstallCommand extends WebUIElement {
@observable installOptions: InstallOption[] = [];
@observable installReady = false;
@observable copyState: CopyState = "idle";

private copyGeneration = 0;
private preferenceStorage?: Storage;

protected hydratedCallback(): void {
let selected = 0;
Expand All @@ -29,9 +32,31 @@ export class InstallCommand extends WebUIElement {
console.error("[WebUI docs] Install commands require exactly one selected command.");
return;
}
this.restoreManager();
this.installReady = true;
}

private restoreManager(): void {
let storage: Storage;
let stored: string | null;
try {
const available = this.ownerDocument.defaultView?.localStorage;
if (!available) {
console.warn("[WebUI docs] Package-manager preferences require localStorage; selection remains page-local.");
return;
}
storage = available;
stored = storage.getItem(packageManagerKey);
} catch (error) {
console.warn("[WebUI docs] Cannot restore package-manager preferences; selection remains page-local.", error);
return;
}
if (stored !== null && this.installOptions.some(option => option.id === stored)) {
this.selectManager(stored);
}
this.preferenceStorage = storage;
}

selectManager(id: string): void {
if (!this.installOptions.some(option => option.id === id)) {
this.copyState = "unavailable";
Expand All @@ -45,6 +70,14 @@ export class InstallCommand extends WebUIElement {
selected: option.id === id,
}));
if (this.copyState !== "copying") this.copyState = "idle";
if (this.preferenceStorage) {
try {
this.preferenceStorage.setItem(packageManagerKey, id);
} catch (error) {
this.preferenceStorage = undefined;
console.warn("[WebUI docs] Cannot save package-manager preferences; selection remains page-local.", error);
}
}
}

async copy(): Promise<void> {
Expand Down
20 changes: 0 additions & 20 deletions docs/.webui-press/regions/home-features.html
Original file line number Diff line number Diff line change
Expand Up @@ -137,23 +137,3 @@ <h2 id="start-title">Make your first page</h2>
</div>
<a class="site-button" href="{{site.base}}start/first-page/">Start building</a>
</section>

<section class="benchmark-notes" aria-labelledby="benchmark-notes-title">
<h2 id="benchmark-notes-title">Benchmark notes</h2>
<p>
Official campaign results for a 100-todo app, not a current framework ranking.
The featured WebUI configuration uses Light DOM with Rust and Actix.
</p>
<ol>
<for each="note in benchmarkNotes">
<li id="benchmark-note-{{note.id}}" tabindex="-1">
<strong>{{note.label}}</strong>. {{note.description}} {{note.samplingLabel}}
</li>
</for>
</ol>
<p>
The campaign covers all 15 configurations from one clean source revision.
The download is a sanitized extract, not a complete raw-run archive.
<a href="{{site.base}}reference/benchmarks/">Full data and methodology</a>
</p>
</section>
6 changes: 4 additions & 2 deletions docs/.webui-press/scripts/prepare-benchmarks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ try {
);
}
const fullView = buildBenchmarkView(evidence);
const renderedState = { benchmarks: fullView };
const renderedState = {
benchmarks: fullView,
benchmarkNotes: buildBenchmarkNotes(evidence),
};
const previewState = {
previewMetrics: buildBenchmarkPreviews(evidence),
benchmarkNotes: buildBenchmarkNotes(evidence),
};
for (const [file, content] of [[download, evidence], [state, renderedState], [homeState, previewState]]) {
try {
Expand Down
Loading
Loading