kopia lustrzana https://github.com/shoelace-style/shoelace
453 wiersze
19 KiB
TypeScript
453 wiersze
19 KiB
TypeScript
import '../../../dist/shoelace.js';
|
|
import { aTimeout, elementUpdated, expect, fixture, oneEvent, waitUntil } from '@open-wc/testing';
|
|
import { clickOnElement } from '../../internal/test.js';
|
|
import { html } from 'lit';
|
|
import { isElementVisibleFromOverflow } from '../../internal/test/element-visible-overflow.js';
|
|
import { queryByTestId } from '../../internal/test/data-testid-helpers.js';
|
|
import { sendKeys } from '@web/test-runner-commands';
|
|
import { waitForScrollingToEnd } from '../../internal/test/wait-for-scrolling.js';
|
|
import type { HTMLTemplateResult } from 'lit';
|
|
import type { SlTabShowEvent } from '../../events/sl-tab-show.js';
|
|
import type SlTab from '../tab/tab.js';
|
|
import type SlTabGroup from './tab-group.js';
|
|
import type SlTabPanel from '../tab-panel/tab-panel.js';
|
|
|
|
interface ClientRectangles {
|
|
body?: DOMRect;
|
|
navigation?: DOMRect;
|
|
}
|
|
|
|
const waitForScrollButtonsToBeRendered = async (tabGroup: SlTabGroup): Promise<void> => {
|
|
await waitUntil(() => {
|
|
const scrollButtons = tabGroup.shadowRoot?.querySelectorAll('sl-icon-button');
|
|
return scrollButtons?.length === 2;
|
|
});
|
|
};
|
|
|
|
const getClientRectangles = (tabGroup: SlTabGroup): ClientRectangles => {
|
|
const shadowRoot = tabGroup.shadowRoot;
|
|
if (shadowRoot) {
|
|
const nav = shadowRoot.querySelector<HTMLElement>('[part=nav]');
|
|
const body = shadowRoot.querySelector<HTMLElement>('[part=body]');
|
|
return {
|
|
body: body?.getBoundingClientRect(),
|
|
navigation: nav?.getBoundingClientRect()
|
|
};
|
|
}
|
|
return {};
|
|
};
|
|
|
|
const expectHeaderToBeVisible = (container: HTMLElement, dataTestId: string): void => {
|
|
const generalHeader = queryByTestId<SlTab>(container, dataTestId);
|
|
expect(generalHeader).not.to.be.null;
|
|
expect(generalHeader).to.be.visible;
|
|
};
|
|
|
|
const expectOnlyOneTabPanelToBeActive = async (container: HTMLElement, dataTestIdOfActiveTab: string) => {
|
|
await waitUntil(() => {
|
|
const tabPanels = Array.from(container.getElementsByTagName('sl-tab-panel'));
|
|
const activeTabPanels = tabPanels.filter((element: SlTabPanel) => element.hasAttribute('active'));
|
|
return activeTabPanels.length === 1;
|
|
});
|
|
const tabPanels = Array.from(container.getElementsByTagName('sl-tab-panel'));
|
|
const activeTabPanels = tabPanels.filter((element: SlTabPanel) => element.hasAttribute('active'));
|
|
expect(activeTabPanels).to.have.lengthOf(1);
|
|
expect(activeTabPanels[0]).to.have.attribute('data-testid', dataTestIdOfActiveTab);
|
|
};
|
|
|
|
const expectPromiseToHaveName = async (showEventPromise: Promise<SlTabShowEvent>, expectedName: string) => {
|
|
const showEvent = await showEventPromise;
|
|
expect(showEvent.detail.name).to.equal(expectedName);
|
|
};
|
|
|
|
const waitForHeaderToBeActive = async (container: HTMLElement, headerTestId: string): Promise<SlTab> => {
|
|
const generalHeader = queryByTestId<SlTab>(container, headerTestId);
|
|
await waitUntil(() => {
|
|
return generalHeader?.hasAttribute('active');
|
|
});
|
|
if (generalHeader) {
|
|
return generalHeader;
|
|
} else {
|
|
throw new Error(`did not find error with testid=${headerTestId}`);
|
|
}
|
|
};
|
|
|
|
describe('<sl-tab-group>', () => {
|
|
it('renders', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general">General</sl-tab>
|
|
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
|
|
expect(tabGroup).to.be.visible;
|
|
});
|
|
|
|
it('is accessible', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general">General</sl-tab>
|
|
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
|
|
await expect(tabGroup).to.be.accessible();
|
|
});
|
|
|
|
it('displays all tabs', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general" data-testid="general-tab-header">General</sl-tab>
|
|
<sl-tab slot="nav" panel="disabled" disabled data-testid="disabled-tab-header">Disabled</sl-tab>
|
|
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
|
<sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
|
|
expectHeaderToBeVisible(tabGroup, 'general-tab-header');
|
|
expectHeaderToBeVisible(tabGroup, 'disabled-tab-header');
|
|
});
|
|
|
|
it('shows the first tab to be active by default', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general">General</sl-tab>
|
|
<sl-tab slot="nav" panel="custom">Custom</sl-tab>
|
|
<sl-tab-panel name="general" data-testid="general-tab-content">This is the general tab panel.</sl-tab-panel>
|
|
<sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
|
|
await expectOnlyOneTabPanelToBeActive(tabGroup, 'general-tab-content');
|
|
});
|
|
|
|
describe('proper positioning', () => {
|
|
it('shows the header above the tabs by default', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general">General</sl-tab>
|
|
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
|
|
await aTimeout(0);
|
|
|
|
const clientRectangles = getClientRectangles(tabGroup);
|
|
expect(clientRectangles.body?.top).to.be.greaterThanOrEqual(clientRectangles.navigation?.bottom || -Infinity);
|
|
});
|
|
|
|
it('shows the header below the tabs by setting placement to bottom', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general">General</sl-tab>
|
|
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
tabGroup.placement = 'bottom';
|
|
|
|
await aTimeout(0);
|
|
|
|
const clientRectangles = getClientRectangles(tabGroup);
|
|
expect(clientRectangles.body?.bottom).to.be.lessThanOrEqual(clientRectangles.navigation?.top || +Infinity);
|
|
});
|
|
|
|
it('shows the header left of the tabs by setting placement to start', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general">General</sl-tab>
|
|
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
tabGroup.placement = 'start';
|
|
|
|
await aTimeout(0);
|
|
|
|
const clientRectangles = getClientRectangles(tabGroup);
|
|
expect(clientRectangles.body?.left).to.be.greaterThanOrEqual(clientRectangles.navigation?.right || -Infinity);
|
|
});
|
|
|
|
it('shows the header right of the tabs by setting placement to end', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general">General</sl-tab>
|
|
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
tabGroup.placement = 'end';
|
|
|
|
await aTimeout(0);
|
|
|
|
const clientRectangles = getClientRectangles(tabGroup);
|
|
expect(clientRectangles.body?.right).to.be.lessThanOrEqual(clientRectangles.navigation?.left || -Infinity);
|
|
});
|
|
});
|
|
|
|
describe('scrolling behavior', () => {
|
|
const generateTabs = (n: number): HTMLTemplateResult[] => {
|
|
const result: HTMLTemplateResult[] = [];
|
|
for (let i = 0; i < n; i++) {
|
|
result.push(
|
|
html`<sl-tab slot="nav" panel="tab-${i}">Tab ${i}</sl-tab>
|
|
<sl-tab-panel name="tab-${i}">Content of tab ${i}0</sl-tab-panel> `
|
|
);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
before(() => {
|
|
// disabling failing on resize observer ... unfortunately on webkit this is not really specific
|
|
// https://github.com/WICG/resize-observer/issues/38#issuecomment-422126006
|
|
// https://stackoverflow.com/a/64197640
|
|
const errorHandler = window.onerror;
|
|
window.onerror = (
|
|
event: string | Event,
|
|
source?: string | undefined,
|
|
lineno?: number | undefined,
|
|
colno?: number | undefined,
|
|
error?: Error | undefined
|
|
) => {
|
|
if ((event as string).includes('ResizeObserver') || event === 'Script error.') {
|
|
return true;
|
|
} else if (errorHandler) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
return errorHandler(event, source, lineno, colno, error);
|
|
} else {
|
|
return true;
|
|
}
|
|
};
|
|
});
|
|
|
|
it('shows scroll buttons on too many tabs', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`<sl-tab-group> ${generateTabs(30)} </sl-tab-group>`);
|
|
|
|
await waitForScrollButtonsToBeRendered(tabGroup);
|
|
|
|
const scrollButtons = tabGroup.shadowRoot?.querySelectorAll('sl-icon-button');
|
|
expect(scrollButtons, 'Both scroll buttons should be shown').to.have.length(2);
|
|
|
|
tabGroup.disconnectedCallback();
|
|
});
|
|
|
|
it('does not show scroll buttons on too many tabs if deactivated', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`<sl-tab-group> ${generateTabs(30)} </sl-tab-group>`);
|
|
tabGroup.noScrollControls = true;
|
|
|
|
await aTimeout(0);
|
|
|
|
const scrollButtons = tabGroup.shadowRoot?.querySelectorAll('sl-icon-button');
|
|
expect(scrollButtons).to.have.length(0);
|
|
});
|
|
|
|
it('does not show scroll buttons if all tabs fit on the screen', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`<sl-tab-group> ${generateTabs(2)} </sl-tab-group>`);
|
|
|
|
await aTimeout(0);
|
|
|
|
const scrollButtons = tabGroup.shadowRoot?.querySelectorAll('sl-icon-button');
|
|
expect(scrollButtons).to.have.length(0);
|
|
});
|
|
|
|
// TODO - this fails sporadically, likely due to a timing issue. It tests fine manually.
|
|
it.skip('does not show scroll buttons if placement is start', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`<sl-tab-group> ${generateTabs(50)} </sl-tab-group>`);
|
|
tabGroup.placement = 'start';
|
|
|
|
await aTimeout(0);
|
|
|
|
const scrollButtons = tabGroup.shadowRoot?.querySelectorAll('sl-icon-button');
|
|
expect(scrollButtons).to.have.length(0);
|
|
});
|
|
|
|
// TODO - this fails sporadically, likely due to a timing issue. It tests fine manually.
|
|
it.skip('does not show scroll buttons if placement is end', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`<sl-tab-group> ${generateTabs(50)} </sl-tab-group>`);
|
|
tabGroup.placement = 'end';
|
|
|
|
await aTimeout(0);
|
|
|
|
const scrollButtons = tabGroup.shadowRoot?.querySelectorAll('sl-icon-button');
|
|
expect(scrollButtons).to.have.length(0);
|
|
});
|
|
|
|
// TODO - this fails sporadically, likely due to a timing issue. It tests fine manually.
|
|
it.skip('does scroll on scroll button click', async () => {
|
|
const numberOfElements = 15;
|
|
const tabGroup = await fixture<SlTabGroup>(
|
|
html`<sl-tab-group> ${generateTabs(numberOfElements)} </sl-tab-group>`
|
|
);
|
|
|
|
await waitForScrollButtonsToBeRendered(tabGroup);
|
|
const scrollButtons = tabGroup.shadowRoot?.querySelectorAll('sl-icon-button');
|
|
expect(scrollButtons).to.have.length(2);
|
|
|
|
const firstTab = tabGroup.querySelector('[panel="tab-0"]');
|
|
expect(firstTab).not.to.be.null;
|
|
const lastTab = tabGroup.querySelector(`[panel="tab-${numberOfElements - 1}"]`);
|
|
expect(lastTab).not.to.be.null;
|
|
expect(isElementVisibleFromOverflow(tabGroup, firstTab!)).to.be.true;
|
|
expect(isElementVisibleFromOverflow(tabGroup, lastTab!)).to.be.false;
|
|
|
|
const scrollToRightButton = tabGroup.shadowRoot?.querySelector('sl-icon-button[part*="scroll-button--end"]');
|
|
expect(scrollToRightButton).not.to.be.null;
|
|
await clickOnElement(scrollToRightButton!);
|
|
|
|
await elementUpdated(tabGroup);
|
|
await waitForScrollingToEnd(firstTab!);
|
|
await waitForScrollingToEnd(lastTab!);
|
|
|
|
expect(isElementVisibleFromOverflow(tabGroup, firstTab!)).to.be.false;
|
|
expect(isElementVisibleFromOverflow(tabGroup, lastTab!)).to.be.true;
|
|
});
|
|
});
|
|
|
|
describe('tab selection', () => {
|
|
const expectCustomTabToBeActiveAfter = async (tabGroup: SlTabGroup, action: () => Promise<void>): Promise<void> => {
|
|
const generalHeader = await waitForHeaderToBeActive(tabGroup, 'general-header');
|
|
generalHeader.focus();
|
|
|
|
const customHeader = queryByTestId<SlTab>(tabGroup, 'custom-header');
|
|
expect(customHeader).not.to.have.attribute('active');
|
|
|
|
const showEventPromise = oneEvent(tabGroup, 'sl-tab-show') as Promise<SlTabShowEvent>;
|
|
await action();
|
|
|
|
expect(customHeader).to.have.attribute('active');
|
|
await expectPromiseToHaveName(showEventPromise, 'custom');
|
|
return expectOnlyOneTabPanelToBeActive(tabGroup, 'custom-tab-content');
|
|
};
|
|
|
|
const expectGeneralTabToBeStillActiveAfter = async (
|
|
tabGroup: SlTabGroup,
|
|
action: () => Promise<void>
|
|
): Promise<void> => {
|
|
const generalHeader = await waitForHeaderToBeActive(tabGroup, 'general-header');
|
|
generalHeader.focus();
|
|
|
|
let showEventFired = false;
|
|
let hideEventFired = false;
|
|
oneEvent(tabGroup, 'sl-tab-show').then(() => (showEventFired = true));
|
|
oneEvent(tabGroup, 'sl-tab-hide').then(() => (hideEventFired = true));
|
|
await action();
|
|
|
|
expect(generalHeader).to.have.attribute('active');
|
|
expect(showEventFired).to.be.false;
|
|
expect(hideEventFired).to.be.false;
|
|
return expectOnlyOneTabPanelToBeActive(tabGroup, 'general-tab-content');
|
|
};
|
|
|
|
it('selects a tab by clicking on it', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general" data-testid="general-header">General</sl-tab>
|
|
<sl-tab slot="nav" panel="custom" data-testid="custom-header">Custom</sl-tab>
|
|
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
|
<sl-tab-panel name="custom" data-testid="custom-tab-content">This is the custom tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
|
|
const customHeader = queryByTestId<SlTab>(tabGroup, 'custom-header');
|
|
return expectCustomTabToBeActiveAfter(tabGroup, () => clickOnElement(customHeader!));
|
|
});
|
|
|
|
it('does not change if the active tab is reselected', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general" data-testid="general-header">General</sl-tab>
|
|
<sl-tab slot="nav" panel="custom">Custom</sl-tab>
|
|
<sl-tab-panel name="general" data-testid="general-tab-content">This is the general tab panel.</sl-tab-panel>
|
|
<sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
|
|
const generalHeader = queryByTestId(tabGroup, 'general-header');
|
|
return expectGeneralTabToBeStillActiveAfter(tabGroup, () => clickOnElement(generalHeader!));
|
|
});
|
|
|
|
it('does not change if a disabled tab is clicked', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general" data-testid="general-header">General</sl-tab>
|
|
<sl-tab slot="nav" panel="disabled" data-testid="disabled-header" disabled>disabled</sl-tab>
|
|
<sl-tab-panel name="general" data-testid="general-tab-content">This is the general tab panel.</sl-tab-panel>
|
|
<sl-tab-panel name="disabled">This is the disabled tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
|
|
const disabledHeader = queryByTestId(tabGroup, 'disabled-header');
|
|
return expectGeneralTabToBeStillActiveAfter(tabGroup, () => clickOnElement(disabledHeader!));
|
|
});
|
|
|
|
it('selects a tab by using the arrow keys', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general" data-testid="general-header">General</sl-tab>
|
|
<sl-tab slot="nav" panel="custom" data-testid="custom-header">Custom</sl-tab>
|
|
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
|
<sl-tab-panel name="custom" data-testid="custom-tab-content">This is the custom tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
|
|
return expectCustomTabToBeActiveAfter(tabGroup, () => sendKeys({ press: 'ArrowRight' }));
|
|
});
|
|
|
|
it('selects a tab by using the arrow keys and enter if activation is set to manual', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general" data-testid="general-header">General</sl-tab>
|
|
<sl-tab slot="nav" panel="custom" data-testid="custom-header">Custom</sl-tab>
|
|
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
|
<sl-tab-panel name="custom" data-testid="custom-tab-content">This is the custom tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
tabGroup.activation = 'manual';
|
|
|
|
const generalHeader = await waitForHeaderToBeActive(tabGroup, 'general-header');
|
|
generalHeader.focus();
|
|
|
|
const customHeader = queryByTestId<SlTab>(tabGroup, 'custom-header');
|
|
expect(customHeader).not.to.have.attribute('active');
|
|
|
|
const showEventPromise = oneEvent(tabGroup, 'sl-tab-show') as Promise<SlTabShowEvent>;
|
|
await sendKeys({ press: 'ArrowRight' });
|
|
await aTimeout(0);
|
|
expect(generalHeader).to.have.attribute('active');
|
|
|
|
await sendKeys({ press: 'Enter' });
|
|
|
|
expect(customHeader).to.have.attribute('active');
|
|
await expectPromiseToHaveName(showEventPromise, 'custom');
|
|
return expectOnlyOneTabPanelToBeActive(tabGroup, 'custom-tab-content');
|
|
});
|
|
|
|
it('does not allow selection of disabled tabs with arrow keys', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general" data-testid="general-header">General</sl-tab>
|
|
<sl-tab slot="nav" panel="disabled" disabled>Disabled</sl-tab>
|
|
<sl-tab-panel name="general" data-testid="general-tab-content">This is the general tab panel.</sl-tab-panel>
|
|
<sl-tab-panel name="disabled">This is the custom tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
|
|
return expectGeneralTabToBeStillActiveAfter(tabGroup, () => sendKeys({ press: 'ArrowRight' }));
|
|
});
|
|
|
|
it('selects a tab by using the show function', async () => {
|
|
const tabGroup = await fixture<SlTabGroup>(html`
|
|
<sl-tab-group>
|
|
<sl-tab slot="nav" panel="general" data-testid="general-header">General</sl-tab>
|
|
<sl-tab slot="nav" panel="custom" data-testid="custom-header">Custom</sl-tab>
|
|
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
|
<sl-tab-panel name="custom" data-testid="custom-tab-content">This is the custom tab panel.</sl-tab-panel>
|
|
</sl-tab-group>
|
|
`);
|
|
|
|
return expectCustomTabToBeActiveAfter(tabGroup, () => {
|
|
tabGroup.show('custom');
|
|
return aTimeout(0);
|
|
});
|
|
});
|
|
});
|
|
});
|