From eebb4337934edf01b1e4b0fda31a50786fa040da Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Tue, 1 Sep 2026 14:48:39 -0700 Subject: [PATCH] fix(web-components): menu list focus immediately after connecting to DOM (#36678) --- ...-a244fd3d-fd18-44c5-8d02-d59a97cade4b.json | 7 ++++++ .../src/menu-list/menu-list.base.ts | 20 ++++++++++++++++- .../src/menu-list/menu-list.spec.ts | 22 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 change/@fluentui-web-components-a244fd3d-fd18-44c5-8d02-d59a97cade4b.json diff --git a/change/@fluentui-web-components-a244fd3d-fd18-44c5-8d02-d59a97cade4b.json b/change/@fluentui-web-components-a244fd3d-fd18-44c5-8d02-d59a97cade4b.json new file mode 100644 index 0000000000000..4c74d88526075 --- /dev/null +++ b/change/@fluentui-web-components-a244fd3d-fd18-44c5-8d02-d59a97cade4b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: focus menu items immediately after connecting a 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 89f120627241f..c7aba6f8599ce 100644 --- a/packages/web-components/src/menu-list/menu-list.base.ts +++ b/packages/web-components/src/menu-list/menu-list.base.ts @@ -92,7 +92,25 @@ export class BaseMenuList extends FASTElement { * @public */ public focus(): void { - this.menuItems?.find(item => !item.disabled)?.focus(); + // Queue the focus() call so that browesr gets enough time to execute + // `.setItems()` and set `.menuItems`. + // + // This is useful for: + // + // ``` + // const list = document.createElement("fluent-menu-list"); + // const option = document.createElement("fluent-menu-item"); + // list.append(option); + // document.body.append(list); + // list.focus(); + // ``` + // + // Without `Updates.enqueu()`, the above `focus()` call would fail because + // in `connectedCallback()`, `.setItems()` is called in an + // `Updates.enqueue()`. + Updates.enqueue(() => { + this.menuItems?.find(item => !item.disabled)?.focus(); + }); } protected setItems(): void { 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 45da8a6ee4e49..27206f1f1e4ab 100644 --- a/packages/web-components/src/menu-list/menu-list.spec.ts +++ b/packages/web-components/src/menu-list/menu-list.spec.ts @@ -78,6 +78,28 @@ test.describe('MenuList', () => { await expect(firstItem).toBeFocused(); }); + test('should focus before the queued item refresh completes', async ({ fastPage, page }) => { + await fastPage.setTemplate(''); + await page.evaluate( + ({ menuItemTagName, menuListTagName }) => { + const menuList = document.createElement(menuListTagName); + menuList.dataset.immediateFocus = ''; + for (const label of ['Menu item 1', 'Menu item 2']) { + const menuItem = document.createElement(menuItemTagName); + menuItem.textContent = label; + menuList.append(menuItem); + } + document.body.append(menuList); + menuList.focus(); + }, + { menuItemTagName: MenuItemTagName, menuListTagName: tagName }, + ); + const menuItems = page.locator(`${tagName}[data-immediate-focus]`).locator(MenuItemTagName); + await expect(menuItems.first()).toBeFocused(); + await page.keyboard.press('ArrowDown'); + await expect(menuItems.nth(1)).toBeFocused(); + }); + test('should not throw when `focus()` is called with no items', async ({ fastPage }) => { const { element } = fastPage;