Skip to content

Commit

Permalink
feat(atomic): add arrow key navigation to atomic-tab/atomic-insight-t…
Browse files Browse the repository at this point in the history
…ab (#4799)

This PR adds arrow key navigation to atomic-tab and atomic-insight-tab

https://coveord.atlassian.net/browse/KIT-3799

---------

Co-authored-by: Louis Bompart <[email protected]>
  • Loading branch information
fpbrault and louis-bompart authored Jan 6, 2025
1 parent 82d9737 commit 5e7b15f
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 5 deletions.
21 changes: 17 additions & 4 deletions packages/atomic/src/components/common/tabs/tab-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {h, Component, Element, Host, Listen, State} from '@stencil/core';
import {h, Component, Element, Host, State, Listen} from '@stencil/core';
import {Button} from '../button';
import {TabCommonElement} from './tab-common';

Expand Down Expand Up @@ -152,6 +152,7 @@ export class TabBar {
title={tab.label}
onClick={() => {
tab.select();
this.updatePopoverTabs();
this.tabPopover?.togglePopover();
}}
>
Expand All @@ -161,10 +162,18 @@ export class TabBar {
));
};

private setTabButtonMaxWidth = () => {
this.displayedTabs.forEach((tab) => {
tab.style.setProperty('max-width', `calc(100% - ${this.popoverWidth}px)`);
});
};

private updateTabsDisplay = () => {
this.updateTabVisibility(this.overflowingTabs, false);
this.updateTabVisibility(this.displayedTabs, true);
this.setTabButtonMaxWidth();
this.updatePopoverPosition();
this.updatePopoverTabs();
this.tabPopover?.setButtonVisibility(!!this.overflowingTabs.length);
};

Expand All @@ -173,9 +182,14 @@ export class TabBar {
event.stopPropagation();
this.updatePopoverTabs();
}
public componentWillUpdate() {
this.updateTabsDisplay();
}

public componentDidLoad() {
this.resizeObserver = new ResizeObserver(this.render);
this.resizeObserver = new ResizeObserver(() => {
this.updateTabsDisplay();
});
this.resizeObserver.observe(this.host);
}

Expand All @@ -184,9 +198,8 @@ export class TabBar {
}

public render = () => {
this.updateTabsDisplay();
return (
<Host>
<Host class="overflow-x-clip overflow-y-visible">
<slot></slot>
<tab-popover>{this.popoverTabs}</tab-popover>
</Host>
Expand Down
56 changes: 55 additions & 1 deletion packages/atomic/src/components/common/tabs/tab-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,63 @@ export class TabPopover implements InitializableComponent {

@Listen('keydown')
public handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape' && this.isOpen) {
if (!this.isOpen) {
return;
}

if (e.key === 'Escape') {
this.togglePopover();
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
this.navigatePopover(e.key);
}
}

private getTabButtons(): HTMLButtonElement[] {
const slot = this.popupRef.children[0] as HTMLSlotElement;
return Array.from(slot.assignedElements())
.map((el) => (el as HTMLElement).querySelector('button'))
.filter((el): el is HTMLButtonElement => el !== null);
}

private getCurrentTabIndex(elements: HTMLButtonElement[]): number {
const MAX_SHADOW_DEPTH = 3;
let currentElement = document.activeElement;
let depth = 0;

while (currentElement?.shadowRoot && depth < MAX_SHADOW_DEPTH) {
currentElement = currentElement.shadowRoot.activeElement;
depth++;
}

if (!(currentElement instanceof HTMLButtonElement)) {
return -1;
}

return elements.indexOf(currentElement);
}

private navigatePopover(key: string): void {
const tabButtons = this.getTabButtons();
if (!tabButtons.length) {
return;
}

const currentIndex = this.getCurrentTabIndex(tabButtons);

const startIndex = currentIndex > -1 ? currentIndex : -1;

let nextIndex;
if (currentIndex === -1) {
nextIndex = key === 'ArrowDown' ? 0 : tabButtons.length - 1;
} else {
nextIndex =
key === 'ArrowDown'
? (startIndex + 1) % tabButtons.length
: (startIndex - 1 + tabButtons.length) % tabButtons.length;
}

tabButtons[nextIndex]?.focus();
}

get slotElements() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,66 @@ test.describe('AtomicTabManager', () => {
expect(accessibilityResults.violations).toEqual([]);
});

test.describe('keyboard navigation', () => {
test('should navigate within popover menu using arrow keys', async ({
tabManager,
page,
}) => {
await tabManager.tabPopoverMenuButton.click();
const popoverTabs = tabManager.popoverTabs();
await page.keyboard.press('ArrowDown');
await expect(popoverTabs.first()).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(popoverTabs.nth(1)).toBeFocused();
});

test('should navigate within popover menu using tab', async ({
tabManager,
page,
}) => {
await tabManager.tabPopoverMenuButton.click();
const popoverTabs = tabManager.popoverTabs();
await page.keyboard.press('Tab');
await expect(popoverTabs.first()).toBeFocused();
await page.keyboard.press('Tab');
await expect(popoverTabs.first()).not.toBeFocused();

await page.keyboard.press('Tab');
await expect(tabManager.tabPopoverMenuButton).not.toBeFocused();
});

test('should wrap around when using arrow keys', async ({
tabManager,
page,
}) => {
await tabManager.tabPopoverMenuButton.click();
const popoverTabs = tabManager.popoverTabs();
await page.keyboard.press('ArrowUp');
await expect(popoverTabs.last()).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(popoverTabs.first()).toBeFocused();
});

test('should close popover menu when pressing escape', async ({
tabManager,
page,
}) => {
const popoverTabs = tabManager.popoverTabs();
await expect(popoverTabs.first()).not.toBeVisible();

await tabManager.tabPopoverMenuButton.click();
const allTabs = await popoverTabs.all();
for (const tab of allTabs) {
await expect(tab).toBeVisible();
}

await page.keyboard.press('Escape');
for (const tab of allTabs) {
await expect(tab).not.toBeVisible();
}
});
});

test('should display tabs popover menu button', async ({tabManager}) => {
await expect(tabManager.tabPopoverMenuButton).toBeVisible();
});
Expand Down

0 comments on commit 5e7b15f

Please sign in to comment.