diff --git a/apps/vr-tests-web-components/package.json b/apps/vr-tests-web-components/package.json
index 99a3830b50f54f..026b60422ff0b8 100644
--- a/apps/vr-tests-web-components/package.json
+++ b/apps/vr-tests-web-components/package.json
@@ -18,7 +18,7 @@
"html-react-parser": "4.0.0",
"@fluentui/tokens": ">=1.0.0-alpha",
"@fluentui/web-components": ">=3.0.0-alpha",
- "@microsoft/fast-element": "2.0.0",
+ "@microsoft/fast-element": "^2.0.0",
"tslib": "^2.1.0"
}
}
diff --git a/change/@fluentui-chart-web-components-efc149c5-3d43-4300-a413-b3d9048e2248.json b/change/@fluentui-chart-web-components-efc149c5-3d43-4300-a413-b3d9048e2248.json
new file mode 100644
index 00000000000000..e7def50970997e
--- /dev/null
+++ b/change/@fluentui-chart-web-components-efc149c5-3d43-4300-a413-b3d9048e2248.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "fix: update fast-element dependency version",
+ "packageName": "@fluentui/chart-web-components",
+ "email": "863023+radium-v@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@fluentui-react-headless-components-preview-812588d9-6ce9-49cb-b704-046cb6f998ff.json b/change/@fluentui-react-headless-components-preview-812588d9-6ce9-49cb-b704-046cb6f998ff.json
new file mode 100644
index 00000000000000..1c60969d9ee800
--- /dev/null
+++ b/change/@fluentui-react-headless-components-preview-812588d9-6ce9-49cb-b704-046cb6f998ff.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "fix(headless): fix Popover flipping behavior",
+ "packageName": "@fluentui/react-headless-components-preview",
+ "email": "vgenaev@gmail.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@fluentui-web-components-38caae0f-3c50-46f0-b0c2-bb87de96d641.json b/change/@fluentui-web-components-38caae0f-3c50-46f0-b0c2-bb87de96d641.json
new file mode 100644
index 00000000000000..9e94b949c3f1ed
--- /dev/null
+++ b/change/@fluentui-web-components-38caae0f-3c50-46f0-b0c2-bb87de96d641.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "fix: remove hoisted peer dependency entries",
+ "packageName": "@fluentui/web-components",
+ "email": "863023+radium-v@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@fluentui-web-components-669e37a5-2c7c-4b19-95d4-8b070c6d72f7.json b/change/@fluentui-web-components-669e37a5-2c7c-4b19-95d4-8b070c6d72f7.json
new file mode 100644
index 00000000000000..f103de031e7866
--- /dev/null
+++ b/change/@fluentui-web-components-669e37a5-2c7c-4b19-95d4-8b070c6d72f7.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "fix: enhance accessibility attributes for drawer component",
+ "packageName": "@fluentui/web-components",
+ "email": "863023+radium-v@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@fluentui-web-components-80c47489-fa3b-4929-8f16-f8ff8d79b89f.json b/change/@fluentui-web-components-80c47489-fa3b-4929-8f16-f8ff8d79b89f.json
new file mode 100644
index 00000000000000..b6ae232147899a
--- /dev/null
+++ b/change/@fluentui-web-components-80c47489-fa3b-4929-8f16-f8ff8d79b89f.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "fix keyboard navigation regressions for tree and menu-list",
+ "packageName": "@fluentui/web-components",
+ "email": "machi@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@fluentui-web-components-962383bb-b75c-46a4-bc98-038f6dd7f558.json b/change/@fluentui-web-components-962383bb-b75c-46a4-bc98-038f6dd7f558.json
new file mode 100644
index 00000000000000..fd3625aedc557d
--- /dev/null
+++ b/change/@fluentui-web-components-962383bb-b75c-46a4-bc98-038f6dd7f558.json
@@ -0,0 +1,7 @@
+{
+ "type": "none",
+ "comment": "test: add tests for compound-button, dialog-body, drawer-body, menu-button, and menu-item components",
+ "packageName": "@fluentui/web-components",
+ "email": "863023+radium-v@users.noreply.github.com",
+ "dependentChangeType": "none"
+}
diff --git a/change/@fluentui-web-components-bc13b7de-bb06-4180-a517-c64ffe612490.json b/change/@fluentui-web-components-bc13b7de-bb06-4180-a517-c64ffe612490.json
new file mode 100644
index 00000000000000..059d9e20d1efd2
--- /dev/null
+++ b/change/@fluentui-web-components-bc13b7de-bb06-4180-a517-c64ffe612490.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "fix: synchronize compound-button template with button template",
+ "packageName": "@fluentui/web-components",
+ "email": "863023+radium-v@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@fluentui-web-components-c28cec00-7029-4430-8cf5-17143006a866.json b/change/@fluentui-web-components-c28cec00-7029-4430-8cf5-17143006a866.json
new file mode 100644
index 00000000000000..cd2eda4e6017d8
--- /dev/null
+++ b/change/@fluentui-web-components-c28cec00-7029-4430-8cf5-17143006a866.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "fix: enhance accessibility attributes for dialog component",
+ "packageName": "@fluentui/web-components",
+ "email": "863023+radium-v@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/package.json b/package.json
index 756753194acc26..9dc6138cef295c 100644
--- a/package.json
+++ b/package.json
@@ -71,6 +71,7 @@
"@microsoft/api-extractor": "7.51.0",
"@microsoft/api-extractor-model": "7.31.2",
"@microsoft/eslint-plugin-sdl": "1.0.1",
+ "@microsoft/fast-element": "2.10.4",
"@microsoft/focusgroup-polyfill": "^1.4.1",
"@microsoft/load-themed-styles": "1.10.26",
"@microsoft/loader-load-themed-styles": "2.0.17",
@@ -86,7 +87,7 @@
"@octokit/rest": "18.12.0",
"@oddbird/css-anchor-positioning": "0.4.0",
"@phenomnomnominal/tsquery": "6.1.3",
- "@playwright/test": "1.55.1",
+ "@playwright/test": "1.56.1",
"@react-native/babel-preset": "0.73.21",
"@rnx-kit/eslint-plugin": "0.8.6",
"@rollup/plugin-node-resolve": "13.3.0",
@@ -256,7 +257,7 @@
"parse-diff": "0.7.1",
"patch-package": "8.0.0",
"path-browserify": "1.0.1",
- "playwright": "1.55.1",
+ "playwright": "1.56.1",
"plop": "2.6.0",
"portfinder": "1.0.28",
"postcss": "8.5.10",
@@ -354,7 +355,7 @@
"prettier": "2.8.8",
"puppeteer": "24.42.0",
"ws": "8.17.1",
- "playwright": "1.55.1",
+ "playwright": "1.56.1",
"**/prismjs": "^1.30.0",
"**/@tensile-perf/runner/express": "^4.22.1",
"**/tar-fs": "^2.1.4",
diff --git a/packages/charts/chart-web-components/package.json b/packages/charts/chart-web-components/package.json
index ffaac36ba9d6dc..b190c9a5f06147 100644
--- a/packages/charts/chart-web-components/package.json
+++ b/packages/charts/chart-web-components/package.json
@@ -61,7 +61,6 @@
"test:dev": "node ./scripts/e2e.js"
},
"devDependencies": {
- "@microsoft/fast-element": "2.0.0",
"@tensile-perf/web-components": "~0.2.2",
"@storybook/html": "9.1.17",
"@storybook/html-vite": "9.1.17",
@@ -79,7 +78,7 @@
"tslib": "^2.1.0"
},
"peerDependencies": {
- "@microsoft/fast-element": "^2.0.0-beta.26 || ^2.0.0"
+ "@microsoft/fast-element": "^2.0.0"
},
"beachball": {
"disallowedChangeTypes": [
diff --git a/packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/usePositioning.test.tsx b/packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/usePositioning.test.tsx
index 7f849b3294e97a..e048c4c6421b0a 100644
--- a/packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/usePositioning.test.tsx
+++ b/packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/usePositioning.test.tsx
@@ -54,18 +54,18 @@ describe('usePositioning', () => {
result.current.containerRef(node);
});
- expect(node).toHaveStyle({ position: 'absolute', inset: 'auto', margin: '0px' });
+ expect(node).toHaveStyle({ position: 'fixed', inset: 'auto', margin: '0px' });
});
- it('containerRef honors strategy: "fixed"', () => {
- const result = mountHook({ strategy: 'fixed' });
+ it('containerRef honors strategy: "absolute"', () => {
+ const result = mountHook({ strategy: 'absolute' });
const node = document.createElement('div');
act(() => {
result.current.containerRef(node);
});
- expect(node).toHaveStyle({ position: 'fixed' });
+ expect(node).toHaveStyle({ position: 'absolute' });
});
it('containerRef writes data-placement matching (position, align)', () => {
@@ -79,7 +79,7 @@ describe('usePositioning', () => {
expect(node).toHaveAttribute('data-placement', 'below-start');
});
- it('containerRef sets position-try-fallbacks to the three-try flip chain by default', () => {
+ it('containerRef sets position-try-fallbacks to the default flip chain', () => {
const result = mountHook();
const node = document.createElement('div');
diff --git a/packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/usePositioning.ts b/packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/usePositioning.ts
index 68bcd95c543733..53de720c0bbb58 100644
--- a/packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/usePositioning.ts
+++ b/packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/usePositioning.ts
@@ -17,6 +17,7 @@ import { usePlacementObserver } from './usePlacementObserver';
export type TargetElement = HTMLElement | PositioningVirtualElement;
const DEFAULT_FLIP = ['flip-block', 'flip-inline', 'flip-block flip-inline'];
+
const EMPTY_FALLBACK_POSITIONS: PositioningShorthandValue[] = [];
export function usePositioning(options: PositioningProps): PositioningReturn {
@@ -28,7 +29,7 @@ export function usePositioning(options: PositioningProps): PositioningReturn {
fallbackPositions = EMPTY_FALLBACK_POSITIONS,
offset,
coverTarget = false,
- strategy = 'absolute',
+ strategy = 'fixed',
matchTargetSize,
positioningRef,
} = options;
diff --git a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx
index dfd2e812a21755..12450363816641 100644
--- a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx
+++ b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx
@@ -6,7 +6,7 @@ import styles from './positioning.module.css';
export const MatchTargetSize = (): React.ReactNode => (
-
+
diff --git a/packages/web-components/docs/web-components.api.md b/packages/web-components/docs/web-components.api.md
index 45c4edfeca7653..6b004a99989099 100644
--- a/packages/web-components/docs/web-components.api.md
+++ b/packages/web-components/docs/web-components.api.md
@@ -2514,18 +2514,24 @@ export class Dialog extends FASTElement {
ariaLabel: string | null;
ariaLabelledby?: string;
clickHandler(event: Event): boolean;
- dialog: HTMLDialogElement;
// (undocumented)
- protected dialogChanged(): void;
- emitBeforeToggle: () => void;
+ connectedCallback(): void;
+ dialog: HTMLDialogElement;
+ // @internal
+ get dialogDescribedby(): string | undefined;
+ // @internal
+ get dialogLabel(): string | null | undefined;
+ // @internal
+ get dialogLabelledby(): string | undefined;
+ // @internal
+ get dialogModal(): boolean | undefined;
+ // @internal
+ get dialogRole(): string | undefined;
+ emitBeforeToggle(): void;
emitToggle: () => void;
hide(): void;
show(): void;
type: DialogType;
- // (undocumented)
- protected typeChanged(prev: DialogType | undefined, next: DialogType): void;
- // @internal
- protected updateDialogAttributes(): void;
}
// @public
@@ -2644,27 +2650,29 @@ export class Drawer extends FASTElement {
cancelHandler(): void;
// (undocumented)
clickHandler(event: Event): boolean;
- // @internal (undocumented)
+ // (undocumented)
connectedCallback(): void;
dialog: HTMLDialogElement;
- // @internal (undocumented)
- disconnectedCallback(): void;
+ // @internal
+ get dialogDescribedby(): string | undefined;
+ // @internal
+ get dialogLabel(): string | null | undefined;
+ // @internal
+ get dialogLabelledby(): string | undefined;
+ // @internal
+ get dialogModal(): boolean | undefined;
+ // @internal
+ get dialogRole(): string | null;
emitBeforeToggle: () => void;
emitToggle: () => void;
hide(): void;
- // (undocumented)
- protected observeRoleAttr(): void;
position: DrawerPosition;
// (undocumented)
- protected roleAttrObserver: MutationObserver;
+ role: string | null;
show(): void;
// (undocumented)
size: DrawerSize;
type: DrawerType;
- // (undocumented)
- protected typeChanged(): void;
- // (undocumented)
- protected updateDialogRole(): void;
}
// Warning: (ae-missing-release-tag) "DrawerBody" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
diff --git a/packages/web-components/package.json b/packages/web-components/package.json
index 0f4cd54a7c993c..3064cc7dc3eaf4 100644
--- a/packages/web-components/package.json
+++ b/packages/web-components/package.json
@@ -90,8 +90,6 @@
},
"devDependencies": {
"@custom-elements-manifest/analyzer": "0.10.10",
- "@microsoft/fast-element": "2.0.0",
- "@microsoft/focusgroup-polyfill": "^1.4.1",
"@tensile-perf/web-components": "~0.2.2",
"@storybook/html": "9.1.17",
"@storybook/html-vite": "9.1.17",
diff --git a/packages/web-components/src/compound-button/compound-button.spec.ts b/packages/web-components/src/compound-button/compound-button.spec.ts
new file mode 100644
index 00000000000000..f0df3f89b8589a
--- /dev/null
+++ b/packages/web-components/src/compound-button/compound-button.spec.ts
@@ -0,0 +1,723 @@
+import { expect, test } from '../../test/playwright/index.js';
+import { tagName } from './compound-button.options.js';
+
+test.describe('Compound Button', () => {
+ test.use({
+ tagName,
+ innerHTML: 'Button Description',
+ });
+
+ test('should create with document.createElement()', async ({ page, fastPage }) => {
+ await fastPage.setTemplate();
+
+ let hasError = false;
+
+ page.on('pageerror', () => {
+ hasError = true;
+ });
+
+ await page.evaluate(tagName => {
+ document.createElement(tagName);
+ }, tagName);
+
+ expect(hasError).toBe(false);
+ });
+
+ test('should NOT submit the parent form when clicked and `type` attribute is not set', async ({ fastPage, page }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await element.click();
+
+ await expect(page).not.toHaveURL(/foo/);
+ });
+
+ test('should NOT submit the parent form when clicked and `type` attribute is set to "button"', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await element.click();
+
+ await expect(page).not.toHaveURL(/foo/);
+ });
+
+ test('should NOT submit the parent form when clicked and `type` attribute is set to "reset"', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await element.click();
+
+ await expect(page).not.toHaveURL(/foo/);
+ });
+
+ test("should submit the form with the submit button's name and value when clicked", async ({ fastPage, page }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await element.click();
+
+ await expect(page).toHaveURL(/foo\?bar=baz$/);
+ });
+
+ test('should NOT submit a value when the `name` attribute is NOT set and the `value` attribute is set', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await element.click();
+
+ await expect(page).toHaveURL(/foo\?$/);
+ });
+
+ test('should be focusable by default', async ({ fastPage }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate();
+
+ await element.focus();
+
+ await expect(element).toBeFocused();
+ });
+
+ test('should NOT be focusable when the `disabled` attribute is present', async ({ fastPage }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate({ attributes: { disabled: true } });
+
+ await element.focus();
+
+ await expect(element).not.toBeFocused();
+ });
+
+ test('should apply transparency correctly when the `disabled` attribute is present', async ({ fastPage }) => {
+ const { element } = fastPage;
+ await fastPage.setTemplate({ attributes: { disabled: true } });
+
+ const transparent = 'rgba(0, 0, 0, 0)';
+
+ await expect(element).not.toHaveCSS('border-color', transparent);
+ await expect(element).not.toHaveCSS('background-color', transparent);
+
+ await element.evaluate(node => node.setAttribute('appearance', 'primary'));
+ await expect(element).toHaveCSS('border-color', transparent);
+ await expect(element).not.toHaveCSS('background-color', transparent);
+
+ await element.evaluate(node => node.setAttribute('appearance', 'secondary'));
+ await expect(element).not.toHaveCSS('border-color', transparent);
+ await expect(element).not.toHaveCSS('background-color', transparent);
+
+ await element.evaluate(node => node.setAttribute('appearance', 'outline'));
+ await expect(element).not.toHaveCSS('border-color', transparent);
+ await expect(element).toHaveCSS('background-color', transparent);
+
+ await element.evaluate(node => node.setAttribute('appearance', 'subtle'));
+ await expect(element).toHaveCSS('border-color', transparent);
+ await expect(element).toHaveCSS('background-color', transparent);
+
+ await element.evaluate(node => node.setAttribute('appearance', 'transparent'));
+ await expect(element).toHaveCSS('border-color', transparent);
+ await expect(element).toHaveCSS('background-color', transparent);
+ });
+
+ test('should be focusable when the `disabled-focusable` attribute is present', async ({ fastPage }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate({ attributes: { 'disabled-focusable': true } });
+
+ await element.focus();
+
+ await expect(element).toBeFocused();
+ });
+
+ test('should apply transparency correctly when the `disabled-focusable` attribute is present', async ({
+ fastPage,
+ }) => {
+ const { element } = fastPage;
+ await fastPage.setTemplate({ attributes: { 'disabled-focusable': true } });
+
+ const transparent = 'rgba(0, 0, 0, 0)';
+
+ await expect(element).not.toHaveCSS('border-color', transparent);
+ await expect(element).not.toHaveCSS('background-color', transparent);
+
+ await element.evaluate(node => node.setAttribute('appearance', 'primary'));
+ await expect(element).toHaveCSS('border-color', transparent);
+ await expect(element).not.toHaveCSS('background-color', transparent);
+
+ await element.evaluate(node => node.setAttribute('appearance', 'secondary'));
+ await expect(element).not.toHaveCSS('border-color', transparent);
+ await expect(element).not.toHaveCSS('background-color', transparent);
+
+ await element.evaluate(node => node.setAttribute('appearance', 'outline'));
+ await expect(element).not.toHaveCSS('border-color', transparent);
+ await expect(element).toHaveCSS('background-color', transparent);
+
+ await element.evaluate(node => node.setAttribute('appearance', 'subtle'));
+ await expect(element).toHaveCSS('border-color', transparent);
+ await expect(element).toHaveCSS('background-color', transparent);
+
+ await element.evaluate(node => node.setAttribute('appearance', 'transparent'));
+ await expect(element).toHaveCSS('border-color', transparent);
+ await expect(element).toHaveCSS('background-color', transparent);
+ });
+
+ test('should NOT be clickable when the `disabled` attribute is present', async ({ fastPage, page }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate({ attributes: { disabled: true } });
+
+ const wasNotClicked = await page.evaluate(el => {
+ const event = new KeyboardEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ });
+
+ // The return value of dispatchEvent will be false if any event listener called preventDefault, or true otherwise.
+ return el?.dispatchEvent(event);
+ }, await element.elementHandle());
+
+ expect(wasNotClicked).toEqual(true);
+ });
+
+ for (const key of ['Enter', ' ']) {
+ test(`should NOT be actionable with \`${key}\` keypress when the \`disabled-focusable\` attribute is present`, async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate({ attributes: { 'disabled-focusable': true } });
+
+ await element.focus();
+
+ const isActionable = await page.evaluate(
+ ([el, key]) => {
+ const event = new KeyboardEvent('keypress', {
+ key: key as string,
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ });
+
+ // The return value of dispatchEvent will be false if any event listener called preventDefault, or true otherwise.
+ return (el as HTMLElement).dispatchEvent(event);
+ },
+ [await element.elementHandle(), key],
+ );
+
+ expect(isActionable).toBe(false);
+ });
+ }
+
+ test('should NOT receive focus when the `tabindex` is manually set to -1', async ({ fastPage, page }) => {
+ const element = page.locator(tagName, { hasText: 'Not Focusable' });
+ const focusable = page.locator(tagName, { hasText: 'Receives Focus' });
+
+ await fastPage.setTemplate(/* html */ `
+ <${tagName}>Receives Focus${tagName}>
+ <${tagName} tabindex="-1">Not Focusable${tagName}>
+ `);
+
+ await focusable.focus();
+
+ await expect(focusable).toBeFocused();
+
+ await focusable.press('Tab');
+
+ await expect(element).not.toBeFocused();
+ });
+
+ test('should focus the element when the `autofocus` attribute is present', async ({ fastPage }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate({ attributes: { autofocus: true } });
+
+ await expect(element).toBeFocused();
+ });
+
+ test('should submit the parent form when clicked and `type` attribute is set to "submit"', async ({ fastPage }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await element.click();
+
+ await expect(fastPage.page).toHaveURL(/foo/);
+ });
+
+ test('should reset the parent form when clicked and `type` attribute is set to "reset"', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+ const input = page.locator('#text-input');
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await expect(input).toHaveValue('');
+
+ await input.fill('foo');
+
+ await expect(input).toHaveValue('foo');
+
+ await element.click();
+
+ await expect(input).toHaveValue('');
+ });
+
+ test('should NOT reset the parent form when the `type` attribute is set to "reset" and the `disabled` attribute is present', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+ const input = page.locator('#text-input');
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await input.fill('foo');
+
+ await element.click();
+
+ await expect(input).toHaveValue('foo');
+ });
+
+ test('should do nothing when clicked while not in a form and the `type` attribute is set to "submit"', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ <${tagName} type="submit">Submit Button${tagName}>
+ `);
+
+ await element.click();
+
+ await expect(page).not.toHaveURL(/foo/);
+ });
+
+ test('should do nothing when clicked while not in a form and the `type` attribute is set to "reset"', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+ const input = page.locator('#text-input');
+
+ await fastPage.setTemplate(/* html */ `
+
+ <${tagName} type="reset">Submit Button${tagName}>
+ `);
+
+ await expect(input).toHaveValue('');
+
+ await input.fill('foo');
+
+ await expect(input).toHaveValue('foo');
+
+ await element.click();
+
+ await expect(input).toHaveValue('foo');
+
+ await expect(page).not.toHaveURL(/foo/);
+ });
+
+ test('should NOT submit the parent form when clicked and `disabled` attribute is present', async ({ fastPage }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await element.click();
+
+ await expect(fastPage.page).not.toHaveURL(/foo/);
+ });
+
+ test('should submit the parent form when clicked and the `form` attribute is provided', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+
+ <${tagName} type="submit" form="testform">Submit Button${tagName}>
+ `);
+
+ await expect(page).not.toHaveURL(/foo/);
+
+ await element.click();
+
+ await expect(page).toHaveURL(/foo/);
+ });
+
+ test('should override the form action when the `formaction` attribute is provided', async ({ fastPage, page }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await element.click();
+
+ await expect(page).not.toHaveURL(/foo/);
+
+ await expect(page).toHaveURL(/bar/);
+ });
+
+ test('should override the action of the referenced form when the `formaction` and `form` attributes are provided', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ <${tagName} type="submit" form="testform" formaction="bar">Submit Button${tagName}>
+ `);
+
+ await element.click();
+
+ await expect(page).not.toHaveURL(/foo/);
+
+ await expect(page).toHaveURL(/bar/);
+ });
+
+ test('should override the form method when the `formmethod` attribute is provided', async ({ fastPage, page }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await expect(page).not.toHaveURL(/foo/);
+
+ const method = page.waitForRequest(request => request.method() === 'POST');
+
+ await element.click();
+
+ await expect(method).resolves.toBeTruthy();
+
+ await expect(page).toHaveURL(/foo/);
+ });
+
+ test('should override the method of the referenced form when the `formmethod` and `form` attributes are provided', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+
+ <${tagName} type="submit" form="testform" formmethod="post">Submit Button${tagName}>
+ `);
+
+ await expect(page).not.toHaveURL(/foo/);
+
+ const method = page.waitForRequest(request => request.method() === 'POST');
+
+ await element.click();
+
+ await expect(method).resolves.toBeTruthy();
+
+ await expect(page).toHaveURL(/foo/);
+ });
+
+ test('should override the form encoding when the `formenctype` attribute is provided', async ({ fastPage, page }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await expect(page).not.toHaveURL(/foo/);
+
+ await element.click();
+
+ await expect(page).toHaveURL(/foo\?testinput=hello\+world$/);
+ });
+
+ test('should override the encoding of the referenced form when the `formenctype` and `form` attributes are provided', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+
+ <${tagName} type="submit" form="testform" formenctype="plain/text">Submit Button${tagName}>
+ `);
+
+ await expect(page).not.toHaveURL(/foo/);
+
+ await element.click();
+
+ await expect(page).toHaveURL(/foo\?testinput=hello\+world$/);
+ });
+
+ test('should override the form target when the `formtarget` attribute is provided', async ({ fastPage, page }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await expect(page).not.toHaveURL(/foo/);
+
+ await element.click();
+
+ await expect(page).toHaveURL(/foo/);
+ });
+
+ test('should override the target of the referenced form when the `formtarget` and `form` attributes are provided', async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate(/* html */ `
+
+
+ <${tagName} type="submit" form="testform" formtarget="_self">Submit Button${tagName}>
+ `);
+
+ await expect(page).not.toHaveURL(/foo/);
+
+ await element.click();
+
+ await expect(page).toHaveURL(/foo/);
+ });
+
+ test('should submit the parent form when form validation errors are present and the `formnovalidate` attribute is present', async ({
+ fastPage,
+ page,
+ }) => {
+ const form = page.locator('#test-form');
+ const { element } = fastPage;
+ const input = page.locator('#text-input');
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await input.fill('foo');
+
+ const validity = await form.evaluate((node: HTMLFormElement) => node.checkValidity());
+
+ expect(validity).toBe(false);
+
+ await expect(page).not.toHaveURL(/foo/);
+
+ await element.click();
+
+ await expect(page).toHaveURL(/foo/);
+ });
+
+ test('should submit the referenced form when form validation errors are present and the `formnovalidate` and `form` attributes are present', async ({
+ fastPage,
+ page,
+ }) => {
+ const form = page.locator('#test-form');
+ const { element } = fastPage;
+ const input = page.locator('#text-input');
+
+ await fastPage.setTemplate(/* html */ `
+
+
+ <${tagName} type="submit" form="test-form" formnovalidate>Button${tagName}>
+ `);
+
+ await input.fill('foo');
+
+ expect(await form.evaluate((node: HTMLFormElement) => node.checkValidity())).toBeFalsy();
+
+ await expect(page).not.toHaveURL(/foo/);
+
+ await element.click();
+
+ await expect(page).toHaveURL(/foo/);
+ });
+
+ test('should NOT submit the parent form when form validation errors are present and the `formnovalidate` is NOT present', async ({
+ fastPage,
+ page,
+ }) => {
+ const button = page.locator(tagName);
+ const input = page.locator('#text-input');
+
+ await fastPage.setTemplate(/* html */ `
+
+ `);
+
+ await input.fill('foo');
+
+ const wasNotSubmitted = input.evaluate(
+ node =>
+ new Promise(resolve => {
+ node.addEventListener('invalid', () => resolve(true));
+ }),
+ );
+
+ await button.click();
+
+ await expect(wasNotSubmitted).resolves.toBeTruthy();
+ });
+
+ test('should NOT submit the referenced form when form validation errors are present and the `formnovalidate` attribute is NOT present and the `form` attribute is present', async ({
+ fastPage,
+ page,
+ }) => {
+ const button = page.locator(tagName);
+ const input = page.locator('#text-input');
+
+ await fastPage.setTemplate(/* html */ `
+
+
+ <${tagName} type="submit" form="test-form">Button${tagName}>
+ `);
+
+ await input.fill('foo');
+
+ const wasInvalid = input.evaluate(
+ node =>
+ new Promise(resolve => {
+ node.addEventListener('invalid', () => resolve(true));
+ }),
+ );
+
+ await button.click();
+
+ await expect(wasInvalid).resolves.toBeTruthy();
+ });
+
+ test.describe('description slot', () => {
+ test('should render content in the description slot', async ({ fastPage }) => {
+ const { element } = fastPage;
+ const description = element.locator('slot[name="description"]');
+
+ await fastPage.setTemplate({
+ innerHTML: 'Primary text Secondary text',
+ });
+
+ await expect(description).toHaveCount(1);
+ await expect(element).toContainText('Primary text');
+ await expect(element).toContainText('Secondary text');
+ });
+
+ test('should render without description slot content', async ({ fastPage }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate({ innerHTML: 'Button text only' });
+
+ await expect(element).toContainText('Button text only');
+ });
+
+ test('should place the description slot inside the content part', async ({ fastPage }) => {
+ const { element } = fastPage;
+ const contentPart = element.locator('slot:not([name])');
+ const descriptionSlot = element.locator('slot[name="description"]');
+
+ await fastPage.setTemplate({
+ innerHTML: 'Click me More info',
+ });
+
+ expect(
+ await contentPart.evaluate((node: HTMLSlotElement) =>
+ node.assignedNodes().map(n => ({ textContent: n.textContent })),
+ ),
+ ).toEqual([{ textContent: 'Click me ' }]);
+
+ expect(
+ await descriptionSlot.evaluate((node: HTMLSlotElement) =>
+ node.assignedElements().map(n => ({ textContent: n.textContent })),
+ ),
+ ).toEqual([{ textContent: 'More info' }]);
+ });
+ });
+});
diff --git a/packages/web-components/src/compound-button/compound-button.template.ts b/packages/web-components/src/compound-button/compound-button.template.ts
index bfe14d286e5959..49b5c4eb01c353 100644
--- a/packages/web-components/src/compound-button/compound-button.template.ts
+++ b/packages/web-components/src/compound-button/compound-button.template.ts
@@ -4,12 +4,16 @@ import type { CompoundButton } from './compound-button.js';
import type { CompoundButtonOptions } from './compound-button.options.js';
/**
- * The template for the Compound Button component.
+ * Generates a template for the Button component.
+ *
* @public
*/
export function buttonTemplate(options: CompoundButtonOptions = {}): ElementViewTemplate {
return html`
- x.disabled}" tabindex="${x => (x.disabled ? null : x.tabIndex ?? 0)}">
+ x.clickHandler(c.event as MouseEvent)}"
+ @keypress="${(x, c) => x.keypressHandler(c.event as KeyboardEvent)}"
+ >
${startSlotTemplate(options)}
@@ -22,6 +26,7 @@ export function buttonTemplate(options: CompoundButton
/**
* The template for the Button component.
+ *
* @public
*/
export const template: ElementViewTemplate = buttonTemplate();
diff --git a/packages/web-components/src/dialog-body/dialog-body.spec.ts b/packages/web-components/src/dialog-body/dialog-body.spec.ts
new file mode 100644
index 00000000000000..b751113ab8405e
--- /dev/null
+++ b/packages/web-components/src/dialog-body/dialog-body.spec.ts
@@ -0,0 +1,94 @@
+import { expect, test } from '../../test/playwright/index.js';
+import { tagName } from './dialog-body.options.js';
+
+test.describe('DialogBody', () => {
+ test.use({
+ tagName,
+ innerHTML: 'Dialog Body',
+ });
+
+ test('should create with `document.createElement()`', async ({ page, fastPage }) => {
+ await fastPage.setTemplate('');
+
+ let hasError = false;
+
+ page.on('pageerror', () => {
+ hasError = true;
+ });
+
+ await page.evaluate(tagName => {
+ document.createElement(tagName);
+ }, tagName);
+
+ expect(hasError).toBe(false);
+ });
+
+ test('should display a title when content is provided in the title slot', async ({ fastPage }) => {
+ const { element } = fastPage;
+
+ await fastPage.setTemplate({
+ innerHTML: /* html */ `
+ Dialog Title
+ `,
+ });
+
+ await expect(element).toHaveText('Dialog Title');
+ });
+
+ test("should call the parent element's `hide` method when elements in the close slot are clicked", async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+ const closeButton = element.locator('#close-button');
+ const customDialog = page.locator('custom-dialog');
+
+ await fastPage.setTemplate(/* html */ `
+
+ <${tagName}>
+ Dialog Title
+ Dialog Body
+
+ ${tagName}>
+
+ `);
+
+ await customDialog.evaluate((node: HTMLElement) => {
+ (node as any).hide = () => {
+ node.dataset.hideCalled = 'true';
+ };
+ });
+
+ await closeButton.click();
+
+ await expect(customDialog).toHaveAttribute('data-hide-called', 'true');
+ });
+
+ test("should not throw an error if the parent element doesn't have a `hide` method when elements in the close slot are clicked", async ({
+ fastPage,
+ page,
+ }) => {
+ const { element } = fastPage;
+ const closeButton = element.locator('#close-button');
+
+ await fastPage.setTemplate(/* html */ `
+
+ <${tagName}>
+
Dialog Title
+
Dialog Body
+
+ ${tagName}>
+
+ `);
+
+ let hasError = false;
+
+ page.on('pageerror', () => {
+ hasError = true;
+ });
+
+ await closeButton.click();
+
+ expect(hasError).toBe(false);
+ });
+});
diff --git a/packages/web-components/src/dialog/dialog.template.ts b/packages/web-components/src/dialog/dialog.template.ts
index 321cb4d02dc242..018eae02fa1a65 100644
--- a/packages/web-components/src/dialog/dialog.template.ts
+++ b/packages/web-components/src/dialog/dialog.template.ts
@@ -9,9 +9,11 @@ export const template: ElementViewTemplate