From 557e291a8715369631fb3e41c8ffbab3653f4265 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Fri, 22 May 2026 11:59:28 -0700
Subject: [PATCH 01/44] feat(web-components): update dependencies and add SSR
build scripts
---
package.json | 5 +++--
packages/web-components/package.json | 16 +++++++++++++++-
yarn.lock | 19 +++++++++++--------
3 files changed, 29 insertions(+), 11 deletions(-)
diff --git a/package.json b/package.json
index 54fd34fa3aee17..faa30d5a60204f 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": "file:.yalc/@microsoft/fast-test-harness",
+ "@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/yarn.lock b/yarn.lock
index b2e66256a2a602..d17d71d90a431c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2757,10 +2757,13 @@
resolved "https://registry.yarnpkg.com/@microsoft/fast-element/-/fast-element-2.10.4.tgz#12b8c6c90902f2a54d5b27f618d7d095e133546d"
integrity sha512-OkHIlMztq7+PgkRF1LscgBd/MVzMhGrWPkJRDvOEqdqEdZOKocKvaKcUKZubIBz4RpLIrhLD3lOJzCsspk6jXg==
-"@microsoft/fast-test-harness@0.1.0":
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/@microsoft/fast-test-harness/-/fast-test-harness-0.1.0.tgz#e372cd6e23b921ee4c5aca35a5eb2bcf6b60843a"
- integrity sha512-TEbNxPBbF9XDlqZTF21fymWrTUhBpPxqi4yznFzTdX+mBslNTLPfqOUXxd4zd0Vz1zn3SKtW4OIop0vCSu1oXQ==
+"@microsoft/fast-html@1.0.0-alpha.53":
+ version "1.0.0-alpha.53"
+ resolved "https://registry.yarnpkg.com/@microsoft/fast-html/-/fast-html-1.0.0-alpha.53.tgz#e656a38386a998640fd7af40bbcd5aae321092f9"
+ integrity sha512-S6fUlNjk3aj9SP1U4sRWA1Cf1gns7FLJOed3NMvndWQBfmQczVX8CHXbfVKF55B7nKWRkAQwDHN0wOu7yxw+aw==
+
+"@microsoft/fast-test-harness@file:.yalc/@microsoft/fast-test-harness":
+ version "0.2.0"
dependencies:
cheerio "1.2.0"
@@ -2771,10 +2774,10 @@
dependencies:
exenv-es6 "^1.1.1"
-"@microsoft/focusgroup-polyfill@^1.4.1":
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/@microsoft/focusgroup-polyfill/-/focusgroup-polyfill-1.4.1.tgz#0d3feee675775f7a692a3b1e974eea107503c78e"
- integrity sha512-nXn/kJ5ZnzpR+TWYGV+nU2yaHPJORjWf2lSQJ90VT6Ydsmmkaz/yOYW3CizDGPPsA7ouAu9K8khCUeyfBcz7UA==
+"@microsoft/focusgroup-polyfill@1.5.0":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@microsoft/focusgroup-polyfill/-/focusgroup-polyfill-1.5.0.tgz#f59ab909169b3c82921d845e2034f50426eeab87"
+ integrity sha512-2tw+AISULD5xhP+wIqUheej5wZbwiuPaqjuCa8A8jLdq2ikklZBIvSDLLa6j7zbDbtV5BXnrYgSuyW1Bj5JA4A==
"@microsoft/load-themed-styles@1.10.26", "@microsoft/load-themed-styles@^1.10.26":
version "1.10.26"
From 230bf463359beee300e335d77639c2a120783aab Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 21:44:22 -0700
Subject: [PATCH 02/44] update fast-test-harness dependency to version 0.3.0
---
package.json | 2 +-
yarn.lock | 6 ++++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/package.json b/package.json
index faa30d5a60204f..256344319b0950 100644
--- a/package.json
+++ b/package.json
@@ -75,7 +75,7 @@
"@microsoft/fast-build": "0.6.0",
"@microsoft/fast-element": "2.10.4",
"@microsoft/fast-html": "1.0.0-alpha.53",
- "@microsoft/fast-test-harness": "file:.yalc/@microsoft/fast-test-harness",
+ "@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",
diff --git a/yarn.lock b/yarn.lock
index d17d71d90a431c..a611ecd66bd487 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2762,8 +2762,10 @@
resolved "https://registry.yarnpkg.com/@microsoft/fast-html/-/fast-html-1.0.0-alpha.53.tgz#e656a38386a998640fd7af40bbcd5aae321092f9"
integrity sha512-S6fUlNjk3aj9SP1U4sRWA1Cf1gns7FLJOed3NMvndWQBfmQczVX8CHXbfVKF55B7nKWRkAQwDHN0wOu7yxw+aw==
-"@microsoft/fast-test-harness@file:.yalc/@microsoft/fast-test-harness":
- version "0.2.0"
+"@microsoft/fast-test-harness@0.3.0":
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/@microsoft/fast-test-harness/-/fast-test-harness-0.3.0.tgz#9d5536c8cbf1e4ecae9d70bfe0fe105b5e215caf"
+ integrity sha512-EZ0iLxy/AEaOMthJmalD4fIp4rAM/ypBfqXmMJD4ZffVUVnLkfUw2NFZNUIsMbP/S4+tsfBnBlX5tyKSYD5TIQ==
dependencies:
cheerio "1.2.0"
From 39ac8409953dfef0a2b14c7ce4ca64dd14de66f4 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Fri, 22 May 2026 12:00:19 -0700
Subject: [PATCH 03/44] add SSR entry modules and HTML template for testing
---
.../web-components/test/src/entry-client.ts | 23 +++++++++++++++++
.../web-components/test/src/entry-server.ts | 25 +++++++++++++++++++
packages/web-components/test/ssr.html | 15 +++++++++++
3 files changed, 63 insertions(+)
create mode 100644 packages/web-components/test/src/entry-client.ts
create mode 100644 packages/web-components/test/src/entry-server.ts
create mode 100644 packages/web-components/test/ssr.html
diff --git a/packages/web-components/test/src/entry-client.ts b/packages/web-components/test/src/entry-client.ts
new file mode 100644
index 00000000000000..9d71f2c4f07402
--- /dev/null
+++ b/packages/web-components/test/src/entry-client.ts
@@ -0,0 +1,23 @@
+/**
+ * SSR client-side entry module for the Fluent UI web-components test harness.
+ *
+ * This module is loaded by the test harness on the SSR entry page (`ssr.html`) and is responsible for providing
+ * the necessary client-side setup to hydrate the server-rendered HTML and run the tests. This includes defining any
+ * components used in the SSR fixtures and importing any necessary polyfills or shared setup code.
+ *
+ * Note that this module is separate from the CSR test entry point (`main.ts`) to avoid loading unnecessary code during
+ * SSR tests. Only code that is required for hydrating the SSR fixtures and running the tests should be included here.
+ */
+
+import '@microsoft/fast-test-harness/ssr/entry-client.js';
+import './common.js';
+
+/**
+ * `import.meta.glob` is a Vite-specific feature that allows us to dynamically import all component definitions without
+ * having to manually maintain a list. The `eager: true` option ensures that these modules are imported immediately.
+ *
+ * The SSR/DSD fixtures require a slightly different set of component definitions than the CSR fixtures, facilitated
+ * by the `define-async` modules - these definitions don't include templates or styles, since those are already included
+ * in the server-rendered HTML and don't need to be reloaded on the client via the FAST Element component lifecycle.
+ */
+import.meta.glob('../../src/*/define-async.{ts,js}', { eager: true });
diff --git a/packages/web-components/test/src/entry-server.ts b/packages/web-components/test/src/entry-server.ts
new file mode 100644
index 00000000000000..ce25a91654e1fb
--- /dev/null
+++ b/packages/web-components/test/src/entry-server.ts
@@ -0,0 +1,25 @@
+/**
+ * SSR server-side entry module for the Fluent UI web-components test harness.
+ *
+ * This module is loaded by the test harness server via `vite.ssrLoadModule("/src/entry-server.js")` and must export a
+ * `render` function. The server calls `render()` for each SSR fixture request, injecting the returned HTML into the
+ * `ssr.html` template.
+ *
+ * The `<--stylespreload-->`, `<--templates-->`, and `<--fixture-->` markers in `ssr.html` are replaced with the appropriate content during rendering:
+ * - `<--stylespreload-->` is replaced with a `` tag for the theme stylesheet specified in the renderer options.
+ * - `<--templates-->` is replaced with the rendered HTML for the component templates defined in the renderer options.
+ * - `<--fixture-->` is replaced with the rendered HTML for the specific SSR fixture being requested, which is
+ * determined by the request URL and any query parameters.
+ *
+ * Note that this module should only include code necessary for rendering the SSR fixture source HTML. Any client-side
+ * setup or component definitions should be placed in the separate `entry-client.ts` module, to be loaded on the client
+ * after hydration.
+ */
+
+import { createSSRRenderer } from '@microsoft/fast-test-harness/ssr/render.js';
+
+export const { render } = createSSRRenderer({
+ packageName: '@fluentui/web-components',
+ tagPrefix: 'fluent',
+ themeStylesheet: '/fluent-tokens.css',
+});
diff --git a/packages/web-components/test/ssr.html b/packages/web-components/test/ssr.html
new file mode 100644
index 00000000000000..6f8e371787f5de
--- /dev/null
+++ b/packages/web-components/test/ssr.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From eb9d85fdfd696119d0bb7a4fe7fad27eb0934801 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Fri, 22 May 2026 12:01:00 -0700
Subject: [PATCH 04/44] add DSD modules for accordion and accordion-item
---
.../accordion-item.definition-async.ts | 15 +++++++++++++++
.../src/accordion-item/define-async.ts | 5 +++++
.../src/accordion/accordion.definition-async.ts | 7 +++++++
.../web-components/src/accordion/accordion.ts | 2 +-
.../web-components/src/accordion/define-async.ts | 5 +++++
5 files changed, 33 insertions(+), 1 deletion(-)
create mode 100644 packages/web-components/src/accordion-item/accordion-item.definition-async.ts
create mode 100644 packages/web-components/src/accordion-item/define-async.ts
create mode 100644 packages/web-components/src/accordion/accordion.definition-async.ts
create mode 100644 packages/web-components/src/accordion/define-async.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..a9c63799400114
--- /dev/null
+++ b/packages/web-components/src/accordion-item/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { AccordionItem } from './accordion-item.js';
+import { definition } from './accordion-item.definition-async.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..d47e74dae59dfc
--- /dev/null
+++ b/packages/web-components/src/accordion/accordion.definition-async.ts
@@ -0,0 +1,7 @@
+import { type PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './accordion.options.js';
+
+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);
From f482514787a7c5d3cd5935685f1a2f64bb64ad4a Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Fri, 22 May 2026 16:20:22 -0700
Subject: [PATCH 05/44] add DSD modules for anchor-button
---
.../anchor-button.definition-async.ts | 15 ++++++++++++
.../src/anchor-button/anchor-button.spec.ts | 24 ++++++++++++++-----
.../src/anchor-button/define-async.ts | 5 ++++
3 files changed, 38 insertions(+), 6 deletions(-)
create mode 100644 packages/web-components/src/anchor-button/anchor-button.definition-async.ts
create mode 100644 packages/web-components/src/anchor-button/define-async.ts
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);
From a998a3c62cb15e657681d0f7244756b3c85d467c Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 21:45:58 -0700
Subject: [PATCH 06/44] add DSD modules for avatar
---
.../src/avatar/avatar.definition-async.ts | 15 +++++++++++++++
packages/web-components/src/avatar/avatar.spec.ts | 1 +
.../web-components/src/avatar/define-async.ts | 5 +++++
3 files changed, 21 insertions(+)
create mode 100644 packages/web-components/src/avatar/avatar.definition-async.ts
create mode 100644 packages/web-components/src/avatar/define-async.ts
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);
From 3272a3e0b69656b5db94046d96bdc32fe3466dd3 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 21:47:06 -0700
Subject: [PATCH 07/44] add DSD modules for badge
---
.../src/badge/badge.definition-async.ts | 15 +++++++++++++++
packages/web-components/src/badge/define-async.ts | 5 +++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/badge/badge.definition-async.ts
create mode 100644 packages/web-components/src/badge/define-async.ts
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);
From e168e2206d35c902ae729483e51de9cd2e097c88 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 21:49:57 -0700
Subject: [PATCH 08/44] add DSD modules for button
---
.../src/button/button.definition-async.ts | 15 +++++++++++++++
packages/web-components/src/button/button.spec.ts | 12 ++++++++++--
.../web-components/src/button/define-async.ts | 5 +++++
3 files changed, 30 insertions(+), 2 deletions(-)
create mode 100644 packages/web-components/src/button/button.definition-async.ts
create mode 100644 packages/web-components/src/button/define-async.ts
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);
From d29a06660ddad10ab2142600a22161536ae3caf0 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 21:55:23 -0700
Subject: [PATCH 09/44] add DSD modules for checkbox
---
.../src/checkbox/checkbox.base.ts | 17 ++++++++++-------
.../src/checkbox/checkbox.definition-async.ts | 15 +++++++++++++++
.../web-components/src/checkbox/define-async.ts | 5 +++++
3 files changed, 30 insertions(+), 7 deletions(-)
create mode 100644 packages/web-components/src/checkbox/checkbox.definition-async.ts
create mode 100644 packages/web-components/src/checkbox/define-async.ts
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);
From c1c5f62c429e4f7abb1d2ccf6fb578e600ed456d Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 21:56:26 -0700
Subject: [PATCH 10/44] add DSD modules for compound-button
---
.../compound-button.definition-async.ts | 15 +++++++++++++++
.../src/compound-button/compound-button.spec.ts | 12 ++++++++++--
.../src/compound-button/define-async.ts | 5 +++++
3 files changed, 30 insertions(+), 2 deletions(-)
create mode 100644 packages/web-components/src/compound-button/compound-button.definition-async.ts
create mode 100644 packages/web-components/src/compound-button/define-async.ts
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);
From 199bd60a875f37975c2e2d02efec7afdfc0b1709 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 21:57:31 -0700
Subject: [PATCH 11/44] add DSD modules for counter-badge
---
.../counter-badge.definition-async.ts | 15 +++++++++++++++
.../src/counter-badge/define-async.ts | 5 +++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/counter-badge/counter-badge.definition-async.ts
create mode 100644 packages/web-components/src/counter-badge/define-async.ts
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);
From 4f029f974ca61ad7f724c743ec4844dc121918d7 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 22:02:22 -0700
Subject: [PATCH 12/44] add DSD modules for dialog and dialog-body
---
.../src/dialog-body/define-async.ts | 5 +++++
.../dialog-body/dialog-body.definition-async.ts | 15 +++++++++++++++
.../web-components/src/dialog/define-async.ts | 5 +++++
.../src/dialog/dialog.definition-async.ts | 15 +++++++++++++++
4 files changed, 40 insertions(+)
create mode 100644 packages/web-components/src/dialog-body/define-async.ts
create mode 100644 packages/web-components/src/dialog-body/dialog-body.definition-async.ts
create mode 100644 packages/web-components/src/dialog/define-async.ts
create mode 100644 packages/web-components/src/dialog/dialog.definition-async.ts
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',
+};
From 746c0fce8c93586ef0ca778429eb2b67b631438f Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 22:03:13 -0700
Subject: [PATCH 13/44] add DSD modules for divider
---
.../web-components/src/divider/define-async.ts | 5 +++++
.../src/divider/divider.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/divider/define-async.ts
create mode 100644 packages/web-components/src/divider/divider.definition-async.ts
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',
+};
From 0e594c9dc0f21635ecc37d8535902e271c3db90e Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 22:13:44 -0700
Subject: [PATCH 14/44] add DSD modules for drawer and drawer-body
---
.../src/drawer-body/define-async.ts | 5 +++++
.../drawer-body/drawer-body.definition-async.ts | 15 +++++++++++++++
.../web-components/src/drawer/define-async.ts | 5 +++++
.../src/drawer/drawer.definition-async.ts | 15 +++++++++++++++
4 files changed, 40 insertions(+)
create mode 100644 packages/web-components/src/drawer-body/define-async.ts
create mode 100644 packages/web-components/src/drawer-body/drawer-body.definition-async.ts
create mode 100644 packages/web-components/src/drawer/define-async.ts
create mode 100644 packages/web-components/src/drawer/drawer.definition-async.ts
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',
+};
From 92aa7d873ae61f68211c7f8e1229cb878b0da36b Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 22:50:49 -0700
Subject: [PATCH 15/44] add DSD modules for dropdown, listbox, and option
---
.../src/dropdown/define-async.ts | 5 +++
.../src/dropdown/dropdown.base.ts | 39 ++++++++++---------
.../src/dropdown/dropdown.definition-async.ts | 15 +++++++
.../src/listbox/define-async.ts | 5 +++
.../src/listbox/listbox.definition-async.ts | 15 +++++++
.../web-components/src/option/define-async.ts | 5 +++
.../src/option/option.definition-async.ts | 15 +++++++
7 files changed, 81 insertions(+), 18 deletions(-)
create mode 100644 packages/web-components/src/dropdown/define-async.ts
create mode 100644 packages/web-components/src/dropdown/dropdown.definition-async.ts
create mode 100644 packages/web-components/src/listbox/define-async.ts
create mode 100644 packages/web-components/src/listbox/listbox.definition-async.ts
create mode 100644 packages/web-components/src/option/define-async.ts
create mode 100644 packages/web-components/src/option/option.definition-async.ts
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..029d83e608fb5f 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,12 @@ 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 +514,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/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/option/define-async.ts b/packages/web-components/src/option/define-async.ts
new file mode 100644
index 00000000000000..a8a18ce86fef03
--- /dev/null
+++ b/packages/web-components/src/option/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './option.definition-async.js';
+import { DropdownOption } from './option.js';
+
+RenderableFASTElement(DropdownOption).defineAsync(definition);
diff --git a/packages/web-components/src/option/option.definition-async.ts b/packages/web-components/src/option/option.definition-async.ts
new file mode 100644
index 00000000000000..0c6a27c210d325
--- /dev/null
+++ b/packages/web-components/src/option/option.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './option.options.js';
+
+/**
+ * The async definition configuration for the fluent-option 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',
+};
From a9f66f8362b83a6ba638cb91b51a855dbfb1ab81 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sat, 23 May 2026 22:52:38 -0700
Subject: [PATCH 16/44] add DSD modules for field
---
.../web-components/src/field/define-async.ts | 5 +++++
.../src/field/field.definition-async.ts | 18 ++++++++++++++++++
2 files changed, 23 insertions(+)
create mode 100644 packages/web-components/src/field/define-async.ts
create mode 100644 packages/web-components/src/field/field.definition-async.ts
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,
+ },
+};
From bc6ea6e49099f004c9b949b066af0b943866aa37 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 19:29:10 -0700
Subject: [PATCH 17/44] add DSD modules for image
---
packages/web-components/src/image/define-async.ts | 5 +++++
.../src/image/image.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/image/define-async.ts
create mode 100644 packages/web-components/src/image/image.definition-async.ts
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',
+};
From 2956f84070df64582279a6f2c6e6a4c899bdfe5f Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 19:34:28 -0700
Subject: [PATCH 18/44] add DSD modules for label
---
packages/web-components/src/label/define-async.ts | 5 +++++
.../src/label/label.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/label/define-async.ts
create mode 100644 packages/web-components/src/label/label.definition-async.ts
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',
+};
From 74a14d8e9d9d74d6018d0197aa04da8ff69b1f58 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 19:35:06 -0700
Subject: [PATCH 19/44] add DSD modules for link
---
packages/web-components/src/link/define-async.ts | 5 +++++
.../src/link/link.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/link/define-async.ts
create mode 100644 packages/web-components/src/link/link.definition-async.ts
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',
+};
From d51875efd539e338af6475775f3bec5ac1fc4529 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 19:48:11 -0700
Subject: [PATCH 20/44] add DSD modules for menu
---
packages/web-components/src/menu/define-async.ts | 5 +++++
.../src/menu/menu.definition-async.ts | 15 +++++++++++++++
packages/web-components/src/menu/menu.template.ts | 8 +-------
3 files changed, 21 insertions(+), 7 deletions(-)
create mode 100644 packages/web-components/src/menu/define-async.ts
create mode 100644 packages/web-components/src/menu/menu.definition-async.ts
diff --git a/packages/web-components/src/menu/define-async.ts b/packages/web-components/src/menu/define-async.ts
new file mode 100644
index 00000000000000..07a9465cea837a
--- /dev/null
+++ b/packages/web-components/src/menu/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { Menu } from './menu.js';
+import { definition } from './menu.definition-async.js';
+
+RenderableFASTElement(Menu).defineAsync(definition);
diff --git a/packages/web-components/src/menu/menu.definition-async.ts b/packages/web-components/src/menu/menu.definition-async.ts
new file mode 100644
index 00000000000000..d7c3bc1380d14f
--- /dev/null
+++ b/packages/web-components/src/menu/menu.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './menu.options.js';
+
+/**
+ * The async definition configuration for the fluent-menu 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/menu.template.ts b/packages/web-components/src/menu/menu.template.ts
index c934c1f90b50be..96608950f72791 100644
--- a/packages/web-components/src/menu/menu.template.ts
+++ b/packages/web-components/src/menu/menu.template.ts
@@ -3,13 +3,7 @@ import type { Menu } from './menu.js';
export function menuTemplate(): ElementViewTemplate {
return html`
- x.openOnHover}"
- ?open-on-context="${x => x.openOnContext}"
- ?close-on-scroll="${x => x.closeOnScroll}"
- ?persist-on-item-click="${x => x.persistOnItemClick}"
- @keydown="${(x, c) => x.menuKeydownHandler(c.event as KeyboardEvent)}"
- >
+ x.menuKeydownHandler(c.event as KeyboardEvent)}">
From 317ebdd340754500cb6d587711ea52ee6c7ca849 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 19:49:04 -0700
Subject: [PATCH 21/44] add DSD modules for menu-button
---
.../src/menu-button/define-async.ts | 5 +++++
.../menu-button/menu-button.definition-async.ts | 15 +++++++++++++++
.../src/menu-button/menu-button.definition.ts | 2 +-
.../src/menu-button/menu-button.spec.ts | 15 +++++++++++----
.../src/menu-button/menu-button.styles.ts | 8 ++++++++
5 files changed, 40 insertions(+), 5 deletions(-)
create mode 100644 packages/web-components/src/menu-button/define-async.ts
create mode 100644 packages/web-components/src/menu-button/menu-button.definition-async.ts
create mode 100644 packages/web-components/src/menu-button/menu-button.styles.ts
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;
From 0a4d80ae0eb2dff4b440aa771dff7d7b389b3e8a Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 19:49:55 -0700
Subject: [PATCH 22/44] add DSD modules for menu-item
---
.../web-components/src/menu-item/define-async.ts | 5 +++++
.../src/menu-item/menu-item.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/menu-item/define-async.ts
create mode 100644 packages/web-components/src/menu-item/menu-item.definition-async.ts
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',
+};
From 91729b4957c00b3d5355e8645bbb1db095eac5cf Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:14:54 -0700
Subject: [PATCH 23/44] add DSD modules for menu-list
---
.../web-components/src/menu-list/define-async.ts | 5 +++++
.../src/menu-list/menu-list.base.ts | 5 +++++
.../src/menu-list/menu-list.definition-async.ts | 15 +++++++++++++++
.../src/menu-list/menu-list.spec.ts | 16 ++++++++++------
.../src/menu-list/menu-list.template.ts | 2 +-
5 files changed, 36 insertions(+), 7 deletions(-)
create mode 100644 packages/web-components/src/menu-list/define-async.ts
create mode 100644 packages/web-components/src/menu-list/menu-list.definition-async.ts
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}>
- <${MenuItemTagName} checked role="menuitemradio">Menu item 2${MenuItemTagName}>
- <${MenuItemTagName} role="menuitemradio">Menu item 3${MenuItemTagName}>
- <${MenuItemTagName} role="menuitemradio">Menu item 4${MenuItemTagName}>
- ${tagName}>
+ <${MenuItemTagName} role="menuitemradio">Menu Item 1${MenuItemTagName}>
+ <${MenuItemTagName} checked role="menuitemradio">Menu item 2${MenuItemTagName}>
+ <${MenuItemTagName} role="menuitemradio">Menu item 3${MenuItemTagName}>
+ <${MenuItemTagName} role="menuitemradio">Menu item 4${MenuItemTagName}>
`,
});
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`
-
+
`;
From cb73195144093a2ae27ce75000930ea92a9a5f7e Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:17:37 -0700
Subject: [PATCH 24/44] add DSD modules for message-bar
---
.../src/message-bar/define-async.ts | 5 +++++
.../message-bar/message-bar.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/message-bar/define-async.ts
create mode 100644 packages/web-components/src/message-bar/message-bar.definition-async.ts
diff --git a/packages/web-components/src/message-bar/define-async.ts b/packages/web-components/src/message-bar/define-async.ts
new file mode 100644
index 00000000000000..1a9fd874a6bfaa
--- /dev/null
+++ b/packages/web-components/src/message-bar/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './message-bar.definition-async.js';
+import { MessageBar } from './message-bar.js';
+
+RenderableFASTElement(MessageBar).defineAsync(definition);
diff --git a/packages/web-components/src/message-bar/message-bar.definition-async.ts b/packages/web-components/src/message-bar/message-bar.definition-async.ts
new file mode 100644
index 00000000000000..21b61d437e87c6
--- /dev/null
+++ b/packages/web-components/src/message-bar/message-bar.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './message-bar.options.js';
+
+/**
+ * The async definition configuration for the fluent-message-bar 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',
+};
From 84f14e423fe6792fc23d73871946bf5ac451b6ef Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:20:24 -0700
Subject: [PATCH 25/44] add DSD modules for progress-bar
---
.../src/progress-bar/define-async.ts | 5 +++++
.../progress-bar/progress-bar.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/progress-bar/define-async.ts
create mode 100644 packages/web-components/src/progress-bar/progress-bar.definition-async.ts
diff --git a/packages/web-components/src/progress-bar/define-async.ts b/packages/web-components/src/progress-bar/define-async.ts
new file mode 100644
index 00000000000000..2141578a470fd0
--- /dev/null
+++ b/packages/web-components/src/progress-bar/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './progress-bar.definition-async.js';
+import { ProgressBar } from './progress-bar.js';
+
+RenderableFASTElement(ProgressBar).defineAsync(definition);
diff --git a/packages/web-components/src/progress-bar/progress-bar.definition-async.ts b/packages/web-components/src/progress-bar/progress-bar.definition-async.ts
new file mode 100644
index 00000000000000..7d71946ce81a55
--- /dev/null
+++ b/packages/web-components/src/progress-bar/progress-bar.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './progress-bar.options.js';
+
+/**
+ * The async definition configuration for the fluent-progress-bar 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',
+};
From ee00a68853383da2573136f3446d01b565e1759f Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:23:12 -0700
Subject: [PATCH 26/44] add DSD modules for radio
---
packages/web-components/src/radio/define-async.ts | 5 +++++
.../src/radio/radio.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/radio/define-async.ts
create mode 100644 packages/web-components/src/radio/radio.definition-async.ts
diff --git a/packages/web-components/src/radio/define-async.ts b/packages/web-components/src/radio/define-async.ts
new file mode 100644
index 00000000000000..fc45bfbe4d5f23
--- /dev/null
+++ b/packages/web-components/src/radio/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './radio.definition-async.js';
+import { Radio } from './radio.js';
+
+RenderableFASTElement(Radio).defineAsync(definition);
diff --git a/packages/web-components/src/radio/radio.definition-async.ts b/packages/web-components/src/radio/radio.definition-async.ts
new file mode 100644
index 00000000000000..e70fcfbc88574e
--- /dev/null
+++ b/packages/web-components/src/radio/radio.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './radio.options.js';
+
+/**
+ * The async definition configuration for the fluent-radio 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',
+};
From 2417a00f5a72ebd7187f01fb8827b3ac0f10d15e Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:33:08 -0700
Subject: [PATCH 27/44] add DSD modules for radio-group
---
.../src/radio-group/define-async.ts | 5 +++++
.../radio-group/radio-group.definition-async.ts | 15 +++++++++++++++
.../src/radio-group/radio-group.spec.ts | 8 +++++---
3 files changed, 25 insertions(+), 3 deletions(-)
create mode 100644 packages/web-components/src/radio-group/define-async.ts
create mode 100644 packages/web-components/src/radio-group/radio-group.definition-async.ts
diff --git a/packages/web-components/src/radio-group/define-async.ts b/packages/web-components/src/radio-group/define-async.ts
new file mode 100644
index 00000000000000..87375126606290
--- /dev/null
+++ b/packages/web-components/src/radio-group/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './radio-group.definition-async.js';
+import { RadioGroup } from './radio-group.js';
+
+RenderableFASTElement(RadioGroup).defineAsync(definition);
diff --git a/packages/web-components/src/radio-group/radio-group.definition-async.ts b/packages/web-components/src/radio-group/radio-group.definition-async.ts
new file mode 100644
index 00000000000000..3fa3396b0d5635
--- /dev/null
+++ b/packages/web-components/src/radio-group/radio-group.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './radio-group.options.js';
+
+/**
+ * The async definition configuration for the fluent-radio-group 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/radio-group/radio-group.spec.ts b/packages/web-components/src/radio-group/radio-group.spec.ts
index 6b581d079c232b..d657818d3f4084 100644
--- a/packages/web-components/src/radio-group/radio-group.spec.ts
+++ b/packages/web-components/src/radio-group/radio-group.spec.ts
@@ -236,12 +236,14 @@ test.describe('RadioGroup', () => {
const { element } = fastPage;
const radios = element.locator(RadioTagName);
- await fastPage.setTemplate(/* html */ `
- <${tagName} value="bar">
+ await fastPage.setTemplate({
+ attributes: { value: 'bar' },
+ innerHTML: /* html */ `
<${RadioTagName} id="radio-1" name="radio" value="foo">${RadioTagName}>
<${RadioTagName} id="radio-2" name="radio" value="bar">${RadioTagName}>
<${RadioTagName} id="radio-3" name="radio" value="baz">${RadioTagName}>
- `);
+ `,
+ });
await expect(radios.nth(0)).toHaveJSProperty('checked', false);
From 62faef242f80f412e931b5fa11c9694085410d6c Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:34:50 -0700
Subject: [PATCH 28/44] add DSD modules for rating-display
---
.../src/rating-display/define-async.ts | 5 +++++
.../rating-display.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/rating-display/define-async.ts
create mode 100644 packages/web-components/src/rating-display/rating-display.definition-async.ts
diff --git a/packages/web-components/src/rating-display/define-async.ts b/packages/web-components/src/rating-display/define-async.ts
new file mode 100644
index 00000000000000..905fe90d3f8603
--- /dev/null
+++ b/packages/web-components/src/rating-display/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './rating-display.definition-async.js';
+import { RatingDisplay } from './rating-display.js';
+
+RenderableFASTElement(RatingDisplay).defineAsync(definition);
diff --git a/packages/web-components/src/rating-display/rating-display.definition-async.ts b/packages/web-components/src/rating-display/rating-display.definition-async.ts
new file mode 100644
index 00000000000000..668ab908ec50fd
--- /dev/null
+++ b/packages/web-components/src/rating-display/rating-display.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './rating-display.options.js';
+
+/**
+ * The async definition configuration for the fluent-rating-display 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',
+};
From 34f19686e6486b1f90a8bed0962b7b7e0589d312 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:36:48 -0700
Subject: [PATCH 29/44] add DSD modules for slider
---
.../web-components/src/slider/define-async.ts | 5 +++++
.../src/slider/slider.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/slider/define-async.ts
create mode 100644 packages/web-components/src/slider/slider.definition-async.ts
diff --git a/packages/web-components/src/slider/define-async.ts b/packages/web-components/src/slider/define-async.ts
new file mode 100644
index 00000000000000..537437e857680a
--- /dev/null
+++ b/packages/web-components/src/slider/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './slider.definition-async.js';
+import { Slider } from './slider.js';
+
+RenderableFASTElement(Slider).defineAsync(definition);
diff --git a/packages/web-components/src/slider/slider.definition-async.ts b/packages/web-components/src/slider/slider.definition-async.ts
new file mode 100644
index 00000000000000..52984b1185f69b
--- /dev/null
+++ b/packages/web-components/src/slider/slider.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './slider.options.js';
+
+/**
+ * The async definition configuration for the fluent-slider 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',
+};
From 8fdbf8373a0967f52077b6c3c227913b67486561 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:37:34 -0700
Subject: [PATCH 30/44] add DSD modules for spinner
---
.../web-components/src/spinner/define-async.ts | 5 +++++
.../src/spinner/spinner.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/spinner/define-async.ts
create mode 100644 packages/web-components/src/spinner/spinner.definition-async.ts
diff --git a/packages/web-components/src/spinner/define-async.ts b/packages/web-components/src/spinner/define-async.ts
new file mode 100644
index 00000000000000..2a289a586963d9
--- /dev/null
+++ b/packages/web-components/src/spinner/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './spinner.definition-async.js';
+import { Spinner } from './spinner.js';
+
+RenderableFASTElement(Spinner).defineAsync(definition);
diff --git a/packages/web-components/src/spinner/spinner.definition-async.ts b/packages/web-components/src/spinner/spinner.definition-async.ts
new file mode 100644
index 00000000000000..f581a21c843488
--- /dev/null
+++ b/packages/web-components/src/spinner/spinner.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './spinner.options.js';
+
+/**
+ * The async definition configuration for the fluent-spinner 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',
+};
From f2b6feefac13942291d70b431f6e1e63fe345b50 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:39:10 -0700
Subject: [PATCH 31/44] add DSD modules for switch
---
.../web-components/src/switch/define-async.ts | 5 +++++
.../src/switch/switch.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/switch/define-async.ts
create mode 100644 packages/web-components/src/switch/switch.definition-async.ts
diff --git a/packages/web-components/src/switch/define-async.ts b/packages/web-components/src/switch/define-async.ts
new file mode 100644
index 00000000000000..3a25e9fc595c8e
--- /dev/null
+++ b/packages/web-components/src/switch/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './switch.definition-async.js';
+import { Switch } from './switch.js';
+
+RenderableFASTElement(Switch).defineAsync(definition);
diff --git a/packages/web-components/src/switch/switch.definition-async.ts b/packages/web-components/src/switch/switch.definition-async.ts
new file mode 100644
index 00000000000000..2b1c2c227c4dd7
--- /dev/null
+++ b/packages/web-components/src/switch/switch.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './switch.options.js';
+
+/**
+ * The async definition configuration for the fluent-switch 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',
+};
From 77fa80046ceaccc83786bdbff326deaec190ee88 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:40:54 -0700
Subject: [PATCH 32/44] add DSD modules for tab
---
packages/web-components/src/tab/define-async.ts | 5 +++++
.../src/tab/tab.definition-async.ts | 15 +++++++++++++++
packages/web-components/src/tab/tab.spec.ts | 2 +-
3 files changed, 21 insertions(+), 1 deletion(-)
create mode 100644 packages/web-components/src/tab/define-async.ts
create mode 100644 packages/web-components/src/tab/tab.definition-async.ts
diff --git a/packages/web-components/src/tab/define-async.ts b/packages/web-components/src/tab/define-async.ts
new file mode 100644
index 00000000000000..0dec63a5aceb07
--- /dev/null
+++ b/packages/web-components/src/tab/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './tab.definition-async.js';
+import { Tab } from './tab.js';
+
+RenderableFASTElement(Tab).defineAsync(definition);
diff --git a/packages/web-components/src/tab/tab.definition-async.ts b/packages/web-components/src/tab/tab.definition-async.ts
new file mode 100644
index 00000000000000..f013d18c43e414
--- /dev/null
+++ b/packages/web-components/src/tab/tab.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './tab.options.js';
+
+/**
+ * The async definition configuration for the fluent-tab 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/tab/tab.spec.ts b/packages/web-components/src/tab/tab.spec.ts
index 5e905f3e874651..c7d38b5b8d0595 100644
--- a/packages/web-components/src/tab/tab.spec.ts
+++ b/packages/web-components/src/tab/tab.spec.ts
@@ -28,7 +28,7 @@ test.describe('Tab', () => {
await fastPage.setTemplate();
- await expect(element).toHaveRole('tab');
+ await expect(element).toHaveJSProperty('elementInternals.role', 'tab');
});
test('should have a slot attribute of `tab`', async ({ fastPage }) => {
From f7c474e4d362b266865af28535fb901d5169488b Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:46:41 -0700
Subject: [PATCH 33/44] add DSD modules for tablist
---
.../web-components/src/tablist/define-async.ts | 5 +++++
.../src/tablist/tablist.definition-async.ts | 15 +++++++++++++++
.../web-components/src/tablist/tablist.spec.ts | 4 ++--
3 files changed, 22 insertions(+), 2 deletions(-)
create mode 100644 packages/web-components/src/tablist/define-async.ts
create mode 100644 packages/web-components/src/tablist/tablist.definition-async.ts
diff --git a/packages/web-components/src/tablist/define-async.ts b/packages/web-components/src/tablist/define-async.ts
new file mode 100644
index 00000000000000..34ed996aa2a1b9
--- /dev/null
+++ b/packages/web-components/src/tablist/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './tablist.definition-async.js';
+import { Tablist } from './tablist.js';
+
+RenderableFASTElement(Tablist).defineAsync(definition);
diff --git a/packages/web-components/src/tablist/tablist.definition-async.ts b/packages/web-components/src/tablist/tablist.definition-async.ts
new file mode 100644
index 00000000000000..eee672e6aadef1
--- /dev/null
+++ b/packages/web-components/src/tablist/tablist.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './tablist.options.js';
+
+/**
+ * The async definition configuration for the fluent-tablist 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/tablist/tablist.spec.ts b/packages/web-components/src/tablist/tablist.spec.ts
index 6878e54aa64f96..3e578c2f48c37d 100644
--- a/packages/web-components/src/tablist/tablist.spec.ts
+++ b/packages/web-components/src/tablist/tablist.spec.ts
@@ -82,7 +82,7 @@ test.describe('Tablist', () => {
await fastPage.setTemplate();
- await expect(element).toHaveAttribute('role', 'tablist');
+ await expect(element).toHaveJSProperty('elementInternals.role', 'tablist');
});
test('should set a default orientation value of `horizontal` when `orientation` is not provided', async ({
@@ -397,7 +397,7 @@ test.describe('Tablist', () => {
Panel three
`);
- const tabs = element.getByRole('tab');
+ const tabs = element.locator(TabTagName);
const firstTab = tabs.nth(0);
const secondTab = tabs.nth(1);
const firstPanel = page.getByText('Panel one');
From 0c87661092f8e9203278b9c95864c7601a583267 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 20:50:56 -0700
Subject: [PATCH 34/44] add DSD modules for text
---
packages/web-components/src/text/define-async.ts | 5 +++++
.../src/text/text.definition-async.ts | 15 +++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 packages/web-components/src/text/define-async.ts
create mode 100644 packages/web-components/src/text/text.definition-async.ts
diff --git a/packages/web-components/src/text/define-async.ts b/packages/web-components/src/text/define-async.ts
new file mode 100644
index 00000000000000..5bb04edd821e4e
--- /dev/null
+++ b/packages/web-components/src/text/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './text.definition-async.js';
+import { Text } from './text.js';
+
+RenderableFASTElement(Text).defineAsync(definition);
diff --git a/packages/web-components/src/text/text.definition-async.ts b/packages/web-components/src/text/text.definition-async.ts
new file mode 100644
index 00000000000000..850ba1315f29fb
--- /dev/null
+++ b/packages/web-components/src/text/text.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './text.options.js';
+
+/**
+ * The async definition configuration for the fluent-text 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',
+};
From 57c4a16b5aba70d7c9c510d1c9d7254ade140e86 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 22:01:24 -0700
Subject: [PATCH 35/44] add DSD modules for text-input
---
.../src/text-input/define-async.ts | 5 +++
.../src/text-input/text-input.base.ts | 6 +--
.../text-input/text-input.definition-async.ts | 18 +++++++++
.../src/text-input/text-input.spec.ts | 40 +++++++++++++++++--
.../src/text-input/text-input.template.ts | 2 +-
5 files changed, 63 insertions(+), 8 deletions(-)
create mode 100644 packages/web-components/src/text-input/define-async.ts
create mode 100644 packages/web-components/src/text-input/text-input.definition-async.ts
diff --git a/packages/web-components/src/text-input/define-async.ts b/packages/web-components/src/text-input/define-async.ts
new file mode 100644
index 00000000000000..05fc2acae51195
--- /dev/null
+++ b/packages/web-components/src/text-input/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './text-input.definition-async.js';
+import { TextInput } from './text-input.js';
+
+RenderableFASTElement(TextInput).defineAsync(definition);
diff --git a/packages/web-components/src/text-input/text-input.base.ts b/packages/web-components/src/text-input/text-input.base.ts
index 1326aca0153cdb..dfcb652490ca98 100644
--- a/packages/web-components/src/text-input/text-input.base.ts
+++ b/packages/web-components/src/text-input/text-input.base.ts
@@ -388,7 +388,7 @@ export class BaseTextInput extends FASTElement {
public set value(value: string) {
this.currentValue = value;
- if (this.$fastController.isConnected) {
+ if (this.elementInternals && this.control) {
this.control.value = value ?? '';
this.setFormValue(value);
this.setValidity();
@@ -596,7 +596,7 @@ export class BaseTextInput 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);
}
/**
@@ -609,7 +609,7 @@ export class BaseTextInput extends FASTElement {
* @internal
*/
public setValidity(flags?: Partial, message?: string, anchor?: HTMLElement): void {
- if (this.$fastController.isConnected && this.control) {
+ if (this.elementInternals && this.control) {
if (this.disabled) {
this.elementInternals.setValidity({});
return;
diff --git a/packages/web-components/src/text-input/text-input.definition-async.ts b/packages/web-components/src/text-input/text-input.definition-async.ts
new file mode 100644
index 00000000000000..f95e17eacc7b37
--- /dev/null
+++ b/packages/web-components/src/text-input/text-input.definition-async.ts
@@ -0,0 +1,18 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './text-input.options.js';
+
+/**
+ * The async definition configuration for the fluent-text-input 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/text-input/text-input.spec.ts b/packages/web-components/src/text-input/text-input.spec.ts
index ff957f0ecb5181..fe322d3b1e7bcb 100644
--- a/packages/web-components/src/text-input/text-input.spec.ts
+++ b/packages/web-components/src/text-input/text-input.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 type { TextInput } from './text-input.js';
import { ImplicitSubmissionBlockingTypes, tagName } from './text-input.options.js';
@@ -23,10 +24,17 @@ test.describe('TextInput', () => {
expect(hasError).toBe(false);
});
- 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();
});
@@ -334,10 +342,10 @@ test.describe('TextInput', () => {
const label = element.locator('label');
await fastPage.setTemplate({
- innerHTML: '\n \n',
+ innerHTML: ' ',
});
- await expect(element).toHaveText(/\n\s\n/);
+ await expect(element).toHaveText('\u00A0');
await expect(label).toBeHidden();
});
@@ -802,6 +810,30 @@ test.describe('TextInput', () => {
await expect(control).toHaveValue('');
});
+ test('should reset the value to the initial value when the form is reset and the `value` attribute was set pre-connection', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+ const control = element.locator('input');
+ const reset = page.locator('button');
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await expect(control).toHaveValue('initial');
+
+ await control.fill('hello');
+
+ await reset.click();
+
+ await expect(control).toHaveValue('initial');
+ });
+
test('should change the `value` property when the `current-value` attribute changes', async ({ fastPage, page }) => {
const { element } = fastPage;
diff --git a/packages/web-components/src/text-input/text-input.template.ts b/packages/web-components/src/text-input/text-input.template.ts
index d10a5385f09a7c..5a2e701d248930 100644
--- a/packages/web-components/src/text-input/text-input.template.ts
+++ b/packages/web-components/src/text-input/text-input.template.ts
@@ -40,7 +40,7 @@ export function textInputTemplate(options: TextInputOptions
size="${x => x.size}"
spellcheck="${x => x.spellcheck}"
type="${x => x.type}"
- value="${x => x.initialValue}"
+ value="${x => x.value}"
${ref('control')}
/>
${endSlotTemplate(options)}
From 7e5457d2c2539ceadc5b80f23ca20413af24ba96 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 23:00:28 -0700
Subject: [PATCH 36/44] add DSD modules for textarea
---
.../src/textarea/define-async.ts | 5 ++
.../src/textarea/textarea.base.ts | 24 ++++----
.../src/textarea/textarea.definition-async.ts | 18 ++++++
.../src/textarea/textarea.spec.ts | 55 ++++++++-----------
4 files changed, 60 insertions(+), 42 deletions(-)
create mode 100644 packages/web-components/src/textarea/define-async.ts
create mode 100644 packages/web-components/src/textarea/textarea.definition-async.ts
diff --git a/packages/web-components/src/textarea/define-async.ts b/packages/web-components/src/textarea/define-async.ts
new file mode 100644
index 00000000000000..685d41ffc52dd0
--- /dev/null
+++ b/packages/web-components/src/textarea/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './textarea.definition-async.js';
+import { TextArea } from './textarea.js';
+
+RenderableFASTElement(TextArea).defineAsync(definition);
diff --git a/packages/web-components/src/textarea/textarea.base.ts b/packages/web-components/src/textarea/textarea.base.ts
index d035c87a18cccd..38038ab64f6da2 100644
--- a/packages/web-components/src/textarea/textarea.base.ts
+++ b/packages/web-components/src/textarea/textarea.base.ts
@@ -1,4 +1,4 @@
-import { attr, FASTElement, nullableNumberConverter, observable } from '@microsoft/fast-element';
+import { attr, FASTElement, nullableNumberConverter, observable, Updates } from '@microsoft/fast-element';
import { whitespaceFilter } from '../utils/whitespace-filter.js';
import type { Label } from '../label/label.js';
import { hasMatchingState, swapStates, toggleState } from '../utils/element-internals.js';
@@ -84,9 +84,11 @@ export class BaseTextArea extends FASTElement {
@observable
public defaultSlottedNodes!: Node[];
protected defaultSlottedNodesChanged() {
- const next = this.getContent();
- this.defaultValue = next;
- this.value = next;
+ Updates.enqueue(() => {
+ const next = this.getContent();
+ this.defaultValue = next;
+ this.value = next;
+ });
}
private filteredLabelSlottedNodes: Label[] = [];
@@ -263,9 +265,9 @@ export class BaseTextArea extends FASTElement {
@attr({ attribute: 'readonly', mode: 'boolean' })
public readOnly = false;
protected readOnlyChanged() {
- this.elementInternals.ariaReadOnly = `${!!this.readOnly}`;
+ if (this.elementInternals) {
+ this.elementInternals.ariaReadOnly = `${!!this.readOnly}`;
- if (this.$fastController.isConnected) {
this.setValidity();
}
}
@@ -509,7 +511,7 @@ export class BaseTextArea extends FASTElement {
* @public
*/
public setCustomValidity(message: string | null): void {
- this.elementInternals.setValidity({ customError: !!message }, !!message ? message.toString() : undefined);
+ this.elementInternals?.setValidity({ customError: !!message }, !!message ? message.toString() : undefined);
this.reportValidity();
}
@@ -523,7 +525,7 @@ export class BaseTextArea extends FASTElement {
* @internal
*/
public setValidity(flags?: Partial, message?: string, anchor?: HTMLElement): void {
- if (!this.$fastController.isConnected) {
+ if (!this.elementInternals) {
return;
}
@@ -531,8 +533,8 @@ export class BaseTextArea extends FASTElement {
this.elementInternals.setValidity({});
} else {
this.elementInternals.setValidity(
- flags ?? this.controlEl.validity,
- message ?? this.controlEl.validationMessage,
+ flags ?? this.controlEl?.validity,
+ message ?? this.controlEl?.validationMessage,
anchor ?? this.controlEl,
);
}
@@ -557,7 +559,7 @@ export class BaseTextArea extends FASTElement {
private getContent(): string {
return (
this.defaultSlottedNodes
- .map(node => {
+ ?.map(node => {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
return (node as Element).outerHTML;
diff --git a/packages/web-components/src/textarea/textarea.definition-async.ts b/packages/web-components/src/textarea/textarea.definition-async.ts
new file mode 100644
index 00000000000000..48d817bdff6c65
--- /dev/null
+++ b/packages/web-components/src/textarea/textarea.definition-async.ts
@@ -0,0 +1,18 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './textarea.options.js';
+
+/**
+ * The async definition configuration for the fluent-textarea 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/textarea/textarea.spec.ts b/packages/web-components/src/textarea/textarea.spec.ts
index 5d9bff314906f4..1b6dcec013ea37 100644
--- a/packages/web-components/src/textarea/textarea.spec.ts
+++ b/packages/web-components/src/textarea/textarea.spec.ts
@@ -151,38 +151,31 @@ test.describe('TextArea', () => {
}) => {
const { element } = fastPage;
- // Mock CSS.supports to simulate a browser that doesn't support `field-sizing: content` (e.g. Firefox)
- // and restore it after to avoid affecting subsequent tests
- await page.evaluate(() => {
- const originalSupports = CSS.supports.bind(CSS);
- (window as Window & { __originalCSSSupports?: typeof CSS.supports }).__originalCSSSupports = originalSupports;
- CSS.supports = (property: string, value?: string) => {
- if (property === 'field-sizing: content' || (property === 'field-sizing' && value === 'content')) {
- return false;
- }
- return originalSupports(property, value as string);
- };
- });
-
- try {
- await fastPage.setTemplate({ attributes: { 'auto-resize': true } });
-
- const autoSizerIsInsideRoot = await element.evaluate((node: TextArea) => {
- const root = node.shadowRoot?.querySelector('.root');
- return root?.contains(node.autoSizerEl ?? null) ?? false;
- });
+ await page.addInitScript({
+ content: `
+ const originalSupports = CSS.supports.bind(CSS);
+ window.__originalCSSSupports = originalSupports;
+ CSS.supports = (property, value) => {
+ if (property === 'field-sizing: content' || (property === 'field-sizing' && value === 'content')) {
+ console.log('Mocking CSS.supports to return false for', property);
+ return false;
+ }
+ return originalSupports(property, value);
+ };
+ `,
+ });
- expect(autoSizerIsInsideRoot).toBe(true);
- } finally {
- // Restore the original CSS.supports to avoid affecting other tests
- await page.evaluate(() => {
- const w = window as Window & { __originalCSSSupports?: typeof CSS.supports };
- if (w.__originalCSSSupports) {
- CSS.supports = w.__originalCSSSupports;
- delete w.__originalCSSSupports;
- }
- });
- }
+ // Ensures the next navigation creates a new context where the patched CSS.supports is in effect.
+ await fastPage.goto();
+
+ await fastPage.setTemplate({ attributes: { 'auto-resize': true } });
+
+ const autoSizerIsInsideRoot = await element.evaluate((node: TextArea) => {
+ const root = node.shadowRoot?.querySelector('.root');
+ return root?.contains(node.autoSizerEl ?? null) ?? false;
+ });
+
+ expect(autoSizerIsInsideRoot).toBe(true);
});
test('should toggle block attribute', async ({ fastPage }) => {
From 0771f9cbc6b4fa233e5eeb7cfa461632765ee20e Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 23:00:47 -0700
Subject: [PATCH 37/44] skip setTheme tests in SSR
---
packages/web-components/src/theme/set-theme.spec.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/packages/web-components/src/theme/set-theme.spec.ts b/packages/web-components/src/theme/set-theme.spec.ts
index 883943b72d0ec6..e2460b3413607b 100644
--- a/packages/web-components/src/theme/set-theme.spec.ts
+++ b/packages/web-components/src/theme/set-theme.spec.ts
@@ -18,6 +18,11 @@ declare global {
}
}
+test.skip(
+ ({ ssr }) => ssr === true,
+ 'setTheme tests are the same in SSR and CSR, so they are only run in CSR to avoid unnecessary test runs.',
+);
+
test.describe('setTheme()', () => {
test('should set and uset global tokens', async ({ fastPage, page }) => {
const html = page.locator('html');
From 7f862e90a9759369aaa7efb7e76989e25054ccee Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 23:03:18 -0700
Subject: [PATCH 38/44] add DSD modules for toggle-button
---
.../src/toggle-button/define-async.ts | 5 +++++
.../toggle-button.definition-async.ts | 15 +++++++++++++++
.../src/toggle-button/toggle-button.ts | 5 ++---
3 files changed, 22 insertions(+), 3 deletions(-)
create mode 100644 packages/web-components/src/toggle-button/define-async.ts
create mode 100644 packages/web-components/src/toggle-button/toggle-button.definition-async.ts
diff --git a/packages/web-components/src/toggle-button/define-async.ts b/packages/web-components/src/toggle-button/define-async.ts
new file mode 100644
index 00000000000000..baf3faafaaf5ac
--- /dev/null
+++ b/packages/web-components/src/toggle-button/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './toggle-button.definition-async.js';
+import { ToggleButton } from './toggle-button.js';
+
+RenderableFASTElement(ToggleButton).defineAsync(definition);
diff --git a/packages/web-components/src/toggle-button/toggle-button.definition-async.ts b/packages/web-components/src/toggle-button/toggle-button.definition-async.ts
new file mode 100644
index 00000000000000..93d76563d86f67
--- /dev/null
+++ b/packages/web-components/src/toggle-button/toggle-button.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './toggle-button.options.js';
+
+/**
+ * The async definition configuration for the fluent-toggle-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/toggle-button/toggle-button.ts b/packages/web-components/src/toggle-button/toggle-button.ts
index 28d0bd2becfb42..67cf5abf5c3600 100644
--- a/packages/web-components/src/toggle-button/toggle-button.ts
+++ b/packages/web-components/src/toggle-button/toggle-button.ts
@@ -70,9 +70,8 @@ export class ToggleButton extends Button {
* @internal
*/
private setPressedState(): void {
- if (this.$fastController.isConnected) {
- const ariaPressed = `${this.mixed ? 'mixed' : !!this.pressed}`;
- this.elementInternals.ariaPressed = ariaPressed;
+ if (this.elementInternals) {
+ this.elementInternals.ariaPressed = `${this.mixed ? 'mixed' : !!this.pressed}`;
toggleState(this.elementInternals, 'pressed', !!this.pressed || !!this.mixed);
}
}
From 1c85fe35fa5525cd2244ef4996205478fa868c0a Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Sun, 24 May 2026 23:10:12 -0700
Subject: [PATCH 39/44] add DSD modules for tooltip
---
.../web-components/src/tooltip/define-async.ts | 5 +++++
.../src/tooltip/tooltip.definition-async.ts | 15 +++++++++++++++
packages/web-components/src/tooltip/tooltip.ts | 2 ++
3 files changed, 22 insertions(+)
create mode 100644 packages/web-components/src/tooltip/define-async.ts
create mode 100644 packages/web-components/src/tooltip/tooltip.definition-async.ts
diff --git a/packages/web-components/src/tooltip/define-async.ts b/packages/web-components/src/tooltip/define-async.ts
new file mode 100644
index 00000000000000..abf328763a9797
--- /dev/null
+++ b/packages/web-components/src/tooltip/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './tooltip.definition-async.js';
+import { Tooltip } from './tooltip.js';
+
+RenderableFASTElement(Tooltip).defineAsync(definition);
diff --git a/packages/web-components/src/tooltip/tooltip.definition-async.ts b/packages/web-components/src/tooltip/tooltip.definition-async.ts
new file mode 100644
index 00000000000000..a1fbd6039ebd47
--- /dev/null
+++ b/packages/web-components/src/tooltip/tooltip.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './tooltip.options.js';
+
+/**
+ * The async definition configuration for the fluent-tooltip 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/tooltip/tooltip.ts b/packages/web-components/src/tooltip/tooltip.ts
index 6821483d3d1957..94f6b237e86fae 100644
--- a/packages/web-components/src/tooltip/tooltip.ts
+++ b/packages/web-components/src/tooltip/tooltip.ts
@@ -85,6 +85,8 @@ export class Tooltip extends FASTElement {
public connectedCallback(): void {
super.connectedCallback();
+ this.popover ??= 'auto';
+
// If the anchor element is not found, tooltip will not be shown
if (!this.anchorElement) {
return;
From adf3a1024056ddeb92fa3104cda0596e39fa8982 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Mon, 25 May 2026 18:11:32 -0700
Subject: [PATCH 40/44] update tooltip anchor positioning polyfill handling
---
.../web-components/src/tooltip/tooltip.ts | 42 ++++++++++++-------
1 file changed, 28 insertions(+), 14 deletions(-)
diff --git a/packages/web-components/src/tooltip/tooltip.ts b/packages/web-components/src/tooltip/tooltip.ts
index 94f6b237e86fae..d9d10b6d2410fd 100644
--- a/packages/web-components/src/tooltip/tooltip.ts
+++ b/packages/web-components/src/tooltip/tooltip.ts
@@ -51,9 +51,7 @@ export class Tooltip extends FASTElement {
* @internal
*/
public positioningChanged(): void {
- if (!AnchorPositioningCSSSupported) {
- this.setFallbackStyles();
- }
+ this.setFallbackStyles();
}
/**
@@ -144,6 +142,13 @@ export class Tooltip extends FASTElement {
* @internal
*/
public showTooltip(delay: number = this.defaultDelay): void {
+ if (!this.anchorPositioningReady) {
+ this.setFallbackStyles().then(() => {
+ this.showTooltip(delay);
+ });
+ return;
+ }
+
setTimeout(() => {
this.setAttribute('aria-hidden', 'false');
this.showPopover();
@@ -185,7 +190,22 @@ export class Tooltip extends FASTElement {
*/
public blurAnchorHandler = () => this.hideTooltip(0);
- private setFallbackStyles(): void {
+ /**
+ * Indicates whether the tooltip styles have been applied for browsers that do not support anchor positioning.
+ * @internal
+ */
+ private anchorPositioningReady: boolean = false;
+
+ /**
+ * Sets fallback styles for the tooltip for browsers that do not support CSS anchor positioning.
+ * @internal
+ */
+ private async setFallbackStyles(): Promise {
+ if (AnchorPositioningCSSSupported) {
+ this.anchorPositioningReady = true;
+ return;
+ }
+
if (!this.anchorElement) {
return;
}
@@ -242,16 +262,10 @@ export class Tooltip extends FASTElement {
}
`;
- if (window.CSS_ANCHOR_POLYFILL) {
- window.CSS_ANCHOR_POLYFILL.call({ element: this.anchorPositioningStyleElement });
- }
- }
-}
+ if ((window as any).CSS_ANCHOR_POLYFILL) {
+ await (window as any).CSS_ANCHOR_POLYFILL({ elements: [this.anchorPositioningStyleElement!] });
-declare global {
- interface Window {
- CSS_ANCHOR_POLYFILL?: {
- call: (options: { element: HTMLStyleElement }) => void;
- };
+ this.anchorPositioningReady = true;
+ }
}
}
From 898d6ce2b2588da025ea1fa7472aef012e57e7e7 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Mon, 25 May 2026 13:14:29 -0700
Subject: [PATCH 41/44] add DSD modules for tree and tree-item
---
.../src/tree-item/define-async.ts | 5 +++++
.../src/tree-item/tree-item.base.ts | 21 +++++++++++++++----
.../tree-item/tree-item.definition-async.ts | 15 +++++++++++++
.../src/tree-item/tree-item.template.ts | 2 +-
.../web-components/src/tree/define-async.ts | 5 +++++
.../src/tree/tree.definition-async.ts | 15 +++++++++++++
.../web-components/src/tree/tree.template.ts | 1 +
7 files changed, 59 insertions(+), 5 deletions(-)
create mode 100644 packages/web-components/src/tree-item/define-async.ts
create mode 100644 packages/web-components/src/tree-item/tree-item.definition-async.ts
create mode 100644 packages/web-components/src/tree/define-async.ts
create mode 100644 packages/web-components/src/tree/tree.definition-async.ts
diff --git a/packages/web-components/src/tree-item/define-async.ts b/packages/web-components/src/tree-item/define-async.ts
new file mode 100644
index 00000000000000..5f601712e51a43
--- /dev/null
+++ b/packages/web-components/src/tree-item/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './tree-item.definition-async.js';
+import { TreeItem } from './tree-item.js';
+
+RenderableFASTElement(TreeItem).defineAsync(definition);
diff --git a/packages/web-components/src/tree-item/tree-item.base.ts b/packages/web-components/src/tree-item/tree-item.base.ts
index 3137b81a88504c..23ef1dc5626a3e 100644
--- a/packages/web-components/src/tree-item/tree-item.base.ts
+++ b/packages/web-components/src/tree-item/tree-item.base.ts
@@ -1,4 +1,4 @@
-import { attr, css, type ElementStyles, FASTElement, observable } from '@microsoft/fast-element';
+import { attr, css, type ElementStyles, FASTElement, observable, Updates } from '@microsoft/fast-element';
import { toggleState } from '../utils/element-internals.js';
import { isTreeItem } from './tree-item.options.js';
@@ -29,6 +29,16 @@ export class BaseTreeItem extends FASTElement {
this.elementInternals.role = 'treeitem';
}
+ connectedCallback(): void {
+ super.connectedCallback();
+
+ this.tabIndex = Number(this.getAttribute('tabindex') || '0');
+
+ if (isTreeItem(this.parentElement)) {
+ this.slot ||= 'item';
+ }
+ }
+
/**
* When true, the control will be appear expanded by user interaction.
* When true, the control will be appear expanded by user interaction.
@@ -53,7 +63,7 @@ export class BaseTreeItem extends FASTElement {
newState: next ? 'open' : 'closed',
});
toggleState(this.elementInternals, 'expanded', next);
- if (this.childTreeItems && this.childTreeItems.length > 0) {
+ if (this.childTreeItems?.length) {
this.elementInternals.ariaExpanded = next ? 'true' : 'false';
// Update focusgroup attributes after subtree show/hide rendering is done.
requestAnimationFrame(() => {
@@ -93,8 +103,11 @@ export class BaseTreeItem extends FASTElement {
*/
protected selectedChanged(prev: boolean, next: boolean): void {
this.$emit('change');
- toggleState(this.elementInternals, 'selected', next);
- this.elementInternals.ariaSelected = next ? 'true' : 'false';
+
+ if (this.elementInternals) {
+ toggleState(this.elementInternals, 'selected', next);
+ this.elementInternals.ariaSelected = next ? 'true' : 'false';
+ }
}
/**
diff --git a/packages/web-components/src/tree-item/tree-item.definition-async.ts b/packages/web-components/src/tree-item/tree-item.definition-async.ts
new file mode 100644
index 00000000000000..569001ba831d9b
--- /dev/null
+++ b/packages/web-components/src/tree-item/tree-item.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './tree-item.options.js';
+
+/**
+ * The async definition configuration for the fluent-tree-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/tree-item/tree-item.template.ts b/packages/web-components/src/tree-item/tree-item.template.ts
index de07b139fcd298..b555a2ab3b2225 100644
--- a/packages/web-components/src/tree-item/tree-item.template.ts
+++ b/packages/web-components/src/tree-item/tree-item.template.ts
@@ -10,7 +10,7 @@ const chevronIcon = html`
`;
export const template = html`
- x.selected}">
+ x.selected}">
diff --git a/packages/web-components/src/tree/define-async.ts b/packages/web-components/src/tree/define-async.ts
new file mode 100644
index 00000000000000..8bf7969caffc13
--- /dev/null
+++ b/packages/web-components/src/tree/define-async.ts
@@ -0,0 +1,5 @@
+import { RenderableFASTElement } from '@microsoft/fast-html';
+import { definition } from './tree.definition-async.js';
+import { Tree } from './tree.js';
+
+RenderableFASTElement(Tree).defineAsync(definition);
diff --git a/packages/web-components/src/tree/tree.definition-async.ts b/packages/web-components/src/tree/tree.definition-async.ts
new file mode 100644
index 00000000000000..bc87dbbb5432c3
--- /dev/null
+++ b/packages/web-components/src/tree/tree.definition-async.ts
@@ -0,0 +1,15 @@
+import type { PartialFASTElementDefinition } from '@microsoft/fast-element';
+import { tagName } from './tree.options.js';
+
+/**
+ * The async definition configuration for the fluent-tree 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/tree/tree.template.ts b/packages/web-components/src/tree/tree.template.ts
index 05b8e8073e1473..84d034b2146e84 100644
--- a/packages/web-components/src/tree/tree.template.ts
+++ b/packages/web-components/src/tree/tree.template.ts
@@ -3,6 +3,7 @@ import type { Tree } from './tree.js';
export const template = html`
x.clickHandler(c.event)}"
@keydown="${(x, c) => x.keydownHandler(c.event as KeyboardEvent)}"
From 7a8fb3a041db25344e9781469708aadadc5e048c Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Mon, 25 May 2026 18:13:21 -0700
Subject: [PATCH 42/44] add changefile
---
...eb-components-ff6dd466-4b60-4451-ab21-3929917fa50a.json | 7 +++++++
1 file changed, 7 insertions(+)
create mode 100644 change/@fluentui-web-components-ff6dd466-4b60-4451-ab21-3929917fa50a.json
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"
+}
From d9b43de2887d5c631b2c49f19629270a4ffe8d8f Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Tue, 26 May 2026 08:38:15 -0700
Subject: [PATCH 43/44] address PR feedback
---
.../src/accordion-item/define-async.ts | 2 +-
.../accordion/accordion.definition-async.ts | 8 +++++++
.../src/dropdown/dropdown.base.ts | 4 +---
.../web-components/src/menu/define-async.ts | 2 +-
.../src/textarea/textarea.spec.ts | 21 ++++++++-----------
.../src/tree-item/tree-item.base.ts | 2 +-
6 files changed, 21 insertions(+), 18 deletions(-)
diff --git a/packages/web-components/src/accordion-item/define-async.ts b/packages/web-components/src/accordion-item/define-async.ts
index a9c63799400114..09d74534a8dd1a 100644
--- a/packages/web-components/src/accordion-item/define-async.ts
+++ b/packages/web-components/src/accordion-item/define-async.ts
@@ -1,5 +1,5 @@
import { RenderableFASTElement } from '@microsoft/fast-html';
-import { AccordionItem } from './accordion-item.js';
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
index d47e74dae59dfc..80eb838510f606 100644
--- a/packages/web-components/src/accordion/accordion.definition-async.ts
+++ b/packages/web-components/src/accordion/accordion.definition-async.ts
@@ -1,6 +1,14 @@
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',
diff --git a/packages/web-components/src/dropdown/dropdown.base.ts b/packages/web-components/src/dropdown/dropdown.base.ts
index 029d83e608fb5f..0521679615e389 100644
--- a/packages/web-components/src/dropdown/dropdown.base.ts
+++ b/packages/web-components/src/dropdown/dropdown.base.ts
@@ -446,9 +446,7 @@ export class BaseDropdown extends FASTElement {
public get enabledOptions(): DropdownOption[] {
return (
this.listbox?.enabledOptions ??
- Array.from(this.querySelectorAll('*')).filter(
- (o): o is DropdownOption => isDropdownOption(o) && !o.disabled,
- )
+ Array.from(this.querySelectorAll('*')).filter((o): o is DropdownOption => isDropdownOption(o) && !o.disabled)
);
}
diff --git a/packages/web-components/src/menu/define-async.ts b/packages/web-components/src/menu/define-async.ts
index 07a9465cea837a..33dcd84c56e45d 100644
--- a/packages/web-components/src/menu/define-async.ts
+++ b/packages/web-components/src/menu/define-async.ts
@@ -1,5 +1,5 @@
import { RenderableFASTElement } from '@microsoft/fast-html';
-import { Menu } from './menu.js';
import { definition } from './menu.definition-async.js';
+import { Menu } from './menu.js';
RenderableFASTElement(Menu).defineAsync(definition);
diff --git a/packages/web-components/src/textarea/textarea.spec.ts b/packages/web-components/src/textarea/textarea.spec.ts
index 1b6dcec013ea37..30e41542c091bb 100644
--- a/packages/web-components/src/textarea/textarea.spec.ts
+++ b/packages/web-components/src/textarea/textarea.spec.ts
@@ -151,18 +151,15 @@ test.describe('TextArea', () => {
}) => {
const { element } = fastPage;
- await page.addInitScript({
- content: `
- const originalSupports = CSS.supports.bind(CSS);
- window.__originalCSSSupports = originalSupports;
- CSS.supports = (property, value) => {
- if (property === 'field-sizing: content' || (property === 'field-sizing' && value === 'content')) {
- console.log('Mocking CSS.supports to return false for', property);
- return false;
- }
- return originalSupports(property, value);
- };
- `,
+ await page.addInitScript(() => {
+ const originalSupports = CSS.supports.bind(CSS);
+ (window as any).__originalCSSSupports = originalSupports;
+ (CSS as any).supports = (property: string, value: string) => {
+ if (property === 'field-sizing: content' || (property === 'field-sizing' && value === 'content')) {
+ return false;
+ }
+ return originalSupports(property, value);
+ };
});
// Ensures the next navigation creates a new context where the patched CSS.supports is in effect.
diff --git a/packages/web-components/src/tree-item/tree-item.base.ts b/packages/web-components/src/tree-item/tree-item.base.ts
index 23ef1dc5626a3e..55562bc8778814 100644
--- a/packages/web-components/src/tree-item/tree-item.base.ts
+++ b/packages/web-components/src/tree-item/tree-item.base.ts
@@ -1,4 +1,4 @@
-import { attr, css, type ElementStyles, FASTElement, observable, Updates } from '@microsoft/fast-element';
+import { attr, css, type ElementStyles, FASTElement, observable } from '@microsoft/fast-element';
import { toggleState } from '../utils/element-internals.js';
import { isTreeItem } from './tree-item.options.js';
From c80688c50e42e793d98df40b86474eb75b342543 Mon Sep 17 00:00:00 2001
From: John Kreitlow <863023+radium-v@users.noreply.github.com>
Date: Tue, 26 May 2026 11:06:12 -0700
Subject: [PATCH 44/44] add SSR support to Playwright configuration and
streamline e2e test execution
---
packages/web-components/playwright.config.ts | 34 +++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
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',
});