Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scrollable tabs #2440

Merged
merged 61 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
97e5e0b
Working implementation.
atmgrifter00 Sep 19, 2024
d46ba68
Minor changes. Tests.
atmgrifter00 Sep 23, 2024
9e1c054
Change files
atmgrifter00 Sep 23, 2024
2230643
Fix build.
atmgrifter00 Sep 23, 2024
6e36099
Turn off prettier on templates.
atmgrifter00 Sep 23, 2024
6b03d00
Merge branch 'main' into scrollable-tabs
atmgrifter00 Sep 27, 2024
f745a10
Merge branch 'main' into scrollable-tabs
atmgrifter00 Oct 14, 2024
d5e8c81
Slight fixes and tests.
atmgrifter00 Oct 15, 2024
224b08b
Minor update.
atmgrifter00 Oct 16, 2024
f4dccf3
Name change
atmgrifter00 Oct 16, 2024
3ecfd42
Remove commented out line.
atmgrifter00 Oct 16, 2024
47bd01b
Remove changes to tabs storybook.
atmgrifter00 Oct 16, 2024
4c31925
Update packages/nimble-components/src/tabs/index.ts
Oct 17, 2024
18b239c
Handle PR feedback.
atmgrifter00 Oct 17, 2024
f25634d
Remove unneeded styling.
atmgrifter00 Oct 17, 2024
3e83e90
Merge branch 'main' into scrollable-tabs
Oct 17, 2024
f2baaf8
Hide scroll buttons from ARIA.
atmgrifter00 Oct 17, 2024
0566358
Merge branch 'scrollable-tabs' of https://github.com/ni/nimble into s…
atmgrifter00 Oct 17, 2024
355aac2
Missed a needed change.
atmgrifter00 Oct 17, 2024
061e646
Remove fit.
atmgrifter00 Oct 17, 2024
316e1b7
Prettier
atmgrifter00 Oct 17, 2024
6d326b0
Prettier
atmgrifter00 Oct 17, 2024
b1ef8ee
Handle PR feedback.
atmgrifter00 Oct 18, 2024
f8581ea
Remove unneeded template element.
atmgrifter00 Oct 18, 2024
0ed5c78
Fix test I forgot to update.
atmgrifter00 Oct 18, 2024
16bec4e
Handle PR feedback.
atmgrifter00 Oct 21, 2024
721cc7a
Add storybook options. Fix tab scroll issue and add test.
atmgrifter00 Oct 22, 2024
4c83ac3
Prettier.
atmgrifter00 Oct 22, 2024
2018dbf
Clean up tests.
atmgrifter00 Oct 22, 2024
d89d21a
Handle PR feedback.
atmgrifter00 Oct 22, 2024
bd1e9e0
Fix labels.
atmgrifter00 Oct 22, 2024
27d642b
Handle PR feedback.
atmgrifter00 Oct 22, 2024
3350e9f
Prettier.
atmgrifter00 Oct 22, 2024
d10f9a8
Update packages/nimble-components/src/label-provider/core/label-token…
rajsite Oct 22, 2024
df51461
Merge branch 'main' into scrollable-tabs
rajsite Oct 22, 2024
da38c01
Fix label attribute name
atmgrifter00 Oct 22, 2024
d56b658
Merge branch 'scrollable-tabs' of https://github.com/ni/nimble into s…
atmgrifter00 Oct 22, 2024
59d7d30
Update packages/storybook/src/nimble/tabs/tabs.stories.ts
Oct 22, 2024
d9beb08
Update packages/storybook/src/nimble/anchor-tabs/anchor-tabs.stories.ts
Oct 22, 2024
d7ae3dd
Handle PR feedback.
atmgrifter00 Oct 22, 2024
583d3c6
Merge branch 'scrollable-tabs' of https://github.com/ni/nimble into s…
atmgrifter00 Oct 22, 2024
197cb23
Update packages/storybook/src/nimble/tabs/tabs.stories.ts
Oct 22, 2024
6288f36
Update packages/storybook/src/nimble/anchor-tabs/anchor-tabs.stories.ts
Oct 22, 2024
fa56503
Minor fixes.
atmgrifter00 Oct 22, 2024
37f7ec6
Apply suggestions from code review
rajsite Oct 22, 2024
4895d3a
Fix panel overflow.
atmgrifter00 Oct 23, 2024
d526d69
Merge branch 'scrollable-tabs' of https://github.com/ni/nimble into s…
atmgrifter00 Oct 23, 2024
ce55e55
Merge branch 'main' into scrollable-tabs
Oct 23, 2024
191f64f
Merge branch 'main' into scrollable-tabs
Oct 23, 2024
5d654cf
Make tabs animate when you scroll with buttons.
atmgrifter00 Oct 23, 2024
5f25d19
Merge branch 'scrollable-tabs' of https://github.com/ni/nimble into s…
atmgrifter00 Oct 23, 2024
60cada3
Update packages/nimble-components/src/utilities/tests/timeout.ts
Oct 23, 2024
e1b6ce2
Update packages/storybook/src/nimble/tabs/tabs.stories.ts
Oct 23, 2024
c5237bf
Update packages/storybook/src/nimble/anchor-tabs/anchor-tabs.stories.ts
Oct 23, 2024
1cd67c1
Update packages/storybook/src/nimble/anchor-tabs/anchor-tabs.stories.ts
Oct 23, 2024
c76224b
Handle PR feedback.
atmgrifter00 Oct 23, 2024
03f3342
Merge branch 'scrollable-tabs' of https://github.com/ni/nimble into s…
atmgrifter00 Oct 23, 2024
ee1486f
Fix matrix storybook styling.
atmgrifter00 Oct 23, 2024
959e9db
More PR feedback.
atmgrifter00 Oct 23, 2024
107a24e
Fix tags in tests.
atmgrifter00 Oct 23, 2024
6ec8fc8
Prettier.
atmgrifter00 Oct 23, 2024
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 @@
{
jattasNI marked this conversation as resolved.
Show resolved Hide resolved
jattasNI marked this conversation as resolved.
Show resolved Hide resolved
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
"type": "minor",
"comment": "Scrollable tabs for Tabs and AnchorTabs.",
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
"packageName": "@ni/nimble-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions packages/nimble-components/src/anchor-tab/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const styles = css`
align-items: center;
justify-content: center;
cursor: pointer;
text-wrap: nowrap;
--ni-private-active-indicator-width: 2px;
--ni-private-focus-indicator-width: 1px;
--ni-private-indicator-lines-gap: 1px;
Expand Down
75 changes: 71 additions & 4 deletions packages/nimble-components/src/anchor-tabs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ import {
FoundationElementDefinition,
FoundationElement
} from '@microsoft/fast-foundation';
import { styles } from './styles';
import { template } from './template';
import { styles } from '../patterns/tabs/styles';
import { template } from '../patterns/tabs/template';
import type { AnchorTab } from '../anchor-tab';
import type { TabsOwner } from '../patterns/tabs/types';

declare global {
interface HTMLElementTagNameMap {
Expand All @@ -41,7 +42,7 @@ export type TabsOptions = FoundationElementDefinition & StartEndOptions;
/**
* A nimble-styled set of anchor tabs
*/
export class AnchorTabs extends FoundationElement {
export class AnchorTabs extends FoundationElement implements TabsOwner {
/**
* The id of the active tab
*
Expand All @@ -58,6 +59,12 @@ export class AnchorTabs extends FoundationElement {
@observable
public tabs!: HTMLElement[];

/**
* @internal
*/
@observable
public showScrollButtons = false;

/**
* A reference to the active tab
* @public
Expand All @@ -70,14 +77,44 @@ export class AnchorTabs extends FoundationElement {
*/
public tablist!: HTMLElement;

/**
* @internal
*/
public readonly leftScrollButton?: HTMLElement;

/**
* @internal
*/
public readonly tabSlotName = 'anchortab';

private readonly tabListResizeObserver: ResizeObserver;
private tabIds: string[] = [];

public constructor() {
super();
this.tabListResizeObserver = new ResizeObserver(entries => {
let tabListVisibleWidth = entries[0]?.contentRect.width;
if (tabListVisibleWidth !== undefined) {
const buttonWidth = this.leftScrollButton?.clientWidth ?? 0;
tabListVisibleWidth = Math.ceil(tabListVisibleWidth);
if (this.showScrollButtons) {
tabListVisibleWidth += buttonWidth * 2;
}
this.showScrollButtons = tabListVisibleWidth < this.tablist.scrollWidth;
}
});
}

/**
* @internal
*/
public activeidChanged(_oldValue: string, _newValue: string): void {
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
if (this.$fastController.isConnected) {
this.setTabs();
this.activetab?.scrollIntoView({
block: 'nearest',
inline: 'start'
});
}
}

Expand All @@ -91,15 +128,43 @@ export class AnchorTabs extends FoundationElement {
}
}

/**
* @internal
*/
public onScrollLeftClick(): void {
this.tablist.scrollBy({
left: -this.tablist.clientWidth,
behavior: 'smooth'
});
}

/**
* @internal
*/
public onScrollRightClick(): void {
this.tablist.scrollBy({
left: this.tablist.clientWidth,
behavior: 'smooth'
});
}

/**
* @internal
*/
public override connectedCallback(): void {
super.connectedCallback();

this.tabListResizeObserver.observe(this.tablist);
this.tabIds = this.getTabIds();
}

/**
* @internal
*/
public override disconnectedCallback(): void {
super.disconnectedCallback();
this.tabListResizeObserver.disconnect();
}

private readonly isDisabledElement = (el: Element): el is HTMLElement => {
return el.getAttribute('aria-disabled') === 'true';
};
Expand Down Expand Up @@ -277,6 +342,8 @@ export class AnchorTabs extends FoundationElement {
tab === focusedTab ? 'true' : 'false'
);
});

focusedTab.scrollIntoView({ block: 'nearest', inline: 'start' });
};

private getTabAnchor(tab: AnchorTab): HTMLAnchorElement {
Expand Down
23 changes: 0 additions & 23 deletions packages/nimble-components/src/anchor-tabs/styles.ts

This file was deleted.

18 changes: 0 additions & 18 deletions packages/nimble-components/src/anchor-tabs/template.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { AnchorTabs } from '..';
import { anchorTabTag } from '../../anchor-tab';
import { TabsBasePageObject } from '../../patterns/tabs/testing/tabs-base.pageobject';

/**
* Page object for the `nimble-anchor-tabs` component to provide consistent ways
* of querying and interacting with the component during tests.
*/
export class AnchorTabsPageObject extends TabsBasePageObject<AnchorTabs> {
public constructor(tabsElement: AnchorTabs) {
super(tabsElement, anchorTabTag);
}
}
121 changes: 113 additions & 8 deletions packages/nimble-components/src/anchor-tabs/tests/anchor-tabs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import '../../anchor-tab';
import { anchorTabTag, type AnchorTab } from '../../anchor-tab';
import { waitForUpdatesAsync } from '../../testing/async-helpers';
import { fixture, Fixture } from '../../utilities/tests/fixture';
import { AnchorTabsPageObject } from '../testing/anchor-tabs.pageobject';

describe('AnchorTabs', () => {
let element: AnchorTabs;
let connect: () => Promise<void>;
let disconnect: () => Promise<void>;
let pageObject: AnchorTabsPageObject;

function tab(index: number): AnchorTab {
return element.children[index] as AnchorTab;
Expand All @@ -42,6 +44,7 @@ describe('AnchorTabs', () => {
beforeEach(async () => {
({ element, connect, disconnect } = await setup());
await connect();
pageObject = new AnchorTabsPageObject(element);
});

afterEach(async () => {
Expand Down Expand Up @@ -118,8 +121,7 @@ describe('AnchorTabs', () => {
expect(tab(0).tabIndex).toBe(-1);
expect(tab(1).tabIndex).toBe(0);
expect(tab(2).tabIndex).toBe(-1);
tab(0).dispatchEvent(new Event('click'));
await waitForUpdatesAsync();
await pageObject.clickTab(0);
expect(tab(0).tabIndex).toBe(0);
expect(tab(1).tabIndex).toBe(-1);
expect(tab(2).tabIndex).toBe(-1);
Expand All @@ -130,9 +132,7 @@ describe('AnchorTabs', () => {
anchor(0).addEventListener('click', () => {
timesClicked += 1;
});
tab(0).dispatchEvent(
new KeyboardEvent('keydown', { key: keySpace })
);
await pageObject.pressKeyOnTab(0, keySpace);
await waitForUpdatesAsync();
expect(timesClicked).toBe(1);
});
Expand All @@ -142,9 +142,7 @@ describe('AnchorTabs', () => {
anchor(0).addEventListener('click', () => {
timesClicked += 1;
});
tab(0).dispatchEvent(
new KeyboardEvent('keydown', { key: keyEnter })
);
await pageObject.pressKeyOnTab(0, keyEnter);
await waitForUpdatesAsync();
expect(timesClicked).toBe(1);
});
Expand Down Expand Up @@ -374,4 +372,111 @@ describe('AnchorTabs', () => {
expect(document.activeElement).toBe(tab(0));
});
});

describe('scroll buttons', () => {
async function setup(): Promise<Fixture<AnchorTabs>> {
return await fixture<AnchorTabs>(
html`<${anchorTabsTag} activeid="tab-two">
<${anchorTabTag}>Tab 1</${anchorTabTag}>
<${anchorTabTag} id="tab-two">Tab 2</${anchorTabTag}>
<${anchorTabTag} id="tab-three">Tab 3</${anchorTabTag}>
<${anchorTabTag} id="tab-four">Tab 4</${anchorTabTag}>
<${anchorTabTag} id="tab-five">Tab 5</${anchorTabTag}>
<${anchorTabTag} id="tab-six">Tab 6</${anchorTabTag}>
</${anchorTabsTag}>`
);
}

let tabsPageObject: AnchorTabsPageObject;

beforeEach(async () => {
({ element, connect, disconnect } = await setup());
await connect();
tabsPageObject = new AnchorTabsPageObject(element);
});

afterEach(async () => {
await disconnect();
});

it('should not show scroll buttons when the tabs fit within the container', () => {
expect(tabsPageObject.areScrollButtonsVisible()).toBeFalse();
});

it('should show scroll buttons when the tabs overflow the container', async () => {
await tabsPageObject.setTabsWidth(300);
expect(tabsPageObject.areScrollButtonsVisible()).toBeTrue();
});

it('should hide scroll buttons when the tabs no longer overflow the container', async () => {
await tabsPageObject.setTabsWidth(300);
await tabsPageObject.setTabsWidth(1000);
expect(tabsPageObject.areScrollButtonsVisible()).toBeFalse();
});

it('should scroll left when the left scroll button is clicked', async () => {
await tabsPageObject.setTabsWidth(300);
element.activeid = 'tab-six'; // scrolls to the last tab
const currentScrollOffset = tabsPageObject.getTabsViewScrollOffset();
await tabsPageObject.clickScrollLeftButton();
expect(tabsPageObject.getTabsViewScrollOffset()).toBeLessThan(
currentScrollOffset
);
});

it('should not scroll left when the left scroll button is clicked and the first tab is active', async () => {
await tabsPageObject.setTabsWidth(300);
await tabsPageObject.clickScrollLeftButton();
expect(tabsPageObject.getTabsViewScrollOffset()).toBe(0);
});

it('should scroll right when the right scroll button is clicked', async () => {
await tabsPageObject.setTabsWidth(300);
await tabsPageObject.clickScrollRightButton();
expect(tabsPageObject.getTabsViewScrollOffset()).toBeGreaterThan(0);
});

it('should not scroll right when the right scroll button is clicked and the last tab is active', async () => {
await tabsPageObject.setTabsWidth(300);
element.activeid = 'tab-six'; // scrolls to the last tab
const currentScrollOffset = tabsPageObject.getTabsViewScrollOffset();
await tabsPageObject.clickScrollRightButton();
expect(tabsPageObject.getTabsViewScrollOffset()).toBe(
currentScrollOffset
);
});

it('should show scroll buttons when new tab is added and tabs overflow the container', async () => {
await tabsPageObject.setTabsWidth(450);
expect(tabsPageObject.areScrollButtonsVisible()).toBeFalse();
await tabsPageObject.addTab('New Tab With Extremely Long Name');
expect(tabsPageObject.areScrollButtonsVisible()).toBeTrue();
});

it('should hide scroll buttons when tab is removed and tabs no longer overflow the container', async () => {
await tabsPageObject.setTabsWidth(500);
await tabsPageObject.addTab('New Tab With Extremely Long Name');
expect(tabsPageObject.areScrollButtonsVisible()).toBeTrue();
await tabsPageObject.removeTab(6);
expect(tabsPageObject.areScrollButtonsVisible()).toBeFalse();
});

it('should show scroll buttons when tab label is updated and tabs overflow the container', async () => {
await tabsPageObject.setTabsWidth(450);
expect(tabsPageObject.areScrollButtonsVisible()).toBeFalse();
await tabsPageObject.updateTabLabel(
0,
'New Tab With Extremely Long Name'
);
expect(tabsPageObject.areScrollButtonsVisible()).toBeTrue();
});

it('should hide scroll buttons when tab label is updated and tabs no longer overflow the container', async () => {
await tabsPageObject.setTabsWidth(550);
await tabsPageObject.addTab('New Tab With Extremely Long Name');
expect(tabsPageObject.areScrollButtonsVisible()).toBeTrue();
await tabsPageObject.updateTabLabel(6, 'Tab 6');
expect(tabsPageObject.areScrollButtonsVisible()).toBeFalse();
});
});
});
Loading