diff --git a/change/@fluentui-web-components-b11fac43-0a4c-4db4-8f05-1694dbdfbd40.json b/change/@fluentui-web-components-b11fac43-0a4c-4db4-8f05-1694dbdfbd40.json new file mode 100644 index 0000000000000..8bd83a1c65515 --- /dev/null +++ b/change/@fluentui-web-components-b11fac43-0a4c-4db4-8f05-1694dbdfbd40.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: select a tab when click() is called on it", + "packageName": "@fluentui/web-components", + "email": "machi@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/docs/web-components.api.md b/packages/web-components/docs/web-components.api.md index 8732dacfa46be..af72cf614720f 100644 --- a/packages/web-components/docs/web-components.api.md +++ b/packages/web-components/docs/web-components.api.md @@ -904,6 +904,8 @@ export class BaseTablist extends FASTElement { // @internal elementInternals: ElementInternals; // @internal (undocumented) + handleClick(event: PointerEvent): void; + // @internal (undocumented) handleFocusIn(event: FocusEvent): void; orientation: TablistOrientation; // (undocumented) diff --git a/packages/web-components/src/tablist/tablist.base.ts b/packages/web-components/src/tablist/tablist.base.ts index 325c99496b198..bb7ee0ad365e6 100644 --- a/packages/web-components/src/tablist/tablist.base.ts +++ b/packages/web-components/src/tablist/tablist.base.ts @@ -158,11 +158,20 @@ export class BaseTablist extends FASTElement { /** @internal */ public handleFocusIn(event: FocusEvent) { - const target = event.target as Node; - if (!isTab(target) || target.disabled) { + this.activeid = (event.target as HTMLElement).id; + } + + /** @internal */ + public handleClick(event: PointerEvent) { + // We only need to handle click event when `tab.click()` is called, a user + // click will be handled by `focusin` event. And as per the DOM spec, + // calling `Element.click()` results in `isTrusted=false`: + // https://dom.spec.whatwg.org/#dom-event-istrusted + if (event.isTrusted) { return; } - this.activeid = target.id; + + this.activeid = (event.target as HTMLElement).id; } private changeTab(oldId: undefined | string, newId: string) { @@ -170,7 +179,7 @@ export class BaseTablist extends FASTElement { const prevTab = oldId ? rootNode.getElementById(oldId) : null; const nextTab = rootNode.getElementById(newId); - if (!isTab(nextTab) || !this.contains(nextTab)) { + if (!isTab(nextTab) || nextTab.disabled || !this.contains(nextTab)) { return; } diff --git a/packages/web-components/src/tablist/tablist.spec.ts b/packages/web-components/src/tablist/tablist.spec.ts index 3e578c2f48c37..4d95f4f0bd83a 100644 --- a/packages/web-components/src/tablist/tablist.spec.ts +++ b/packages/web-components/src/tablist/tablist.spec.ts @@ -150,6 +150,73 @@ test.describe('Tablist', () => { }); test.describe('active tab', () => { + test('should select a tab when it’s clicked', async ({ fastPage }) => { + const { element } = fastPage; + const tabs = element.locator(TabTagName); + + await fastPage.setTemplate(); + + const secondTab = tabs.nth(1); + const secondTabId = (await secondTab.getAttribute('id')) as string; + + await secondTab.click(); + + await expect(tabs.nth(0)).not.toHaveAttribute('aria-selected', 'true'); + await expect(secondTab).toHaveAttribute('aria-selected', 'true'); + await expect(element).toHaveAttribute('activeid', secondTabId); + }); + + test('should select a tab when it’s focused', async ({ fastPage }) => { + const { element } = fastPage; + const tabs = element.locator(TabTagName); + + await fastPage.setTemplate(); + + const secondTab = tabs.nth(1); + const secondTabId = (await secondTab.getAttribute('id')) as string; + + await secondTab.focus(); + + await expect(tabs.nth(0)).not.toHaveAttribute('aria-selected', 'true'); + await expect(secondTab).toHaveAttribute('aria-selected', 'true'); + await expect(element).toHaveAttribute('activeid', secondTabId); + }); + + test('should select a tab when the focus is moved onto it', async ({ fastPage }) => { + const { element } = fastPage; + const tabs = element.locator(TabTagName); + + await fastPage.setTemplate(); + + const firstTab = tabs.nth(0); + const secondTabId = (await tabs.nth(1).getAttribute('id')) as string; + + await firstTab.focus(); + await firstTab.press('ArrowRight'); + + await expect(firstTab).not.toHaveAttribute('aria-selected', 'true'); + await expect(tabs.nth(1)).toHaveAttribute('aria-selected', 'true'); + await expect(element).toHaveAttribute('activeid', secondTabId); + }); + + test('should select a tab when `click()` is called on it', async ({ fastPage }) => { + const { element } = fastPage; + const tabs = element.locator(TabTagName); + + await fastPage.setTemplate(); + + const secondTab = tabs.nth(1); + const secondTabId = (await secondTab.getAttribute('id')) as string; + + await secondTab.evaluate((node: Tab) => { + node.click(); + }); + + await expect(tabs.nth(0)).not.toHaveAttribute('aria-selected', 'true'); + await expect(secondTab).toHaveAttribute('aria-selected', 'true'); + await expect(element).toHaveAttribute('activeid', secondTabId); + }); + test('should set an `aria-selected` attribute on the active tab when `activeid` is provided', async ({ fastPage, }) => { diff --git a/packages/web-components/src/tablist/tablist.template.html b/packages/web-components/src/tablist/tablist.template.html index f436038c4e8ab..15416ee5dab0a 100644 --- a/packages/web-components/src/tablist/tablist.template.html +++ b/packages/web-components/src/tablist/tablist.template.html @@ -1,5 +1,5 @@ -