Skip to content

Commit

Permalink
feat(tabs): make component responsive (#8616)
Browse files Browse the repository at this point in the history
**Related Issue:** #6689 

## Summary

This improves how `tabs` works at narrow widths where `tab-title`s can't
be displayed entirely.

### Notable changes

* overflowing `tab-title`s will be clipped
* affordances to scroll clipped `tab-titles` forward/backward will be
displayed accordingly
* selecting a `tab-title` that is partially clipped will be scrolled
into view
* `tab-title`s can be scrolled via wheel or drag

See [design
spec](https://www.figma.com/file/JAINbGHSykI8JQCVzOHiSz/Tabs---responsive-design-%5B6689%5D?node-id=862%3A13837&mode=dev)
for more details.

---------

Co-authored-by: eliza <eli97736@esri.com>
  • Loading branch information
jcfranco and Elijbet authored Feb 27, 2024
1 parent 19deb75 commit 83a2ef4
Show file tree
Hide file tree
Showing 10 changed files with 883 additions and 181 deletions.
18 changes: 18 additions & 0 deletions packages/calcite-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { StepperItemChangeEventDetail, StepperItemEventDetail, StepperItemKeyEve
import { StepperMessages } from "./components/stepper/assets/stepper/t9n";
import { StepperItemMessages } from "./components/stepper-item/assets/stepper-item/t9n";
import { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces";
import { TabNavMessages } from "./components/tab-nav/assets/tab-nav/t9n";
import { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces";
import { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n";
import { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
Expand Down Expand Up @@ -163,6 +164,7 @@ export { StepperItemChangeEventDetail, StepperItemEventDetail, StepperItemKeyEve
export { StepperMessages } from "./components/stepper/assets/stepper/t9n";
export { StepperItemMessages } from "./components/stepper-item/assets/stepper-item/t9n";
export { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces";
export { TabNavMessages } from "./components/tab-nav/assets/tab-nav/t9n";
export { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces";
export { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n";
export { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
Expand Down Expand Up @@ -4591,6 +4593,14 @@ export namespace Components {
"indicatorOffset": number;
"indicatorWidth": number;
"layout": TabLayout;
/**
* Use this property to override individual strings used by the component.
*/
"messageOverrides": Partial<TabNavMessages>;
/**
* Made into a prop for testing purposes only.
*/
"messages": TabNavMessages;
/**
* Specifies the position of `calcite-tab-nav` and `calcite-tab-title` components in relation to, and is inherited from the parent `calcite-tabs`, defaults to `top`.
*/
Expand Down Expand Up @@ -12165,6 +12175,14 @@ declare namespace LocalJSX {
"indicatorOffset"?: number;
"indicatorWidth"?: number;
"layout"?: TabLayout;
/**
* Use this property to override individual strings used by the component.
*/
"messageOverrides"?: Partial<TabNavMessages>;
/**
* Made into a prop for testing purposes only.
*/
"messages"?: TabNavMessages;
"onCalciteInternalTabChange"?: (event: CalciteTabNavCustomEvent<TabChangeEventDetail>) => void;
/**
* Emits when the selected `calcite-tab` changes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,8 +648,7 @@ describe("calcite-button", () => {
t9n("calcite-button");
});

describe('automatic tooltip', ()=>{

describe("automatic tooltip", () => {
it("shows tooltip for buttons with truncated long text", async () => {
const shortText = "Hi!";
const longText =
Expand Down Expand Up @@ -685,7 +684,6 @@ describe("calcite-button", () => {

expect(button).not.toHaveAttribute("title");
});

});

it("should set aria-expanded attribute on shadowDOM element when used as trigger", async () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/calcite-components/src/components/tab-nav/resources.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
export const ICON = {
chevronRight: "chevron-right",
chevronLeft: "chevron-left",
};

export const CSS = {
activeIndicatorContainer: "tab-nav-active-indicator-container",
container: "tab-nav",
containerHasEndTabTitleOverflow: "tab-nav--end-overflow",
containerHasStartTabTitleOverflow: "tab-nav--start-overflow",
scrollButton: "scroll-button",
scrollButtonContainer: "scroll-button-container",
scrollBackwardContainerButton: "scroll-button-container--backward",
scrollForwardContainerButton: "scroll-button-container--forward",
tabTitleSlotWrapper: "tab-titles-slot-wrapper",
};
175 changes: 173 additions & 2 deletions packages/calcite-components/src/components/tab-nav/tab-nav.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { newE2EPage } from "@stencil/core/testing";
import { accessible, defaults, renders, hidden } from "../../tests/commonTests";
import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing";
import { accessible, defaults, hidden, renders, t9n } from "../../tests/commonTests";
import { html } from "../../../support/formatting";
import { CSS } from "./resources";
import { getElementRect } from "../../tests/utils";

describe("calcite-tab-nav", () => {
describe("defaults", () => {
Expand All @@ -19,6 +21,10 @@ describe("calcite-tab-nav", () => {
accessible("calcite-tab-nav");
});

describe("translation support", () => {
t9n("calcite-tab-nav");
});

it("emits on user interaction", async () => {
const page = await newE2EPage();
await page.setContent(
Expand Down Expand Up @@ -110,4 +116,169 @@ describe("calcite-tab-nav", () => {
await page.keyboard.press("Home");
expect(await page.evaluate(() => document.activeElement.id)).toBe("tab1");
});

describe("responsiveness", () => {
const tabsHTML = html`
<calcite-tabs>
<calcite-tab-nav slot="title-group">
<calcite-tab-title selected>Tab 1 Title</calcite-tab-title>
<calcite-tab-title>Tab 2 Title</calcite-tab-title>
<calcite-tab-title>Tab 3 Title</calcite-tab-title>
<calcite-tab-title>Tab 4 Title</calcite-tab-title>
<calcite-tab-title>Tab 5 Title</calcite-tab-title>
<calcite-tab-title>Tab 6 Title</calcite-tab-title>
<calcite-tab-title>Tab 7 Title</calcite-tab-title>
<calcite-tab-title>Tab 8 Title</calcite-tab-title>
</calcite-tab-nav>
<calcite-tab selected>Tab 1 Content</calcite-tab>
<calcite-tab>Tab 2 Content</calcite-tab>
<calcite-tab>Tab 3 Content</calcite-tab>
<calcite-tab>Tab 4 Content</calcite-tab>
<calcite-tab>Tab 5 Content</calcite-tab>
<calcite-tab>Tab 6 Content</calcite-tab>
<calcite-tab>Tab 7 Content</calcite-tab>
<calcite-tab>Tab 8 Content</calcite-tab>
</calcite-tabs>
`;
const sizeShowingAllTabs = { width: 1200, height: 1200 };
const sizeShowingSomeTabs = { width: 350, height: 1200 };

let page: E2EPage;
let scrollBackButton: E2EElement;
let scrollForwardButton: E2EElement;
let scrollContainer: E2EElement;

async function assertScrollButtonVisibility(
backExpectedVisibility: boolean,
expectedForwardVisibility: boolean,
): Promise<void> {
/* we need to find the scroll buttons to ensure visibility */
expect(await scrollBackButton.isVisible()).toBe(backExpectedVisibility);
expect(await scrollForwardButton.isVisible()).toBe(expectedForwardVisibility);
}

beforeEach(async () => {
page = await newE2EPage();
await page.setContent(tabsHTML);
await page.setViewport(sizeShowingSomeTabs);
await page.waitForChanges();
scrollBackButton = await page.find(`calcite-tab-nav >>> .${CSS.scrollBackwardContainerButton}`);
scrollForwardButton = await page.find(`calcite-tab-nav >>> .${CSS.scrollForwardContainerButton}`);
scrollContainer = await page.find(`calcite-tab-nav >>> .${CSS.tabTitleSlotWrapper}`);
});

it("shows scrolling buttons if tab-titles overflow", async () => {
await assertScrollButtonVisibility(false, true);

await page.click("calcite-tab-title:nth-child(4)");
await page.waitForChanges();

await assertScrollButtonVisibility(true, true);

await page.setViewport(sizeShowingAllTabs);
await page.waitForChanges();

await assertScrollButtonVisibility(false, false);

await page.setViewport(sizeShowingSomeTabs);
await page.waitForChanges();

await assertScrollButtonVisibility(false, true);

await page.click("calcite-tab-title:nth-child(4)");
await page.waitForChanges();

await assertScrollButtonVisibility(true, true);
});

it("scrolling tabs via buttons", async () => {
await assertScrollButtonVisibility(false, true);

let scrollEnd = scrollContainer.waitForEvent("scrollend");
await scrollForwardButton.click();
await page.waitForChanges();
await scrollEnd;

await assertScrollButtonVisibility(true, true);

scrollEnd = scrollContainer.waitForEvent("scrollend");
await scrollForwardButton.click();
await page.waitForChanges();
await scrollEnd;

await assertScrollButtonVisibility(true, false);

scrollEnd = scrollContainer.waitForEvent("scrollend");
await scrollBackButton.click();
await page.waitForChanges();
await scrollEnd;

await assertScrollButtonVisibility(true, true);

scrollEnd = scrollContainer.waitForEvent("scrollend");
await scrollBackButton.click();
await page.waitForChanges();
await scrollEnd;

await assertScrollButtonVisibility(false, true);
});

it("scrolling tabs via mouse wheel", async () => {
await assertScrollButtonVisibility(false, true);

const tabNavBounds = await getElementRect(page, "calcite-tab-nav");
await page.mouse.move(tabNavBounds.x + tabNavBounds.width / 2, tabNavBounds.y + tabNavBounds.height / 2);
await page.mouse.wheel({ deltaY: 200 });
await page.waitForChanges();

await assertScrollButtonVisibility(true, true);

await page.mouse.wheel({ deltaY: 200 });
await page.waitForChanges();

await assertScrollButtonVisibility(true, false);

await page.mouse.wheel({ deltaY: -200 });
await page.waitForChanges();

await assertScrollButtonVisibility(true, true);

await page.mouse.wheel({ deltaY: -200 });
await page.waitForChanges();

await assertScrollButtonVisibility(false, true);
});

it("scrolls into view clipped start or end tab-title when selected", async () => {
const tabNavBounds = await getElementRect(page, "calcite-tab-nav");
await page.mouse.move(tabNavBounds.x + tabNavBounds.width / 2, tabNavBounds.y + tabNavBounds.height / 2);
await page.waitForChanges();

await page.mouse.wheel({ deltaY: 1 });
await page.waitForChanges();

await assertScrollButtonVisibility(true, true);

let scrollEnd = scrollContainer.waitForEvent("scrollend");
const firstTab = await page.find("calcite-tab-title:first-child");
await firstTab.callMethod("click"); // we call method to avoid having E2E click element in the middle, which would hit the scroll button
await page.waitForChanges();
await scrollEnd;

await assertScrollButtonVisibility(false, true);

await page.mouse.wheel({ deltaY: 180 });
await page.waitForChanges();

await assertScrollButtonVisibility(true, true);

scrollEnd = scrollContainer.waitForEvent("scrollend");
const lastTab = await page.find("calcite-tab-title:last-child");
await lastTab.callMethod("click"); // we call method to avoid having E2E click element in the middle, which would hit the scroll button
await page.waitForChanges();
await scrollEnd;

await assertScrollButtonVisibility(true, false);
});
});
});
Loading

0 comments on commit 83a2ef4

Please sign in to comment.