Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
2 changes: 2 additions & 0 deletions packages/web-components/docs/web-components.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 13 additions & 4 deletions packages/web-components/src/tablist/tablist.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,19 +158,28 @@ 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) {
const rootNode = this.getRootNode() as Document | ShadowRoot;
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;
}

Expand Down
67 changes: 67 additions & 0 deletions packages/web-components/src/tablist/tablist.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/web-components/src/tablist/tablist.template.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<f-template name="fluent-tablist" shadowrootmode="open">
<template role="tablist" focusgroup="tablist inline block" @focusin="{handleFocusIn($e)}">
<template role="tablist" focusgroup="tablist inline block" @click="{handleClick($e)}" @focusin="{handleFocusIn($e)}">
{{styles}}
<slot name="tab" f-slotted="{slottedTabs}"></slot>
</template>
Expand Down
1 change: 1 addition & 0 deletions packages/web-components/src/tablist/tablist.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const template = html<Tablist>`
<template
role="tablist"
focusgroup="tablist inline block"
@click="${(x, c) => x.handleClick(c.event as PointerEvent)}"
@focusin="${(x, c) => x.handleFocusIn(c.event as FocusEvent)}"
>
<slot name="tab" ${slotted('slottedTabs')}></slot>
Expand Down
Loading