diff --git a/change/@fluentui-web-components-ff6dd466-4b60-4451-ab21-3929917fa50a.json b/change/@fluentui-web-components-ff6dd466-4b60-4451-ab21-3929917fa50a.json new file mode 100644 index 00000000000000..9caa1f8472275f --- /dev/null +++ b/change/@fluentui-web-components-ff6dd466-4b60-4451-ab21-3929917fa50a.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat(web-components): add SSR support via Declarative Shadow DOM modules", + "packageName": "@fluentui/web-components", + "email": "863023+radium-v@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/package.json b/package.json index 54fd34fa3aee17..256344319b0950 100644 --- a/package.json +++ b/package.json @@ -74,8 +74,9 @@ "@microsoft/eslint-plugin-sdl": "1.0.1", "@microsoft/fast-build": "0.6.0", "@microsoft/fast-element": "2.10.4", - "@microsoft/fast-test-harness": "0.1.0", - "@microsoft/focusgroup-polyfill": "^1.4.1", + "@microsoft/fast-html": "1.0.0-alpha.53", + "@microsoft/fast-test-harness": "0.3.0", + "@microsoft/focusgroup-polyfill": "1.5.0", "@microsoft/load-themed-styles": "1.10.26", "@microsoft/loader-load-themed-styles": "2.0.17", "@microsoft/tsdoc": "0.15.1", diff --git a/packages/web-components/package.json b/packages/web-components/package.json index ee9183d33c02cd..654315cfcb7f53 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -46,10 +46,13 @@ "./theme/*.js": "./dist/esm/theme/*.js", "./*/base.js": "./dist/esm/*/*.base.js", "./*/define.js": "./dist/esm/*/define.js", + "./*/define-async.js": "./dist/esm/*/define-async.js", "./*/definition.js": "./dist/esm/*/*.definition.js", "./*/options.js": "./dist/esm/*/*.options.js", "./*/styles.js": "./dist/esm/*/*.styles.js", + "./*/styles.css": "./dist/esm/*/*.styles.css", "./*/template.js": "./dist/esm/*/*.template.js", + "./*/template.html": "./dist/esm/*/*.template.html", "./*/index.js": "./dist/esm/*/index.js", "./*.js": "./dist/esm/*/define.js", "./custom-elements.json": "./custom-elements.json", @@ -57,6 +60,7 @@ }, "sideEffects": [ "define.*", + "define-async.*", "define-all.*", "index-rollup.*", "index-all-rollup.*", @@ -75,7 +79,11 @@ "compile:benchmark": "rollup -c rollup.bench.js", "clean": "node ./scripts/clean dist", "generate-api": "api-extractor run --local", - "build": "yarn compile && yarn rollup -c && yarn generate-api && yarn analyze", + "build": "yarn compile && yarn build:rollup && yarn build:ssr && yarn generate-api && yarn analyze", + "build:ssr:templates": "fast-test-harness generate-templates --tag-prefix=fluent", + "build:ssr:styles": "fast-test-harness generate-stylesheets", + "build:ssr": "yarn build:ssr:templates && yarn build:ssr:styles", + "build:rollup": "rollup -c", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", "format": "prettier -w src/**/*.{ts,html} src/*.{ts,html} --ignore-path ../../.prettierignore", @@ -106,8 +114,14 @@ }, "peerDependencies": { "@microsoft/fast-element": "^2.0.0", + "@microsoft/fast-html": "^1.0.0-alpha.53", "@microsoft/focusgroup-polyfill": "^1.4.1" }, + "peerDependenciesMeta": { + "@microsoft/fast-html": { + "optional": true + } + }, "beachball": { "disallowedChangeTypes": [ "major", diff --git a/packages/web-components/playwright.config.ts b/packages/web-components/playwright.config.ts index 8f98e850aef1e2..0ee6682982ec2f 100644 --- a/packages/web-components/playwright.config.ts +++ b/packages/web-components/playwright.config.ts @@ -1,10 +1,42 @@ import defaultConfig from '@microsoft/fast-test-harness/playwright.config.mjs'; -import { defineConfig } from '@playwright/test'; +import { defineConfig, devices } from '@playwright/test'; const CI = process.env.CI === 'true'; +// Duplicate each browser project across CSR and SSR rendering modes so a +// single `playwright test` run exercises both. Per-test overrides +// (e.g. `test.use({ ssr: true })`) still take precedence over the +// project-level value. +// +// TODO: Remove this local axis once the equivalent change lands in +// @microsoft/fast-test-harness/playwright.config.mjs. +const browsers = [ + { name: 'chromium', use: devices['Desktop Chrome'] }, + { name: 'firefox', use: devices['Desktop Firefox'] }, + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + deviceScaleFactor: 1, + }, + }, +]; + +const modes = [ + { suffix: 'csr', ssr: false }, + { suffix: 'ssr', ssr: true }, +]; + +const projects = browsers.flatMap(browser => + modes.map(mode => ({ + name: `${browser.name}-${mode.suffix}`, + use: { ...browser.use, ssr: mode.ssr }, + })), +); + export default defineConfig({ ...defaultConfig, + projects, reporter: CI ? 'github' : 'list', testMatch: 'src/**/*.spec.ts', }); diff --git a/packages/web-components/src/accordion-item/accordion-item.definition-async.ts b/packages/web-components/src/accordion-item/accordion-item.definition-async.ts new file mode 100644 index 00000000000000..cd0784a8cc6b77 --- /dev/null +++ b/packages/web-components/src/accordion-item/accordion-item.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './accordion-item.options.js'; + +/** + * The async definition configuration for the fluent-accordion-item element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/accordion-item/define-async.ts b/packages/web-components/src/accordion-item/define-async.ts new file mode 100644 index 00000000000000..09d74534a8dd1a --- /dev/null +++ b/packages/web-components/src/accordion-item/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './accordion-item.definition-async.js'; +import { AccordionItem } from './accordion-item.js'; + +RenderableFASTElement(AccordionItem).defineAsync(definition); diff --git a/packages/web-components/src/accordion/accordion.definition-async.ts b/packages/web-components/src/accordion/accordion.definition-async.ts new file mode 100644 index 00000000000000..80eb838510f606 --- /dev/null +++ b/packages/web-components/src/accordion/accordion.definition-async.ts @@ -0,0 +1,15 @@ +import { type PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './accordion.options.js'; + +/** + * The async definition configuration for the fluent-accordion element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const declarativeDefinition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +} as const; diff --git a/packages/web-components/src/accordion/accordion.ts b/packages/web-components/src/accordion/accordion.ts index cdfcbc24b14731..23bc871bbb5b89 100644 --- a/packages/web-components/src/accordion/accordion.ts +++ b/packages/web-components/src/accordion/accordion.ts @@ -110,7 +110,7 @@ export class Accordion extends FASTElement { */ private setItems = (): void => { waitForConnectedDescendants(this, () => { - if (this.slottedAccordionItems.length === 0) { + if (!this.slottedAccordionItems?.length) { return; } diff --git a/packages/web-components/src/accordion/define-async.ts b/packages/web-components/src/accordion/define-async.ts new file mode 100644 index 00000000000000..0e08c8e1611536 --- /dev/null +++ b/packages/web-components/src/accordion/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { declarativeDefinition } from './accordion.definition-async.js'; +import { Accordion } from './accordion.js'; + +RenderableFASTElement(Accordion).defineAsync(declarativeDefinition); diff --git a/packages/web-components/src/anchor-button/anchor-button.definition-async.ts b/packages/web-components/src/anchor-button/anchor-button.definition-async.ts new file mode 100644 index 00000000000000..b3e7245dd34a7d --- /dev/null +++ b/packages/web-components/src/anchor-button/anchor-button.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './anchor-button.options.js'; + +/** + * The async definition configuration for the fluent-anchor-button element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/anchor-button/anchor-button.spec.ts b/packages/web-components/src/anchor-button/anchor-button.spec.ts index e34999cea9260f..36a586cbc88f83 100644 --- a/packages/web-components/src/anchor-button/anchor-button.spec.ts +++ b/packages/web-components/src/anchor-button/anchor-button.spec.ts @@ -172,9 +172,11 @@ test.describe('Anchor Button', () => { await fastPage.setTemplate({ attributes: { href: expectedUrl } }); + expect(page.url()).not.toMatch(expectedUrl); + await element.click(); - await expect(page).toHaveURL(expectedUrl); + expect(page.url()).toMatch(expectedUrl); }); test('should emit a single click event when clicked', async ({ fastPage, page }) => { @@ -197,8 +199,9 @@ test.describe('Anchor Button', () => { }); test('should navigate to the provided url when clicked while pressing the `Control` key on Windows or `Meta` on Mac', async ({ - fastPage, context, + fastPage, + page, }) => { const { element } = fastPage; @@ -212,7 +215,11 @@ test.describe('Anchor Button', () => { const newPage = await newPagePromise; - await expect(newPage).toHaveURL(expectedUrl); + expect(page.url()).not.toMatch(expectedUrl); + + expect(newPage.url()).toMatch(expectedUrl); + + await newPage.close(); }); test('should navigate to the provided url when `Enter` is pressed via keyboard', async ({ fastPage, page }) => { @@ -226,12 +233,13 @@ test.describe('Anchor Button', () => { await element.press('Enter'); - await expect(page).toHaveURL(expectedUrl); + expect(page.url()).toMatch(expectedUrl); }); test('should navigate to the provided url when `ctrl` and `Enter` are pressed via keyboard', async ({ - fastPage, context, + fastPage, + page, }) => { const { element } = fastPage; @@ -249,6 +257,10 @@ test.describe('Anchor Button', () => { const newPage = await newPagePromise; - await expect(newPage).toHaveURL(expectedUrl); + expect(page.url()).not.toMatch(expectedUrl); + + expect(newPage.url()).toMatch(expectedUrl); + + await newPage.close(); }); }); diff --git a/packages/web-components/src/anchor-button/define-async.ts b/packages/web-components/src/anchor-button/define-async.ts new file mode 100644 index 00000000000000..16ee22566f0634 --- /dev/null +++ b/packages/web-components/src/anchor-button/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './anchor-button.definition-async.js'; +import { AnchorButton } from './anchor-button.js'; + +RenderableFASTElement(AnchorButton).defineAsync(definition); diff --git a/packages/web-components/src/avatar/avatar.definition-async.ts b/packages/web-components/src/avatar/avatar.definition-async.ts new file mode 100644 index 00000000000000..f3a01757798bd4 --- /dev/null +++ b/packages/web-components/src/avatar/avatar.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './avatar.options.js'; + +/** + * The async definition configuration for the fluent-avatar element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/avatar/avatar.spec.ts b/packages/web-components/src/avatar/avatar.spec.ts index 0556964a2eb83e..f5d375173d46a4 100644 --- a/packages/web-components/src/avatar/avatar.spec.ts +++ b/packages/web-components/src/avatar/avatar.spec.ts @@ -142,6 +142,7 @@ test.describe('Avatar', () => { // eslint-disable-next-line playwright/no-conditional-in-test if (color !== AvatarColor.colorful) { + // eslint-disable-next-line playwright/no-conditional-expect await expect.soft(element).toHaveAttribute('data-color', color); } }); diff --git a/packages/web-components/src/avatar/define-async.ts b/packages/web-components/src/avatar/define-async.ts new file mode 100644 index 00000000000000..6a720918f63410 --- /dev/null +++ b/packages/web-components/src/avatar/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './avatar.definition-async.js'; +import { Avatar } from './avatar.js'; + +RenderableFASTElement(Avatar).defineAsync(definition); diff --git a/packages/web-components/src/badge/badge.definition-async.ts b/packages/web-components/src/badge/badge.definition-async.ts new file mode 100644 index 00000000000000..14c467f1db83f5 --- /dev/null +++ b/packages/web-components/src/badge/badge.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './badge.options.js'; + +/** + * The async definition configuration for the fluent-badge element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/badge/define-async.ts b/packages/web-components/src/badge/define-async.ts new file mode 100644 index 00000000000000..17aaae438671f7 --- /dev/null +++ b/packages/web-components/src/badge/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './badge.definition-async.js'; +import { Badge } from './badge.js'; + +RenderableFASTElement(Badge).defineAsync(definition); diff --git a/packages/web-components/src/button/button.definition-async.ts b/packages/web-components/src/button/button.definition-async.ts new file mode 100644 index 00000000000000..19ed5035d8c049 --- /dev/null +++ b/packages/web-components/src/button/button.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './button.options.js'; + +/** + * The async definition configuration for the fluent-button element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/button/button.spec.ts b/packages/web-components/src/button/button.spec.ts index 263a8282786752..825d6ea64d6e11 100644 --- a/packages/web-components/src/button/button.spec.ts +++ b/packages/web-components/src/button/button.spec.ts @@ -1,3 +1,4 @@ +import type { InitialTemplateAttributes } from '@microsoft/fast-test-harness/fixtures/csr-fixture.js'; import { expect, test } from '../../test/playwright/index.js'; import { tagName } from './button.options.js'; @@ -262,10 +263,17 @@ test.describe('Button', () => { await expect(notFocusable).not.toBeFocused(); }); - test('should focus the element when the `autofocus` attribute is present', async ({ fastPage }) => { + test('should focus the element when the `autofocus` attribute is present', async ({ fastPage, ssr }) => { const { element } = fastPage; - await fastPage.setTemplate({ attributes: { autofocus: true } }); + const attributes: InitialTemplateAttributes = { autofocus: true }; + + if (ssr) { + // the host element needs to be focusable for autofocus to work on the server, so we need to set tabindex="0" + attributes.tabindex = '0'; + } + + await fastPage.setTemplate({ attributes }); await expect(element).toBeFocused(); }); diff --git a/packages/web-components/src/button/define-async.ts b/packages/web-components/src/button/define-async.ts new file mode 100644 index 00000000000000..723c7c2fe9f928 --- /dev/null +++ b/packages/web-components/src/button/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './button.definition-async.js'; +import { Button } from './button.js'; + +RenderableFASTElement(Button).defineAsync(definition); diff --git a/packages/web-components/src/checkbox/checkbox.base.ts b/packages/web-components/src/checkbox/checkbox.base.ts index 79c8f4a32aa4ea..b378112216ff95 100644 --- a/packages/web-components/src/checkbox/checkbox.base.ts +++ b/packages/web-components/src/checkbox/checkbox.base.ts @@ -177,7 +177,7 @@ export class BaseCheckbox extends FASTElement { * @internal */ protected requiredChanged(prev: boolean, next: boolean): void { - if (this.$fastController.isConnected) { + if (this.elementInternals) { this.setValidity(); this.elementInternals.ariaRequired = this.required ? 'true' : 'false'; } @@ -248,7 +248,7 @@ export class BaseCheckbox extends FASTElement { * Reflects the {@link https://developer.mozilla.org/docs/Web/API/ElementInternals/validationMessage | `ElementInternals.validationMessage`} property. */ public get validationMessage(): string { - if (this.elementInternals.validationMessage) { + if (this.elementInternals?.validationMessage) { return this.elementInternals.validationMessage; } @@ -295,11 +295,12 @@ export class BaseCheckbox extends FASTElement { public set value(value: string) { this._value = value; - if (this.$fastController.isConnected) { + if (this.elementInternals) { this.setFormValue(value); this.setValidity(); - Observable.notify(this, 'value'); } + + Observable.notify(this, 'value'); } /** @@ -429,7 +430,9 @@ export class BaseCheckbox extends FASTElement { * @internal */ protected setAriaChecked(value: boolean = this.checked) { - this.elementInternals.ariaChecked = value ? 'true' : 'false'; + if (this.elementInternals) { + this.elementInternals.ariaChecked = value ? 'true' : 'false'; + } } /** @@ -438,7 +441,7 @@ export class BaseCheckbox extends FASTElement { * @internal */ public setFormValue(value: File | string | FormData | null, state?: File | string | FormData | null): void { - this.elementInternals.setFormValue(value, value ?? state); + this.elementInternals?.setFormValue(value, value ?? state); } /** @@ -462,7 +465,7 @@ export class BaseCheckbox extends FASTElement { * @internal */ public setValidity(flags?: Partial, message?: string, anchor?: HTMLElement): void { - if (this.$fastController.isConnected) { + if (this.elementInternals) { if (this.disabled || !this.required) { this.elementInternals.setValidity({}); return; diff --git a/packages/web-components/src/checkbox/checkbox.definition-async.ts b/packages/web-components/src/checkbox/checkbox.definition-async.ts new file mode 100644 index 00000000000000..2ecc0367055d5c --- /dev/null +++ b/packages/web-components/src/checkbox/checkbox.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './checkbox.options.js'; + +/** + * The async definition configuration for the fluent-checkbox element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/checkbox/define-async.ts b/packages/web-components/src/checkbox/define-async.ts new file mode 100644 index 00000000000000..c203ddd5c93456 --- /dev/null +++ b/packages/web-components/src/checkbox/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './checkbox.definition-async.js'; +import { Checkbox } from './checkbox.js'; + +RenderableFASTElement(Checkbox).defineAsync(definition); diff --git a/packages/web-components/src/compound-button/compound-button.definition-async.ts b/packages/web-components/src/compound-button/compound-button.definition-async.ts new file mode 100644 index 00000000000000..bdcaa313420b97 --- /dev/null +++ b/packages/web-components/src/compound-button/compound-button.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './compound-button.options.js'; + +/** + * The async definition configuration for the fluent-compound-button element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/compound-button/compound-button.spec.ts b/packages/web-components/src/compound-button/compound-button.spec.ts index f0df3f89b8589a..6afd843b8dd7ec 100644 --- a/packages/web-components/src/compound-button/compound-button.spec.ts +++ b/packages/web-components/src/compound-button/compound-button.spec.ts @@ -1,3 +1,4 @@ +import type { InitialTemplateAttributes } from '@microsoft/fast-test-harness/fixtures/csr-fixture.js'; import { expect, test } from '../../test/playwright/index.js'; import { tagName } from './compound-button.options.js'; @@ -261,10 +262,17 @@ test.describe('Compound Button', () => { await expect(element).not.toBeFocused(); }); - test('should focus the element when the `autofocus` attribute is present', async ({ fastPage }) => { + test('should focus the element when the `autofocus` attribute is present', async ({ fastPage, ssr }) => { const { element } = fastPage; - await fastPage.setTemplate({ attributes: { autofocus: true } }); + const attributes: InitialTemplateAttributes = { autofocus: true }; + + if (ssr) { + // the host element needs to be focusable for autofocus to work on the server, so we need to set tabindex="0" + attributes.tabindex = '0'; + } + + await fastPage.setTemplate({ attributes }); await expect(element).toBeFocused(); }); diff --git a/packages/web-components/src/compound-button/define-async.ts b/packages/web-components/src/compound-button/define-async.ts new file mode 100644 index 00000000000000..4f869a39afd124 --- /dev/null +++ b/packages/web-components/src/compound-button/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './compound-button.definition-async.js'; +import { CompoundButton } from './compound-button.js'; + +RenderableFASTElement(CompoundButton).defineAsync(definition); diff --git a/packages/web-components/src/counter-badge/counter-badge.definition-async.ts b/packages/web-components/src/counter-badge/counter-badge.definition-async.ts new file mode 100644 index 00000000000000..9695542eb818e6 --- /dev/null +++ b/packages/web-components/src/counter-badge/counter-badge.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './counter-badge.options.js'; + +/** + * The async definition configuration for the fluent-counter-badge element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/counter-badge/define-async.ts b/packages/web-components/src/counter-badge/define-async.ts new file mode 100644 index 00000000000000..18bd0394962287 --- /dev/null +++ b/packages/web-components/src/counter-badge/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './counter-badge.definition-async.js'; +import { CounterBadge } from './counter-badge.js'; + +RenderableFASTElement(CounterBadge).defineAsync(definition); diff --git a/packages/web-components/src/dialog-body/define-async.ts b/packages/web-components/src/dialog-body/define-async.ts new file mode 100644 index 00000000000000..8b228a007c3848 --- /dev/null +++ b/packages/web-components/src/dialog-body/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './dialog-body.definition-async.js'; +import { DialogBody } from './dialog-body.js'; + +RenderableFASTElement(DialogBody).defineAsync(definition); diff --git a/packages/web-components/src/dialog-body/dialog-body.definition-async.ts b/packages/web-components/src/dialog-body/dialog-body.definition-async.ts new file mode 100644 index 00000000000000..5bff152fdb8406 --- /dev/null +++ b/packages/web-components/src/dialog-body/dialog-body.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './dialog-body.options.js'; + +/** + * The async definition configuration for the fluent-dialog-body element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/dialog/define-async.ts b/packages/web-components/src/dialog/define-async.ts new file mode 100644 index 00000000000000..9200297217c28d --- /dev/null +++ b/packages/web-components/src/dialog/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './dialog.definition-async.js'; +import { Dialog } from './dialog.js'; + +RenderableFASTElement(Dialog).defineAsync(definition); diff --git a/packages/web-components/src/dialog/dialog.definition-async.ts b/packages/web-components/src/dialog/dialog.definition-async.ts new file mode 100644 index 00000000000000..c1b2d03a92a3f5 --- /dev/null +++ b/packages/web-components/src/dialog/dialog.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './dialog.options.js'; + +/** + * The async definition configuration for the fluent-dialog element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/divider/define-async.ts b/packages/web-components/src/divider/define-async.ts new file mode 100644 index 00000000000000..43f51d9a29a99f --- /dev/null +++ b/packages/web-components/src/divider/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './divider.definition-async.js'; +import { Divider } from './divider.js'; + +RenderableFASTElement(Divider).defineAsync(definition); diff --git a/packages/web-components/src/divider/divider.definition-async.ts b/packages/web-components/src/divider/divider.definition-async.ts new file mode 100644 index 00000000000000..912edb20cb22a5 --- /dev/null +++ b/packages/web-components/src/divider/divider.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './divider.options.js'; + +/** + * The async definition configuration for the fluent-divider element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/drawer-body/define-async.ts b/packages/web-components/src/drawer-body/define-async.ts new file mode 100644 index 00000000000000..fa7d72dabfd88f --- /dev/null +++ b/packages/web-components/src/drawer-body/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './drawer-body.definition-async.js'; +import { DrawerBody } from './drawer-body.js'; + +RenderableFASTElement(DrawerBody).defineAsync(definition); diff --git a/packages/web-components/src/drawer-body/drawer-body.definition-async.ts b/packages/web-components/src/drawer-body/drawer-body.definition-async.ts new file mode 100644 index 00000000000000..c89b63285d5f8d --- /dev/null +++ b/packages/web-components/src/drawer-body/drawer-body.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './drawer-body.options.js'; + +/** + * The async definition configuration for the fluent-drawer-body element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/drawer/define-async.ts b/packages/web-components/src/drawer/define-async.ts new file mode 100644 index 00000000000000..aa5fc1b908b2ba --- /dev/null +++ b/packages/web-components/src/drawer/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './drawer.definition-async.js'; +import { Drawer } from './drawer.js'; + +RenderableFASTElement(Drawer).defineAsync(definition); diff --git a/packages/web-components/src/drawer/drawer.definition-async.ts b/packages/web-components/src/drawer/drawer.definition-async.ts new file mode 100644 index 00000000000000..b35e299b46b71d --- /dev/null +++ b/packages/web-components/src/drawer/drawer.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './drawer.options.js'; + +/** + * The async definition configuration for the fluent-drawer element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/dropdown/define-async.ts b/packages/web-components/src/dropdown/define-async.ts new file mode 100644 index 00000000000000..55b665c210b7fc --- /dev/null +++ b/packages/web-components/src/dropdown/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './dropdown.definition-async.js'; +import { Dropdown } from './dropdown.js'; + +RenderableFASTElement(Dropdown).defineAsync(definition); diff --git a/packages/web-components/src/dropdown/dropdown.base.ts b/packages/web-components/src/dropdown/dropdown.base.ts index 8f14dbd1ae7243..0521679615e389 100644 --- a/packages/web-components/src/dropdown/dropdown.base.ts +++ b/packages/web-components/src/dropdown/dropdown.base.ts @@ -232,24 +232,20 @@ export class BaseDropdown extends FASTElement { notifier.notify('multiple'); - waitForConnectedDescendants( - next, - () => { - this.options.forEach(option => { - option.disabled = option.disabledAttribute || this.disabled; - option.name = this.name; - }); + Updates.enqueue(() => { + this.options.forEach(option => { + option.disabled = option.disabledAttribute || this.disabled; + option.name = this.name; + }); - this.enabledOptions - .filter(x => x.defaultSelected) - .forEach((x, i) => { - x.selected = this.multiple || i === 0; - }); + this.enabledOptions + .filter(x => x.defaultSelected) + .forEach((x, i) => { + x.selected = this.multiple || i === 0; + }); - this.setValidity(); - }, - { idleCallback: true }, - ); + this.setValidity(); + }); if (AnchorPositioningCSSSupported) { // The `anchor-name` property seems to not be isolated between instances in Safari Technology Preview 220 (18.4). @@ -448,7 +444,10 @@ export class BaseDropdown extends FASTElement { * @public */ public get enabledOptions(): DropdownOption[] { - return this.listbox?.enabledOptions ?? []; + return ( + this.listbox?.enabledOptions ?? + Array.from(this.querySelectorAll('*')).filter((o): o is DropdownOption => isDropdownOption(o) && !o.disabled) + ); } /** @@ -513,7 +512,9 @@ export class BaseDropdown extends FASTElement { * @public */ public get options(): DropdownOption[] { - return this.listbox?.options ?? []; + return ( + this.listbox?.options ?? Array.from(this.querySelectorAll('*')).filter(o => isDropdownOption(o)) + ); } /** diff --git a/packages/web-components/src/dropdown/dropdown.definition-async.ts b/packages/web-components/src/dropdown/dropdown.definition-async.ts new file mode 100644 index 00000000000000..bc13f53701b301 --- /dev/null +++ b/packages/web-components/src/dropdown/dropdown.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './dropdown.options.js'; + +/** + * The async definition configuration for the fluent-dropdown element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/field/define-async.ts b/packages/web-components/src/field/define-async.ts new file mode 100644 index 00000000000000..f8ed499e649d83 --- /dev/null +++ b/packages/web-components/src/field/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './field.definition-async.js'; +import { Field } from './field.js'; + +RenderableFASTElement(Field).defineAsync(definition); diff --git a/packages/web-components/src/field/field.definition-async.ts b/packages/web-components/src/field/field.definition-async.ts new file mode 100644 index 00000000000000..0dc76c88961f24 --- /dev/null +++ b/packages/web-components/src/field/field.definition-async.ts @@ -0,0 +1,18 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './field.options.js'; + +/** + * The async definition configuration for the fluent-field element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', + shadowOptions: { + delegatesFocus: true, + }, +}; diff --git a/packages/web-components/src/image/define-async.ts b/packages/web-components/src/image/define-async.ts new file mode 100644 index 00000000000000..870830b404310a --- /dev/null +++ b/packages/web-components/src/image/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './image.definition-async.js'; +import { Image } from './image.js'; + +RenderableFASTElement(Image).defineAsync(definition); diff --git a/packages/web-components/src/image/image.definition-async.ts b/packages/web-components/src/image/image.definition-async.ts new file mode 100644 index 00000000000000..6b8aafbd2206c0 --- /dev/null +++ b/packages/web-components/src/image/image.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './image.options.js'; + +/** + * The async definition configuration for the fluent-image element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/label/define-async.ts b/packages/web-components/src/label/define-async.ts new file mode 100644 index 00000000000000..8ebd43f6a12dd2 --- /dev/null +++ b/packages/web-components/src/label/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './label.definition-async.js'; +import { Label } from './label.js'; + +RenderableFASTElement(Label).defineAsync(definition); diff --git a/packages/web-components/src/label/label.definition-async.ts b/packages/web-components/src/label/label.definition-async.ts new file mode 100644 index 00000000000000..920ed9210dd562 --- /dev/null +++ b/packages/web-components/src/label/label.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './label.options.js'; + +/** + * The async definition configuration for the fluent-label element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/link/define-async.ts b/packages/web-components/src/link/define-async.ts new file mode 100644 index 00000000000000..5afa6875316223 --- /dev/null +++ b/packages/web-components/src/link/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './link.definition-async.js'; +import { Link } from './link.js'; + +RenderableFASTElement(Link).defineAsync(definition); diff --git a/packages/web-components/src/link/link.definition-async.ts b/packages/web-components/src/link/link.definition-async.ts new file mode 100644 index 00000000000000..200ff916aa0e93 --- /dev/null +++ b/packages/web-components/src/link/link.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './link.options.js'; + +/** + * The async definition configuration for the fluent-link element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/listbox/define-async.ts b/packages/web-components/src/listbox/define-async.ts new file mode 100644 index 00000000000000..e24628e1317941 --- /dev/null +++ b/packages/web-components/src/listbox/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './listbox.definition-async.js'; +import { Listbox } from './listbox.js'; + +RenderableFASTElement(Listbox).defineAsync(definition); diff --git a/packages/web-components/src/listbox/listbox.definition-async.ts b/packages/web-components/src/listbox/listbox.definition-async.ts new file mode 100644 index 00000000000000..1aa451393212d5 --- /dev/null +++ b/packages/web-components/src/listbox/listbox.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './listbox.options.js'; + +/** + * The async definition configuration for the fluent-listbox element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/menu-button/define-async.ts b/packages/web-components/src/menu-button/define-async.ts new file mode 100644 index 00000000000000..b1e8d80315b700 --- /dev/null +++ b/packages/web-components/src/menu-button/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './menu-button.definition-async.js'; +import { MenuButton } from './menu-button.js'; + +RenderableFASTElement(MenuButton).defineAsync(definition); diff --git a/packages/web-components/src/menu-button/menu-button.definition-async.ts b/packages/web-components/src/menu-button/menu-button.definition-async.ts new file mode 100644 index 00000000000000..475b17a276d8c4 --- /dev/null +++ b/packages/web-components/src/menu-button/menu-button.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './menu-button.options.js'; + +/** + * The async definition configuration for the fluent-menu-button element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/menu-button/menu-button.definition.ts b/packages/web-components/src/menu-button/menu-button.definition.ts index 8f199cb7b56018..2a7adf248deac5 100644 --- a/packages/web-components/src/menu-button/menu-button.definition.ts +++ b/packages/web-components/src/menu-button/menu-button.definition.ts @@ -1,4 +1,4 @@ -import { styles } from '../button/button.styles.js'; +import { styles } from './menu-button.styles.js'; import { tagName } from './menu-button.options.js'; import { MenuButton } from './menu-button.js'; import { template } from './menu-button.template.js'; diff --git a/packages/web-components/src/menu-button/menu-button.spec.ts b/packages/web-components/src/menu-button/menu-button.spec.ts index 2878631713eb33..e88dadc942242e 100644 --- a/packages/web-components/src/menu-button/menu-button.spec.ts +++ b/packages/web-components/src/menu-button/menu-button.spec.ts @@ -1,6 +1,6 @@ +import type { InitialTemplateAttributes } from '@microsoft/fast-test-harness/fixtures/csr-fixture.js'; import { expect, test } from '../../test/playwright/index.js'; -import { MenuButtonAppearance, MenuButtonSize } from './menu-button.options.js'; -import { tagName } from './menu-button.options.js'; +import { MenuButtonAppearance, MenuButtonSize, tagName } from './menu-button.options.js'; test.describe('MenuButton', () => { test.use({ @@ -317,10 +317,17 @@ test.describe('MenuButton', () => { await expect(element).not.toBeFocused(); }); - test('should focus the element when the `autofocus` attribute is present', async ({ fastPage }) => { + test('should focus the element when the `autofocus` attribute is present', async ({ fastPage, ssr }) => { const { element } = fastPage; - await fastPage.setTemplate({ attributes: { autofocus: true } }); + const attributes: InitialTemplateAttributes = { autofocus: true }; + + if (ssr) { + // the host element needs to be focusable for autofocus to work on the server, so we need to set tabindex="0" + attributes.tabindex = '0'; + } + + await fastPage.setTemplate({ attributes }); await expect(element).toBeFocused(); }); diff --git a/packages/web-components/src/menu-button/menu-button.styles.ts b/packages/web-components/src/menu-button/menu-button.styles.ts new file mode 100644 index 00000000000000..0660316f6bd105 --- /dev/null +++ b/packages/web-components/src/menu-button/menu-button.styles.ts @@ -0,0 +1,8 @@ +import { styles as ButtonStyles } from '../button/button.styles.js'; + +/** + * Styles for the MenuButton component + * + * @public + */ +export const styles = ButtonStyles; diff --git a/packages/web-components/src/menu-item/define-async.ts b/packages/web-components/src/menu-item/define-async.ts new file mode 100644 index 00000000000000..1e12121ec3a638 --- /dev/null +++ b/packages/web-components/src/menu-item/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './menu-item.definition-async.js'; +import { MenuItem } from './menu-item.js'; + +RenderableFASTElement(MenuItem).defineAsync(definition); diff --git a/packages/web-components/src/menu-item/menu-item.definition-async.ts b/packages/web-components/src/menu-item/menu-item.definition-async.ts new file mode 100644 index 00000000000000..8c786ea119dc6a --- /dev/null +++ b/packages/web-components/src/menu-item/menu-item.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './menu-item.options.js'; + +/** + * The async definition configuration for the fluent-menu-item element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/menu-list/define-async.ts b/packages/web-components/src/menu-list/define-async.ts new file mode 100644 index 00000000000000..2af6afeac35ac7 --- /dev/null +++ b/packages/web-components/src/menu-list/define-async.ts @@ -0,0 +1,5 @@ +import { RenderableFASTElement } from '@microsoft/fast-html'; +import { definition } from './menu-list.definition-async.js'; +import { MenuList } from './menu-list.js'; + +RenderableFASTElement(MenuList).defineAsync(definition); diff --git a/packages/web-components/src/menu-list/menu-list.base.ts b/packages/web-components/src/menu-list/menu-list.base.ts index 6ebcd314be5c0d..01eb35094a0682 100644 --- a/packages/web-components/src/menu-list/menu-list.base.ts +++ b/packages/web-components/src/menu-list/menu-list.base.ts @@ -49,6 +49,11 @@ export class BaseMenuList extends FASTElement { */ public connectedCallback(): void { super.connectedCallback(); + + if (!this.slot && this.isNestedMenu()) { + this.slot = 'submenu'; + } + Updates.enqueue(() => { // wait until children have had a chance to // connect before setting/checking their props/attributes diff --git a/packages/web-components/src/menu-list/menu-list.definition-async.ts b/packages/web-components/src/menu-list/menu-list.definition-async.ts new file mode 100644 index 00000000000000..b40b105d34f2b5 --- /dev/null +++ b/packages/web-components/src/menu-list/menu-list.definition-async.ts @@ -0,0 +1,15 @@ +import type { PartialFASTElementDefinition } from '@microsoft/fast-element'; +import { tagName } from './menu-list.options.js'; + +/** + * The async definition configuration for the fluent-menu-list element. + * + * @public + * @remarks + * This is used in server-side rendering (SSR) scenarios where the template + * is provided as a deferred option to be hydrated later. + */ +export const definition: PartialFASTElementDefinition = { + name: tagName, + templateOptions: 'defer-and-hydrate', +}; diff --git a/packages/web-components/src/menu-list/menu-list.spec.ts b/packages/web-components/src/menu-list/menu-list.spec.ts index 686e363528020c..125a7a1cceae26 100644 --- a/packages/web-components/src/menu-list/menu-list.spec.ts +++ b/packages/web-components/src/menu-list/menu-list.spec.ts @@ -178,6 +178,12 @@ test.describe('MenuList', () => { node.removeAttribute('hidden'); }); + await element.evaluate(node => { + node.focus(); + }); + + await expect(menuItems.nth(0)).toBeFocused(); + await element.press('ArrowDown'); await expect(menuItems.nth(1)).toBeFocused(); @@ -663,12 +669,10 @@ test.describe('MenuList', () => { await fastPage.setTemplate({ innerHTML: /* html */ ` - <${tagName}> - <${MenuItemTagName} role="menuitemradio">Menu Item 1 - <${MenuItemTagName} checked role="menuitemradio">Menu item 2 - <${MenuItemTagName} role="menuitemradio">Menu item 3 - <${MenuItemTagName} role="menuitemradio">Menu item 4 - + <${MenuItemTagName} role="menuitemradio">Menu Item 1 + <${MenuItemTagName} checked role="menuitemradio">Menu item 2 + <${MenuItemTagName} role="menuitemradio">Menu item 3 + <${MenuItemTagName} role="menuitemradio">Menu item 4 `, }); diff --git a/packages/web-components/src/menu-list/menu-list.template.ts b/packages/web-components/src/menu-list/menu-list.template.ts index f0f347186ed5cd..f99971e85dc6fd 100644 --- a/packages/web-components/src/menu-list/menu-list.template.ts +++ b/packages/web-components/src/menu-list/menu-list.template.ts @@ -3,7 +3,7 @@ import type { MenuList } from './menu-list.js'; export function menuTemplate(): ElementViewTemplate { return html` -