diff --git a/packages/calcite-components/src/components/tab-nav/readme.md b/packages/calcite-components/src/components/tab-nav/readme.md index 58098465d28..e46d1f01980 100644 --- a/packages/calcite-components/src/components/tab-nav/readme.md +++ b/packages/calcite-components/src/components/tab-nav/readme.md @@ -8,14 +8,16 @@ The tab-nav groups several [calcite-tab-title](../tab-title) components and buil ### Basic -When tab-nav is the only parent, tab-title can inherit its `scale` and `position` from tab-nav: +Tab-nav and tab-title inherit their `scale` and `position` from tab-title parent: ```html - - Layers - Maps - Data - + + + Layers + Maps + Data + + ``` ## Properties diff --git a/packages/calcite-components/src/components/tab-nav/resources.ts b/packages/calcite-components/src/components/tab-nav/resources.ts new file mode 100644 index 00000000000..7f1cc8d7095 --- /dev/null +++ b/packages/calcite-components/src/components/tab-nav/resources.ts @@ -0,0 +1,3 @@ +export const CSS = { + container: "tab-nav", +}; diff --git a/packages/calcite-components/src/components/tab-nav/tab-nav.e2e.ts b/packages/calcite-components/src/components/tab-nav/tab-nav.e2e.ts index 72c8a967f62..01ffed7f55f 100644 --- a/packages/calcite-components/src/components/tab-nav/tab-nav.e2e.ts +++ b/packages/calcite-components/src/components/tab-nav/tab-nav.e2e.ts @@ -1,12 +1,14 @@ import { newE2EPage } from "@stencil/core/testing"; -import { accessible, renders, hidden } from "../../tests/commonTests"; +import { accessible, defaults, renders, hidden } from "../../tests/commonTests"; import { html } from "../../../support/formatting"; describe("calcite-tab-nav", () => { - const tabNavHtml = ""; + describe("defaults", () => { + defaults("calcite-tab-nav", [{ propertyName: "scale", defaultValue: "m" }]); + }); describe("renders", () => { - renders(tabNavHtml, { display: "flex" }); + renders("calcite-tab-nav", { display: "flex" }); }); describe("honors hidden attribute", () => { @@ -14,7 +16,7 @@ describe("calcite-tab-nav", () => { }); describe("accessible: checked", () => { - accessible(tabNavHtml); + accessible("calcite-tab-nav"); }); it("emits on user interaction", async () => { @@ -78,96 +80,6 @@ describe("calcite-tab-nav", () => { }); }); - describe("scale property", () => { - describe("default", () => { - it("should render without scale", async () => { - const page = await newE2EPage({ - html: `${tabNavHtml}`, - }); - const element = await page.find("calcite-tab-nav"); - expect(element).not.toHaveAttribute("scale"); - }); - }); - - describe("when scale is small", () => { - it("should render with small scale", async () => { - const page = await newE2EPage({ - html: ` - Tab 1 Title - Tab 2 Title - Tab 3 Title - Tab 4 Title - `, - }); - const element = await page.find("calcite-tab-nav"); - expect(await (await element.getComputedStyle())["minHeight"]).toEqual("24px"); - expect(element).toEqualAttribute("scale", "s"); - }); - }); - - describe("when scale is medium", () => { - it("should render with medium scale", async () => { - const page = await newE2EPage({ - html: ` - Tab 1 Title - Tab 2 Title - Tab 3 Title - Tab 4 Title - `, - }); - const element = await page.find("calcite-tab-nav"); - expect(await (await element.getComputedStyle())["minHeight"]).toEqual("32px"); - expect(element).toEqualAttribute("scale", "m"); - }); - }); - - describe("when scale is large", () => { - it("should render with medium scale", async () => { - const page = await newE2EPage({ - html: ` - Tab 1 Title - Tab 2 Title - Tab 3 Title - Tab 4 Title - `, - }); - const element = await page.find("calcite-tab-nav"); - expect(await (await element.getComputedStyle())["minHeight"]).toEqual("44px"); - expect(element).toEqualAttribute("scale", "l"); - }); - }); - - describe("when nested within tabs parent", () => { - it("should render with default medium scale", async () => { - const page = await newE2EPage({ - html: `${tabNavHtml}`, - }); - const element = await page.find("calcite-tab-nav"); - expect(element).toEqualAttribute("scale", "m"); - }); - - describe("when tabs scale is small", () => { - it("should render with small scale", async () => { - const page = await newE2EPage({ - html: `${tabNavHtml}`, - }); - const element = await page.find("calcite-tab-nav"); - expect(element).toEqualAttribute("scale", "s"); - }); - }); - - describe("when tabs scale is large", () => { - it("should render with large scale", async () => { - const page = await newE2EPage({ - html: `${tabNavHtml}`, - }); - const element = await page.find("calcite-tab-nav"); - expect(element).toEqualAttribute("scale", "l"); - }); - }); - }); - }); - it("focuses on keyboard interaction", async () => { const page = await newE2EPage(); await page.setContent(html` diff --git a/packages/calcite-components/src/components/tab-nav/tab-nav.scss b/packages/calcite-components/src/components/tab-nav/tab-nav.scss index b5d60b51fcb..368fd06a1d1 100644 --- a/packages/calcite-components/src/components/tab-nav/tab-nav.scss +++ b/packages/calcite-components/src/components/tab-nav/tab-nav.scss @@ -2,18 +2,48 @@ @apply relative flex; } -:host([scale="s"]) { +.scale-s { min-block-size: theme("spacing.6"); } -:host([scale="m"]) { +.scale-m { min-block-size: theme("spacing.8"); } -:host([scale="l"]) { +.scale-l { min-block-size: theme("spacing.11"); } +:host([layout="center"]:not([bordered])) { + // `tab-nav` in all scales in layout="center" has a padding of 20px on both ends + padding-inline: theme("margin.5"); + + //override margin-inline-end for the last child for tab-nav to implement the 20px padding on both ends instead + .tab-nav { + ::slotted(calcite-tab-title:last-child) { + margin-inline-end: theme("margin.0"); + } + } +} + +:host(:not([bordered])) { + .scale-l { + ::slotted(calcite-tab-title) { + margin-inline-end: theme("margin.6"); + } + } + .scale-m { + ::slotted(calcite-tab-title) { + margin-inline-end: theme("margin.5"); + } + } + .scale-s { + ::slotted(calcite-tab-title) { + margin-inline-end: theme("margin.4"); + } + } +} + .tab-nav { @apply flex w-full @@ -45,12 +75,12 @@ @apply justify-evenly; } -:host([position="bottom"]) .tab-nav-active-indicator { +:host .position-bottom .tab-nav-active-indicator { inset-block-end: unset; @apply top-0; } -:host([position="bottom"]) .tab-nav-active-indicator-container { +:host .position-bottom .tab-nav-active-indicator-container { inset-block-end: unset; @apply top-0; } @@ -59,7 +89,7 @@ inset-block-end: unset; // display active blue line above instead of below } -:host([bordered][position="bottom"]) .tab-nav-active-indicator-container { +:host([bordered]) .position-bottom .tab-nav-active-indicator-container { inset-block-end: 0; // display active blue line below instead of above inset-block-start: unset; } diff --git a/packages/calcite-components/src/components/tab-nav/tab-nav.tsx b/packages/calcite-components/src/components/tab-nav/tab-nav.tsx index 9f2199d5dd1..3ae7c6ab52a 100644 --- a/packages/calcite-components/src/components/tab-nav/tab-nav.tsx +++ b/packages/calcite-components/src/components/tab-nav/tab-nav.tsx @@ -21,6 +21,7 @@ import { createObserver } from "../../utils/observers"; import { Scale } from "../interfaces"; import { TabChangeEventDetail, TabCloseEventDetail } from "../tab/interfaces"; import { TabID, TabLayout, TabPosition } from "../tabs/interfaces"; +import { CSS } from "./resources"; /** * @slot - A slot for adding `calcite-tab-title`s. @@ -55,9 +56,11 @@ export class TabNav { @Prop({ mutable: true }) selectedTitle: HTMLCalciteTabTitleElement = null; /** + * Specifies the size of the component inherited from the parent `calcite-tabs`, defaults to `m`. + * * @internal */ - @Prop({ reflect: true, mutable: true }) scale: Scale = "m"; + @Prop() scale: Scale = "m"; /** * @internal @@ -65,9 +68,11 @@ export class TabNav { @Prop({ reflect: true, mutable: true }) layout: TabLayout = "inline"; /** - * @internal + * 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`. + * + * @internal */ - @Prop({ reflect: true, mutable: true }) position: TabPosition = "bottom"; + @Prop() position: TabPosition = "bottom"; /** * @internal @@ -121,10 +126,6 @@ export class TabNav { this.resizeObserver?.observe(this.el); } - disconnectedCallback(): void { - this.resizeObserver?.disconnect(); - } - componentWillLoad(): void { const storageKey = `calcite-tab-nav-${this.storageId}`; if (localStorage && this.storageId && localStorage.getItem(storageKey)) { @@ -137,8 +138,6 @@ export class TabNav { const { parentTabsEl } = this; this.layout = parentTabsEl?.layout; - this.position = parentTabsEl?.position; - this.scale = parentTabsEl?.scale; this.bordered = parentTabsEl?.bordered; // fix issue with active tab-title not lining up with blue indicator if (this.selectedTitle) { @@ -161,6 +160,10 @@ export class TabNav { } } + disconnectedCallback(): void { + this.resizeObserver?.disconnect(); + } + render(): VNode { const dir = getElementDir(this.el); const width = `${this.indicatorWidth}px`; @@ -169,7 +172,11 @@ export class TabNav { return (
(this.tabNavEl = el)} diff --git a/packages/calcite-components/src/components/tab-nav/usage/Basic.md b/packages/calcite-components/src/components/tab-nav/usage/Basic.md index 6d4a68d2356..7cf407af314 100644 --- a/packages/calcite-components/src/components/tab-nav/usage/Basic.md +++ b/packages/calcite-components/src/components/tab-nav/usage/Basic.md @@ -1,9 +1,11 @@ -When tab-nav is the only parent, tab-title can inherit its `scale` and `position` from tab-nav: +Tabs `scale` and `position` properties are inherited by it's child components, tab-nav and tab-title. ```html - - Layers - Maps - Data - + + + Layers + Maps + Data + + ``` diff --git a/packages/calcite-components/src/components/tab-title/resources.ts b/packages/calcite-components/src/components/tab-title/resources.ts index 477cf6c8ca4..d03df24cee7 100644 --- a/packages/calcite-components/src/components/tab-title/resources.ts +++ b/packages/calcite-components/src/components/tab-title/resources.ts @@ -4,8 +4,8 @@ export const CSS = { content: "content", contentHasText: "content--has-text", iconEnd: "icon-end", - iconStart: "icon-start", iconPresent: "icon-present", + iconStart: "icon-start", titleIcon: "calcite-tab-title--icon", }; diff --git a/packages/calcite-components/src/components/tab-title/tab-title.e2e.ts b/packages/calcite-components/src/components/tab-title/tab-title.e2e.ts index e220ccdb174..ebe7faf7f79 100644 --- a/packages/calcite-components/src/components/tab-title/tab-title.e2e.ts +++ b/packages/calcite-components/src/components/tab-title/tab-title.e2e.ts @@ -4,8 +4,8 @@ import { html } from "../../../support/formatting"; import { CSS } from "./resources"; describe("calcite-tab-title", () => { - const tabTitleHtml = ""; const tabTitleClosableHtml = ""; + const multiTabTitleClosableMarkup = ` @@ -41,7 +41,7 @@ describe("calcite-tab-title", () => { const closeSelector = `calcite-tab-title >>> .${CSS.closeButton}`; describe("renders", () => { - renders(tabTitleHtml, { display: "block" }); + renders("calcite-tab-title", { display: "block" }); renders(multiTabTitleClosableMarkup, { display: "flex" }); }); @@ -108,10 +108,18 @@ describe("calcite-tab-title", () => { page = await newE2EPage(); await page.setContent( html` - + + + Tab 1 Title + Tab 2 Title + Tab 3 Title + + Tab 1 Content + Tab 2 Content + Tab 3 Content Text Text - + ` ); @@ -338,155 +346,6 @@ describe("calcite-tab-title", () => { expect(activeEventSpy).toHaveReceivedEventTimes(2); }); - describe("when parent element is tab-nav", () => { - describe("when position is top, default", () => { - it("should render with bottom border on hover", async () => { - const page = await newE2EPage({ - html: ` - - Tab 1 title - - `, - }); - const element = await page.find("#for-hover"); - await element.hover(); - - const container = await page.find("#for-hover >>> .container"); - const containerStyles = await container.getComputedStyle(); - expect(containerStyles["border-top-width"]).toEqual("0px"); - expect(containerStyles["border-bottom-width"]).not.toEqual("0px"); - }); - }); - - describe("when position is bottom", () => { - it("should render with top border on hover", async () => { - const page = await newE2EPage({ - html: ` - - Tab 1 Title - Tab 2 Title - - `, - }); - const element = await page.find("#for-hover"); - await element.hover(); - - const container = await page.find("#for-hover >>> .container"); - const containerStyles = await container.getComputedStyle(); - expect(containerStyles["border-top-width"]).not.toEqual("0px"); - expect(containerStyles["border-bottom-width"]).toEqual("0px"); - }); - }); - - describe("scale property", () => { - let page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - }); - - it("should inherit default medium scale from tab-nav", async () => { - await page.setContent( - html` - - Tab Title - Tab 2 Title - - ` - ); - - const tabTitleNav = await page.find("calcite-tab-nav"); - const tabTitleEl = await page.find("calcite-tab-title"); - const content = await page.find(`calcite-tab-title >>> .${CSS.content}`); - const contentStyles = await content.getComputedStyle(); - - expect(tabTitleEl).toEqualAttribute("scale", "m"); - expect(tabTitleNav).toEqualAttribute("scale", "m"); - expect(contentStyles.fontSize).toEqual("14px"); - expect(contentStyles.lineHeight).toEqual("16px"); - }); - - it("should inherit small scale from tab-nav", async () => { - await page.setContent( - html` - - Tab Title - Tab 2 Title - - ` - ); - - const tabTitleNav = await page.find("calcite-tab-nav"); - const tabTitleEl = await page.find("calcite-tab-title"); - const content = await page.find(`calcite-tab-title >>> .${CSS.content}`); - const contentStyles = await content.getComputedStyle(); - - expect(tabTitleEl).toEqualAttribute("scale", "s"); - expect(tabTitleNav).toEqualAttribute("scale", "s"); - expect(contentStyles.fontSize).toEqual("12px"); - expect(contentStyles.lineHeight).toEqual("16px"); - }); - - it("should inherit large scale from tab-nav", async () => { - await page.setContent( - html` - - Tab Title - Tab 2 Title - - ` - ); - - const tabTitleNav = await page.find("calcite-tab-nav"); - const tabTitleEl = await page.find("calcite-tab-title"); - const content = await page.find(`calcite-tab-title >>> .${CSS.content}`); - const contentStyles = await content.getComputedStyle(); - - expect(tabTitleEl).toEqualAttribute("scale", "l"); - expect(tabTitleNav).toEqualAttribute("scale", "l"); - expect(contentStyles.fontSize).toEqual("16px"); - expect(contentStyles.lineHeight).toEqual("20px"); - }); - }); - }); - - describe("when parent element is tabs", () => { - describe("scale property", () => { - it("should inherit default m scale", async () => { - const page = await newE2EPage({ - html: ` - Tab Title - Tab 2 Title - `, - }); - const element = await page.find("calcite-tab-title"); - expect(element).toEqualAttribute("scale", "m"); - }); - - it("should inherit small scale from tabs", async () => { - const page = await newE2EPage({ - html: ` - Tab Title - Tab 2 Title - `, - }); - const element = await page.find("calcite-tab-title"); - expect(element).toEqualAttribute("scale", "s"); - }); - - it("should inherit large scale from tabs", async () => { - const page = await newE2EPage({ - html: ` - Tab 1 Title - Tab 2 Title - `, - }); - const element = await page.find("calcite-tab-title"); - expect(element).toEqualAttribute("scale", "l"); - }); - }); - }); - describe("when the active tab-title changes", () => { it("should move the active tab nav indicator", async () => { const page = await newE2EPage({ diff --git a/packages/calcite-components/src/components/tab-title/tab-title.scss b/packages/calcite-components/src/components/tab-title/tab-title.scss index 275cce426ea..9baf21af777 100644 --- a/packages/calcite-components/src/components/tab-title/tab-title.scss +++ b/packages/calcite-components/src/components/tab-title/tab-title.scss @@ -1,14 +1,20 @@ :host { - @apply block flex-initial outline-none; + @apply block outline-none; margin-inline-start: theme("margin.0"); - margin-inline-end: theme("margin.5"); } -:host([layout="center"][scale="s"]), -:host([layout="center"][scale="m"]), -:host([layout="center"][scale="l"]) { +:host([layout="inline"]) { + @apply flex-initial; +} + +:host([layout="center"]) { + @apply flex-auto; +} + +:host([layout="center"]) .scale-s, +:host([layout="center"]) .scale-m, +:host([layout="center"]) .scale-l { @apply my-0 text-center; - margin-inline-end: theme("margin.0"); flex-basis: theme("spacing.48"); .content { @apply m-auto; @@ -22,25 +28,25 @@ } } -:host([layout="center"][bordered][closable][scale="s"]) { +:host([layout="center"][bordered][closable]) .scale-s { .content { padding-inline-start: 36px; //28px button width + 0.5rem padding } } -:host([layout="center"][bordered][closable][scale="m"]) { +:host([layout="center"][bordered][closable]) .scale-m { .content { padding-inline-start: 40px; //28px button width + 0.75rem padding } } -:host([layout="center"][closable][scale="l"]) { +:host([layout="center"][closable]) .scale-l { .content { padding-inline-start: 40px; //36px button width + .25 padding } } -:host([layout="center"][closable][bordered][scale="l"]) { +:host([layout="center"][closable][bordered]) .scale-s { .content { padding-inline-start: 52px; //36px button width + 1rem padding } @@ -87,21 +93,19 @@ } } -:host([scale="s"]) { - margin-inline-end: 1rem; +.scale-s { .content { @apply text-n2h py-1; } } -:host([scale="m"]) { +.scale-m { .content { @apply text-n1h py-2; } } -:host([scale="l"]) { - margin-inline-end: 1.5rem; +.scale-l { .content { @apply text-0h py-2.5; } @@ -255,13 +259,13 @@ } } -:host([bordered][scale="s"]) { +:host([bordered]) .scale-s { .content { @apply px-2; } } -:host([bordered][scale="l"]) { +:host([bordered]) .scale-l { .content { @apply px-4; } diff --git a/packages/calcite-components/src/components/tab-title/tab-title.tsx b/packages/calcite-components/src/components/tab-title/tab-title.tsx index b5ef893b4cd..89ecddc91e2 100644 --- a/packages/calcite-components/src/components/tab-title/tab-title.tsx +++ b/packages/calcite-components/src/components/tab-title/tab-title.tsx @@ -13,7 +13,7 @@ import { VNode, Watch, } from "@stencil/core"; -import { getElementDir, getElementProp, toAriaBoolean, nodeListToArray } from "../../utils/dom"; +import { getElementDir, toAriaBoolean, nodeListToArray } from "../../utils/dom"; import { guid } from "../../utils/guid"; import { connectInteractive, @@ -95,14 +95,18 @@ export class TabTitle implements InteractiveComponent, LocalizedComponent, T9nCo @Prop({ reflect: true, mutable: true }) layout: TabLayout; /** - * @internal + * 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`. + * + * @internal */ - @Prop({ reflect: true, mutable: true }) position: TabPosition; + @Prop() position: TabPosition = "top"; /** + * Specifies the size of the component inherited from the parent `calcite-tabs`, defaults to `m`. + * * @internal */ - @Prop({ reflect: true, mutable: true }) scale: Scale; + @Prop() scale: Scale = "m"; /** * @internal @@ -177,15 +181,8 @@ export class TabTitle implements InteractiveComponent, LocalizedComponent, T9nCo componentWillRender(): void { if (this.parentTabsEl) { this.layout = this.parentTabsEl.layout; - this.position = this.parentTabsEl.position; - this.scale = this.parentTabsEl.scale; this.bordered = this.parentTabsEl.bordered; } - // handle case when tab-nav is only parent - if (!this.parentTabsEl && this.parentTabNavEl) { - this.position = getElementProp(this.parentTabNavEl, "position", this.position); - this.scale = getElementProp(this.parentTabNavEl, "scale", this.scale); - } } render(): VNode { @@ -222,6 +219,7 @@ export class TabTitle implements InteractiveComponent, LocalizedComponent, T9nCo class={{ container: true, [CSS.iconPresent]: !!this.iconStart || !!this.iconEnd, + [`scale-${this.scale}`]: true, }} hidden={closed} // eslint-disable-next-line react/jsx-sort-props -- ref should be last so node attrs/props are in sync (see https://github.com/Esri/calcite-design-system/pull/6530) diff --git a/packages/calcite-components/src/components/tab/resources.ts b/packages/calcite-components/src/components/tab/resources.ts new file mode 100644 index 00000000000..bf868fd865c --- /dev/null +++ b/packages/calcite-components/src/components/tab/resources.ts @@ -0,0 +1,4 @@ +export const CSS = { + container: "container", + content: "content", +}; diff --git a/packages/calcite-components/src/components/tab/tab.e2e.ts b/packages/calcite-components/src/components/tab/tab.e2e.ts index 655cac5935d..1ff8bf0a3de 100644 --- a/packages/calcite-components/src/components/tab/tab.e2e.ts +++ b/packages/calcite-components/src/components/tab/tab.e2e.ts @@ -1,4 +1,3 @@ -import { newE2EPage } from "@stencil/core/testing"; import { defaults, renders, hidden } from "../../tests/commonTests"; describe("calcite-tab", () => { @@ -18,43 +17,7 @@ describe("calcite-tab", () => { defaults("calcite-tab", [ { propertyName: "tab", defaultValue: undefined }, { propertyName: "selected", defaultValue: false }, - { propertyName: "scale", defaultValue: undefined }, + { propertyName: "scale", defaultValue: "m" }, ]); }); - - describe("when nested within calcite-tabs component", () => { - it("should render with medium scale", async () => { - const page = await newE2EPage({ - html: `${tabHtml}`, - }); - const element = await page.find("calcite-tab"); - expect(element).toEqualAttribute("scale", "m"); - expect(await (await element.getComputedStyle())["font-size"]).toEqual("14px"); - expect(await (await element.getComputedStyle())["line-height"]).toEqual("16px"); // 1rem - }); - - describe("when tabs scale is small", () => { - it("should render with small scale", async () => { - const page = await newE2EPage({ - html: `${tabHtml}`, - }); - const element = await page.find("calcite-tab"); - expect(element).toEqualAttribute("scale", "s"); - expect(await (await element.getComputedStyle())["font-size"]).toEqual("12px"); - expect(await (await element.getComputedStyle())["line-height"]).toEqual("16px"); // 1rem - }); - }); - - describe("when tabs scale is large", () => { - it("should render with large scale", async () => { - const page = await newE2EPage({ - html: `${tabHtml}`, - }); - const element = await page.find("calcite-tab"); - expect(element).toEqualAttribute("scale", "l"); - expect(await (await element.getComputedStyle())["font-size"]).toEqual("16px"); - expect(await (await element.getComputedStyle())["line-height"]).toEqual("20px"); // 1.25rem - }); - }); - }); }); diff --git a/packages/calcite-components/src/components/tab/tab.scss b/packages/calcite-components/src/components/tab/tab.scss index a7236a5114a..1a794bcdf7b 100644 --- a/packages/calcite-components/src/components/tab/tab.scss +++ b/packages/calcite-components/src/components/tab/tab.scss @@ -13,22 +13,21 @@ @apply block h-full w-full overflow-auto; } -section, -.container { - @apply hidden h-full w-full; -} - -:host([scale="s"]) { +.scale-s .content { @apply text-n2h py-1; } -:host([scale="m"]) { +.scale-m .content { @apply text-n1h py-2; } -:host([scale="l"]) { - @apply text-0h; - padding-block: 13px; +.scale-l .content { + @apply text-0h py-2.5; +} + +section, +.container { + @apply hidden h-full w-full; } @include base-component(); diff --git a/packages/calcite-components/src/components/tab/tab.tsx b/packages/calcite-components/src/components/tab/tab.tsx index adcccd6a986..3f0f456bdbc 100644 --- a/packages/calcite-components/src/components/tab/tab.tsx +++ b/packages/calcite-components/src/components/tab/tab.tsx @@ -11,6 +11,7 @@ import { State, VNode, } from "@stencil/core"; +import { CSS } from "./resources"; import { nodeListToArray } from "../../utils/dom"; import { guid } from "../../utils/guid"; import { Scale } from "../interfaces"; @@ -46,9 +47,11 @@ export class Tab { @Prop({ reflect: true, mutable: true }) selected = false; /** + * Specifies the size of the component inherited from the parent `calcite-tabs`, defaults to `m`. + * * @internal */ - @Prop({ reflect: true, mutable: true }) scale: Scale = "m"; + @Prop() scale: Scale = "m"; //-------------------------------------------------------------------------- // @@ -61,8 +64,12 @@ export class Tab { return ( -
-
+
+
@@ -78,10 +85,6 @@ export class Tab { this.calciteInternalTabRegister.emit(); } - componentWillRender(): void { - this.scale = this.parentTabsEl?.scale; - } - disconnectedCallback(): void { // Dispatching to body in order to be listened by other elements that are still connected to the DOM. document.body?.dispatchEvent( diff --git a/packages/calcite-components/src/components/tabs/tabs.e2e.ts b/packages/calcite-components/src/components/tabs/tabs.e2e.ts index 8dbe5ff3f59..ef294223871 100644 --- a/packages/calcite-components/src/components/tabs/tabs.e2e.ts +++ b/packages/calcite-components/src/components/tabs/tabs.e2e.ts @@ -1,10 +1,12 @@ import { newE2EPage } from "@stencil/core/testing"; import { html } from "../../../support/formatting"; -import { accessible, defaults, hidden, renders } from "../../tests/commonTests"; +import { accessible, defaults, hidden, reflects, renders } from "../../tests/commonTests"; import { GlobalTestProps } from "../../tests/utils"; +import { Scale } from "../interfaces"; +import { TabPosition } from "../tabs/interfaces"; describe("calcite-tabs", () => { - const tabsContent = ` + const tabsContent = html` Tab 1 Title Tab 2 Title @@ -16,7 +18,7 @@ describe("calcite-tabs", () => { Tab 3 Content Tab 4 Content `; - const tabsSnippet = `${tabsContent}`; + const tabsSnippet = html`${tabsContent}`; describe("renders", () => { renders(tabsSnippet, { display: "flex" }); @@ -34,6 +36,14 @@ describe("calcite-tabs", () => { ]); }); + describe("reflects", () => { + reflects("calcite-tabs", [ + { propertyName: "layout", value: "inline" }, + { propertyName: "position", value: "top" }, + { propertyName: "scale", value: "m" }, + ]); + }); + describe("accessible: checked", () => { accessible(`${tabsContent}`); }); @@ -117,47 +127,37 @@ describe("calcite-tabs", () => { } }); - describe("when no scale is provided", () => { - it("should render itself and child tab elements with default medium scale", async () => { - const page = await newE2EPage({ - html: `${tabsContent}`, - }); - expect(await page.find("calcite-tabs")).toEqualAttribute("scale", "m"); - expect(await page.find("calcite-tab-nav")).toEqualAttribute("scale", "m"); - expect(await page.find("calcite-tab-title")).toEqualAttribute("scale", "m"); - expect(await page.find("calcite-tab")).toEqualAttribute("scale", "m"); - }); - }); - - describe("when scale is provided", () => { - it("should render itself and child tab elements with corresponding scale (small)", async () => { - const page = await newE2EPage({ - html: `${tabsContent}`, - }); - expect(await page.find("calcite-tabs")).toEqualAttribute("scale", "s"); - expect(await page.find("calcite-tab-nav")).toEqualAttribute("scale", "s"); - expect(await page.find("calcite-tab-title")).toEqualAttribute("scale", "s"); - expect(await page.find("calcite-tab")).toEqualAttribute("scale", "s"); + function testTabsScaleAndPosition(scale: Scale, position: TabPosition) { + const scaleName = scale === "m" ? "default medium" : scale; + + it(`should render itself and child tab elements with corresponding scale (${scaleName}) and position (${position})`, async () => { + const page = await newE2EPage(); + await page.setContent(html`${tabsContent}`); + await page.waitForChanges(); + + const tabs = await page.find("calcite-tabs"); + const tab = await page.find("calcite-tab"); + const tabTitle = await page.find("calcite-tab-title"); + const tabNav = await page.find("calcite-tab-nav"); + + expect(await tabs.getProperty("scale")).toBe(scale); + expect(await tabs.getProperty("position")).toBe(position); + expect(await tabNav.getProperty("scale")).toBe(scale); + expect(await tabNav.getProperty("position")).toBe(position); + expect(await tabTitle.getProperty("scale")).toBe(scale); + expect(await tabTitle.getProperty("position")).toBe(position); + expect(await tab.getProperty("scale")).toBe(scale); }); + } - it("should render itself and child tab elements with corresponding scale (medium)", async () => { - const page = await newE2EPage({ - html: `${tabsContent}`, - }); - expect(await page.find("calcite-tabs")).toEqualAttribute("scale", "m"); - expect(await page.find("calcite-tab-nav")).toEqualAttribute("scale", "m"); - expect(await page.find("calcite-tab-title")).toEqualAttribute("scale", "m"); - expect(await page.find("calcite-tab")).toEqualAttribute("scale", "m"); - }); + describe("calcite-tabs inheritable props", () => { + const scales: Scale[] = ["s", "m", "l"]; + const positions: TabPosition[] = ["top", "bottom"]; - it("should render itself and child tab elements with corresponding scale (large)", async () => { - const page = await newE2EPage({ - html: `${tabsContent}`, + scales.forEach((scale) => { + positions.forEach((position) => { + testTabsScaleAndPosition(scale, position); }); - expect(await page.find("calcite-tabs")).toEqualAttribute("scale", "l"); - expect(await page.find("calcite-tab-nav")).toEqualAttribute("scale", "l"); - expect(await page.find("calcite-tab-title")).toEqualAttribute("scale", "l"); - expect(await page.find("calcite-tab")).toEqualAttribute("scale", "l"); }); }); @@ -258,11 +258,25 @@ describe("calcite-tabs", () => { document.body.innerHTML = `<${wrapperName}>`; const wrapper = document.querySelector(wrapperName); + + async function waitForAnimationFrames(count) { + async function frame() { + if (count > 0) { + await new Promise((resolve) => requestAnimationFrame(resolve)); + count--; + await frame(); + } + } + + await frame(); + } + wrapper.shadowRoot.querySelector("#title-2").click(); - await new Promise((resolve) => requestAnimationFrame(() => resolve())); - await new Promise((resolve) => requestAnimationFrame(() => resolve())); + await waitForAnimationFrames(4); const tabTitle = wrapper.shadowRoot.querySelector("calcite-tab-title[selected]").id; + await waitForAnimationFrames(2); + const tab = wrapper.shadowRoot.querySelector("calcite-tab[selected]").id; return { tabTitle, tab }; }, diff --git a/packages/calcite-components/src/components/tabs/tabs.stories.ts b/packages/calcite-components/src/components/tabs/tabs.stories.ts index f7e8ac5ef76..d7497dc1af1 100644 --- a/packages/calcite-components/src/components/tabs/tabs.stories.ts +++ b/packages/calcite-components/src/components/tabs/tabs.stories.ts @@ -163,16 +163,94 @@ export const setWidth = (): string => html`
`; -export const justTabNav = (): string => html` - +const TabNavHTMLSimple = html` + Tab 1 Title Tab 2 Title Tab 3 Title Tab 4 Title + Tab 1 Content + Tab 2 Content + Tab 3 Content + Tab 4 Content +`; + +const TabNavHTMLVariedTabWidth = html` + + Tab 1 Title + Tab 2 Title + Tab 3 Title + Tab 4 Title + + Tab 1 Content + Tab 2 Content + Tab 3 Content + Tab 4 Content +`; + +const tabStyles = html` + +`; + +export const centerScale_TestOnly = (): string => html` + ${tabStyles} + ${TabNavHTMLSimple} + ${TabNavHTMLSimple} + ${TabNavHTMLSimple} +`; + +export const centerVariedTabWidthScale_TestOnly = (): string => html` + ${tabStyles} + ${TabNavHTMLVariedTabWidth} + ${TabNavHTMLVariedTabWidth} + ${TabNavHTMLVariedTabWidth} +`; + +export const centerBorderedScale_TestOnly = (): string => html` + ${tabStyles} + ${TabNavHTMLSimple} + ${TabNavHTMLSimple} + ${TabNavHTMLSimple} +`; + +export const centerBorderedVariedTabWidthScale_TestOnly = (): string => html` + ${tabStyles} + ${TabNavHTMLVariedTabWidth} + ${TabNavHTMLVariedTabWidth} + ${TabNavHTMLVariedTabWidth} +`; + +export const inlineScale_TestOnly = (): string => html` + ${tabStyles} + ${TabNavHTMLSimple} + ${TabNavHTMLSimple} + ${TabNavHTMLSimple} +`; + +export const inlineVariedTabWidthScale_TestOnly = (): string => html` + ${tabStyles} + ${TabNavHTMLVariedTabWidth} + ${TabNavHTMLVariedTabWidth} + ${TabNavHTMLVariedTabWidth} +`; + +export const inlineBorderedScale_TestOnly = (): string => html` + ${tabStyles} + ${TabNavHTMLSimple} + ${TabNavHTMLSimple} + ${TabNavHTMLSimple} +`; + +export const inlineBorderedVariedTabWidthScale_TestOnly = (): string => html` + ${tabStyles} + ${TabNavHTMLVariedTabWidth} + ${TabNavHTMLVariedTabWidth} + ${TabNavHTMLVariedTabWidth} `; export const disabledTabsAndMediumIconsForLargeTabsTitle_TestOnly = (): string => html` diff --git a/packages/calcite-components/src/components/tabs/tabs.tsx b/packages/calcite-components/src/components/tabs/tabs.tsx index b900fb24ec6..79002c559c6 100644 --- a/packages/calcite-components/src/components/tabs/tabs.tsx +++ b/packages/calcite-components/src/components/tabs/tabs.tsx @@ -1,7 +1,8 @@ -import { Component, Element, Fragment, h, Listen, Prop, State, VNode } from "@stencil/core"; +import { Component, Element, Fragment, h, Listen, Prop, State, VNode, Watch } from "@stencil/core"; import { Scale } from "../interfaces"; import { TabLayout, TabPosition } from "./interfaces"; import { SLOTS } from "./resources"; +import { createObserver } from "../../utils/observers"; /** * @slot - A slot for adding `calcite-tab`s. @@ -25,15 +26,21 @@ export class Tabs { @Prop({ reflect: true }) layout: TabLayout = "inline"; /** - * Specifies the position of the component in relation to the `calcite-tab`s. + * Specifies the position of `calcite-tab-nav` and `calcite-tab-title` components in relation to the `calcite-tabs`, defaults to `top`. */ @Prop({ reflect: true }) position: TabPosition = "top"; /** - * Specifies the size of the component. + * Specifies the size of the component, defaults to `m`. */ @Prop({ reflect: true }) scale: Scale = "m"; + @Watch("position") + @Watch("scale") + handleInheritableProps(): void { + this.updateItems(); + } + /** * When `true`, the component will display with a folder style menu. */ @@ -45,6 +52,19 @@ export class Tabs { // //-------------------------------------------------------------------------- + connectedCallback(): void { + this.mutationObserver.observe(this.el, { childList: true }); + this.updateItems(); + } + + async componentWillLoad(): Promise { + this.updateItems(); + } + + disconnectedCallback(): void { + this.mutationObserver?.disconnect(); + } + render(): VNode { return ( @@ -133,6 +153,42 @@ export class Tabs { */ @State() tabs: HTMLCalciteTabElement[] = []; + mutationObserver = createObserver("mutation", (mutationsList: MutationRecord[]) => { + for (const mutation of mutationsList) { + const target = mutation.target as HTMLElement; + if ( + target.nodeName === "CALCITE-TAB-NAV" || + target.nodeName === "CALCITE-TAB-TITLE" || + target.nodeName === "CALCITE-TAB" + ) { + this.updateItems(); + } + } + }); + + private updateItems(): void { + const { position, scale } = this; + + const nav = this.el.querySelector("calcite-tab-nav"); + if (nav) { + nav.position = position; + nav.scale = scale; + } + + Array.from(this.el.querySelectorAll("calcite-tab")).forEach((tab: HTMLCalciteTabElement) => { + if (tab.parentElement === this.el) { + tab.scale = scale; + } + }); + + Array.from(this.el.querySelectorAll("calcite-tab-nav > calcite-tab-title")).forEach( + (title: HTMLCalciteTabTitleElement) => { + title.position = position; + title.scale = scale; + } + ); + } + //-------------------------------------------------------------------------- // // Private Methods