From e5b0e2412175216210360fee356a404af60f38d5 Mon Sep 17 00:00:00 2001 From: John Kreitlow <863023+radium-v@users.noreply.github.com> Date: Tue, 12 May 2026 10:11:44 -0700 Subject: [PATCH 1/4] fix: clean up fast-element dependency hierarchy and update to 2.10.4 (#36184) ## Previous Behavior The `@microsoft/fast-element` dependency was pinned to version `2.0.0` across multiple packages, and the dependency hierarchy had several issues: - `packages/web-components/package.json` listed `@microsoft/fast-element` and `@microsoft/focusgroup-polyfill` as `devDependencies` despite already declaring them as `peerDependencies`, duplicating the hoisted entries unnecessarily. - `packages/charts/chart-web-components/package.json` had a redundant `devDependencies` entry for `@microsoft/fast-element` pinned to `2.0.0`, and its `peerDependencies` range included the pre-release syntax `^2.0.0-beta.26 || ^2.0.0`. - `apps/vr-tests-web-components/package.json` pinned `@microsoft/fast-element` to exactly `2.0.0`. - The root `package.json` did not hoist `@microsoft/fast-element`, so the resolved version in `yarn.lock` was stuck at `2.0.0`. ## New Behavior - Removed the redundant `devDependencies` entries for `@microsoft/fast-element` and `@microsoft/focusgroup-polyfill` from `packages/web-components/package.json` since they are already declared as `peerDependencies` and satisfied by the root. - Removed the redundant `devDependencies` entry for `@microsoft/fast-element` from `packages/charts/chart-web-components/package.json`. - Simplified the `peerDependencies` range in `chart-web-components` to `^2.0.0` (dropping the unnecessary beta range). - Changed `apps/vr-tests-web-components` to use `^2.0.0` instead of the exact pin. - Added `@microsoft/fast-element` at version `2.10.4` to the root `devDependencies` so it is hoisted and all workspace packages resolve to the same up-to-date version. - `yarn.lock` now resolves `@microsoft/fast-element` to `2.10.4`. --- apps/vr-tests-web-components/package.json | 2 +- ...b-components-efc149c5-3d43-4300-a413-b3d9048e2248.json | 7 +++++++ ...b-components-38caae0f-3c50-46f0-b0c2-bb87de96d641.json | 7 +++++++ package.json | 1 + packages/charts/chart-web-components/package.json | 3 +-- packages/web-components/package.json | 2 -- yarn.lock | 8 ++++---- 7 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 change/@fluentui-chart-web-components-efc149c5-3d43-4300-a413-b3d9048e2248.json create mode 100644 change/@fluentui-web-components-38caae0f-3c50-46f0-b0c2-bb87de96d641.json 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-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/package.json b/package.json index 756753194acc26..2096a7d2c09783 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", 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/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/yarn.lock b/yarn.lock index 629621be4153c6..747a1e412521fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2687,10 +2687,10 @@ eslint-plugin-react "7.35.2" eslint-plugin-security "1.4.0" -"@microsoft/fast-element@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@microsoft/fast-element/-/fast-element-2.0.0.tgz#4b3551159290f14ec59f9b48c6c4c26b4e086a0f" - integrity sha512-Tzv4dCGTg10NNyqWWGRy3bYG4vacQUapjy5gpdpmFbSgvNF8aOzJJMkG5CfVUDoC9gWxY1ZyGgjXX0p86ZXX5w== +"@microsoft/fast-element@2.10.4", "@microsoft/fast-element@^2.0.0": + version "2.10.4" + resolved "https://registry.yarnpkg.com/@microsoft/fast-element/-/fast-element-2.10.4.tgz#12b8c6c90902f2a54d5b27f618d7d095e133546d" + integrity sha512-OkHIlMztq7+PgkRF1LscgBd/MVzMhGrWPkJRDvOEqdqEdZOKocKvaKcUKZubIBz4RpLIrhLD3lOJzCsspk6jXg== "@microsoft/fast-web-utilities@^6.0.0": version "6.0.0" From 52fd89c25172001d37bc9e36a7b2f2587272edbe Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Tue, 12 May 2026 10:19:56 -0700 Subject: [PATCH 2/4] fix(web-components): fix keyboard navigation regressions for tree and menu-list (#36118) --- ...-80c47489-fa3b-4929-8f16-f8ff8d79b89f.json | 7 + .../src/menu-list/menu-list.base.ts | 8 +- .../src/menu-list/menu-list.spec.ts | 38 ++++ packages/web-components/src/tree/tree.base.ts | 2 + packages/web-components/src/tree/tree.spec.ts | 182 ++++++++++++++++++ .../web-components/src/tree/tree.template.ts | 2 +- packages/web-components/src/tree/tree.ts | 2 +- 7 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 change/@fluentui-web-components-80c47489-fa3b-4929-8f16-f8ff8d79b89f.json 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/packages/web-components/src/menu-list/menu-list.base.ts b/packages/web-components/src/menu-list/menu-list.base.ts index d08aaa46d3b2ae..6ebcd314be5c0d 100644 --- a/packages/web-components/src/menu-list/menu-list.base.ts +++ b/packages/web-components/src/menu-list/menu-list.base.ts @@ -1,4 +1,4 @@ -import { FASTElement, observable, Updates } from '@microsoft/fast-element'; +import { FASTElement, Observable, observable, Updates } from '@microsoft/fast-element'; import { isHTMLElement } from '../utils/typings.js'; import type { MenuItemColumnCount } from '../menu-item/menu-item.js'; import type { MenuItem } from '../menu-item/menu-item.js'; @@ -63,6 +63,9 @@ export class BaseMenuList extends FASTElement { */ public disconnectedCallback(): void { super.disconnectedCallback(); + Array.from(this.children).forEach(child => { + Observable.getNotifier(child).unsubscribe(this, 'hidden'); + }); this.menuChildren = undefined; this.removeEventListener('change', this.changedMenuItemHandler); } @@ -100,6 +103,9 @@ export class BaseMenuList extends FASTElement { protected setItems(): void { const children: HTMLElement[] = Array.from(this.children) as HTMLElement[]; + children.forEach((child: Element) => { + Observable.getNotifier(child).subscribe(this, 'hidden'); + }); this.menuChildren = children.filter(child => !child.hasAttribute('hidden')); 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 25372a141be4b9..68511a4c83f8a7 100644 --- a/packages/web-components/src/menu-list/menu-list.spec.ts +++ b/packages/web-components/src/menu-list/menu-list.spec.ts @@ -400,6 +400,44 @@ test.describe('MenuList', () => { await expect(menuItems.nth(0)).toBeFocused(); }); + test('should navigate to previously hidden items when visibility restored', async ({ fastPage }) => { + const { element } = fastPage; + const menuItems = element.locator('fluent-menu-item'); + + await fastPage.setTemplate({ + innerHTML: /* html */ ` + Menu item 1 + + Menu item 3 + Menu item 4 + `, + }); + + await element.evaluate(node => { + node.focus(); + }); + + await expect(menuItems.nth(0)).toBeFocused(); + + await element.press('ArrowDown'); + + await expect(menuItems.nth(2)).toBeFocused(); + + await menuItems.nth(1).evaluate(node => { + 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(); + }); + test('should set the data-indent attribute to 0 correctly on all MenuItem elements when role of menuitem and not content in start slot', async ({ fastPage, }) => { diff --git a/packages/web-components/src/tree/tree.base.ts b/packages/web-components/src/tree/tree.base.ts index aa8114ce468e98..b836dc312a3a8a 100644 --- a/packages/web-components/src/tree/tree.base.ts +++ b/packages/web-components/src/tree/tree.base.ts @@ -83,6 +83,8 @@ export class BaseTree extends FASTElement { if (item?.childTreeItems?.length) { if (!item.expanded) { item.expanded = true; + } else { + return true; } } return; diff --git a/packages/web-components/src/tree/tree.spec.ts b/packages/web-components/src/tree/tree.spec.ts index 72df168c459ea8..03b2c13c6b70c4 100644 --- a/packages/web-components/src/tree/tree.spec.ts +++ b/packages/web-components/src/tree/tree.spec.ts @@ -216,6 +216,7 @@ test.describe('Tree', () => { await expect(treeItem1).toHaveAttribute('selected'); expect(await elementHandle).toBe(false); }); + test('keyboard navigation should work when the tree-item contains focusable elements', async ({ fastPage, page, @@ -241,4 +242,185 @@ test.describe('Tree', () => { await page.keyboard.press(browserName === 'webkit' ? 'Alt+Tab' : 'Tab'); await expect(anchor).toBeFocused(); }); + test('should move focus down with ArrowDown', async ({ fastPage, page }) => { + const { element } = fastPage; + const treeItems = element.locator(`:scope > fluent-tree-item`); + + await fastPage.setTemplate({ + innerHTML: /* html */ ` + Item 1 + Item 2 + Item 3 + `, + }); + + await treeItems.nth(0).focus(); + await expect(treeItems.nth(0)).toBeFocused(); + + await page.keyboard.press('ArrowDown'); + await expect(treeItems.nth(1)).toBeFocused(); + + await page.keyboard.press('ArrowDown'); + await expect(treeItems.nth(2)).toBeFocused(); + }); + + test('should move focus up with ArrowUp', async ({ fastPage, page }) => { + const { element } = fastPage; + const treeItems = element.locator(`:scope > fluent-tree-item`); + + await fastPage.setTemplate({ + innerHTML: /* html */ ` + Item 1 + Item 2 + Item 3 + `, + }); + + await treeItems.nth(0).focus(); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await expect(treeItems.nth(2)).toBeFocused(); + + await page.keyboard.press('ArrowUp'); + await expect(treeItems.nth(1)).toBeFocused(); + + await page.keyboard.press('ArrowUp'); + await expect(treeItems.nth(0)).toBeFocused(); + }); + + test('should expand a collapsed item with ArrowRight', async ({ fastPage, page }) => { + const { element } = fastPage; + const parentItem = element.locator(`:scope > fluent-tree-item`); + + await fastPage.setTemplate({ + innerHTML: /* html */ ` + + Item 1 + Nested Item A + + `, + }); + + await parentItem.focus(); + await expect(parentItem).toBeFocused(); + await expect(parentItem).not.toHaveAttribute('expanded'); + + await page.keyboard.press('ArrowRight'); + await expect(parentItem).toHaveAttribute('expanded'); + }); + + test('should focus child after ArrowRight on an expanded item', async ({ fastPage, page }) => { + const { element } = fastPage; + const parentItem = element.locator(`:scope > fluent-tree-item`); + const nestedItem = parentItem.locator('fluent-tree-item'); + + await fastPage.setTemplate({ + innerHTML: /* html */ ` + + Item 1 + Nested Item A + + `, + }); + + await parentItem.focus(); + // expand first + await page.keyboard.press('ArrowRight'); + await expect(parentItem).toHaveAttribute('expanded'); + await expect(nestedItem).toBeVisible(); + + // arrow right again should focus the nested item + await page.keyboard.press('ArrowRight'); + await expect(nestedItem).toBeFocused(); + }); + + test('should collapse an expanded item with ArrowLeft', async ({ fastPage, page }) => { + const { element } = fastPage; + const parentItem = element.locator(`:scope > fluent-tree-item`); + + await fastPage.setTemplate({ + innerHTML: /* html */ ` + + Item 1 + Nested Item A + + `, + }); + + await parentItem.focus(); + await page.keyboard.press('ArrowRight'); + await expect(parentItem).toHaveAttribute('expanded'); + + await page.keyboard.press('ArrowLeft'); + await expect(parentItem).not.toHaveAttribute('expanded'); + }); + + test('should focus parent item with ArrowLeft on a nested item', async ({ fastPage, page }) => { + const { element } = fastPage; + const parentItem = element.locator(`:scope > fluent-tree-item`); + const nestedItem = parentItem.locator('fluent-tree-item'); + + await fastPage.setTemplate({ + innerHTML: /* html */ ` + + Item 1 + Nested Item A + + `, + }); + + await parentItem.focus(); + // expand and wait for nested item to be visible + await page.keyboard.press('ArrowRight'); + await expect(parentItem).toHaveAttribute('expanded'); + await expect(nestedItem).toBeVisible(); + + // focus nested item + await page.keyboard.press('ArrowRight'); + await expect(nestedItem).toBeFocused(); + + // arrow left on leaf should focus parent + await page.keyboard.press('ArrowLeft'); + await expect(parentItem).toBeFocused(); + }); + + test('should focus first item with Home key', async ({ fastPage, page }) => { + const { element } = fastPage; + const treeItems = element.locator(`:scope > fluent-tree-item`); + + await fastPage.setTemplate({ + innerHTML: /* html */ ` + Item 1 + Item 2 + Item 3 + `, + }); + + await treeItems.nth(0).focus(); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await expect(treeItems.nth(2)).toBeFocused(); + + await page.keyboard.press('Home'); + await expect(treeItems.nth(0)).toBeFocused(); + }); + + test('should focus last item with End key', async ({ fastPage, page }) => { + const { element } = fastPage; + const treeItems = element.locator(`:scope > fluent-tree-item`); + + await fastPage.setTemplate({ + innerHTML: /* html */ ` + Item 1 + Item 2 + Item 3 + `, + }); + + await treeItems.nth(0).focus(); + await expect(treeItems.nth(0)).toBeFocused(); + + await page.keyboard.press('End'); + await expect(treeItems.nth(2)).toBeFocused(); + }); }); diff --git a/packages/web-components/src/tree/tree.template.ts b/packages/web-components/src/tree/tree.template.ts index 04f6392feefa4c..05b8e8073e1473 100644 --- a/packages/web-components/src/tree/tree.template.ts +++ b/packages/web-components/src/tree/tree.template.ts @@ -3,7 +3,7 @@ import type { Tree } from './tree.js'; export const template = html`