diff --git a/src/components/tab-nav/tab-nav.tsx b/src/components/tab-nav/tab-nav.tsx
index 1a8e141334c..092b0c3f82f 100644
--- a/src/components/tab-nav/tab-nav.tsx
+++ b/src/components/tab-nav/tab-nav.tsx
@@ -19,7 +19,7 @@ import {
} from "../../utils/dom";
import { createObserver } from "../../utils/observers";
import { Scale } from "../interfaces";
-import { TabChangeEventDetail } from "../tab/interfaces";
+import { TabChangeEventDetail, TabCloseEventDetail } from "../tab/interfaces";
import { TabID, TabLayout, TabPosition } from "../tabs/interfaces";
/**
@@ -110,7 +110,8 @@ export class TabNav {
this.selectedTitle = await this.getTabTitleById(this.selectedTabId);
}
- @Watch("selectedTitle") selectedTitleChanged(): void {
+ @Watch("selectedTitle")
+ selectedTitleChanged(): void {
this.updateOffsetPosition();
this.updateActiveWidth();
// reset the animation time on tab selection
@@ -181,6 +182,7 @@ export class TabNav {
// eslint-disable-next-line react/jsx-sort-props
ref={(el: HTMLDivElement) => (this.tabNavEl = el)}
>
+
(this.activeIndicatorEl = el as HTMLElement)}
/>
-
);
@@ -231,14 +232,19 @@ export class TabNav {
? event.detail.tab
: this.getIndexOfTabTitle(event.target as HTMLCalciteTabTitleElement);
event.stopPropagation();
- event.preventDefault();
}
- @Listen("calciteTabsActivate") activateTabHandler(event: CustomEvent): void {
+ @Listen("calciteTabsActivate")
+ activateTabHandler(event: CustomEvent): void {
this.calciteTabChange.emit();
+ event.stopPropagation();
+ }
+ @Listen("calciteInternalTabsClose")
+ internalCloseTabHandler(event: CustomEvent): void {
+ const closedTabTitleEl = event.target as HTMLCalciteTabTitleElement;
+ this.handleTabTitleClose(closedTabTitleEl);
event.stopPropagation();
- event.preventDefault();
}
/**
@@ -330,7 +336,6 @@ export class TabNav {
focusElementInGroup(this.enabledTabTitles, el, destination);
event.stopPropagation();
- event.preventDefault();
};
handleContainerScroll = (): void => {
@@ -373,6 +378,37 @@ export class TabNav {
return filterDirectChildren(
this.el,
"calcite-tab-title:not([disabled])"
+ ).filter((tabTitle) => !tabTitle.closed);
+ }
+
+ private handleTabTitleClose(closedTabTitleEl: HTMLCalciteTabTitleElement): void {
+ const { tabTitles } = this;
+
+ const visibleTabTitlesIndices = tabTitles.reduce(
+ (tabTitleIndices, tabTitle, index) =>
+ !tabTitle.closed ? [...tabTitleIndices, index] : tabTitleIndices,
+ []
);
+ const totalVisibleTabTitles = visibleTabTitlesIndices.length;
+
+ if (totalVisibleTabTitles === 1 && tabTitles[visibleTabTitlesIndices[0]].closable) {
+ tabTitles[visibleTabTitlesIndices[0]].closable = false;
+ this.selectedTabId = visibleTabTitlesIndices[0];
+ } else if (totalVisibleTabTitles > 1) {
+ const closedTabTitleIndex = tabTitles.findIndex((el) => el === closedTabTitleEl);
+ const nextTabTitleIndex = visibleTabTitlesIndices.find(
+ (value) => value > closedTabTitleIndex
+ );
+
+ if (this.selectedTabId === closedTabTitleIndex) {
+ this.selectedTabId = nextTabTitleIndex ? nextTabTitleIndex : totalVisibleTabTitles - 1;
+ }
+ }
+
+ requestAnimationFrame(() => {
+ this.updateOffsetPosition();
+ this.updateActiveWidth();
+ tabTitles[this.selectedTabId].focus();
+ });
}
}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages.json b/src/components/tab-title/assets/tab-title/t9n/messages.json
new file mode 100644
index 00000000000..0c5bb0e5a1c
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages.json
@@ -0,0 +1,3 @@
+{
+ "close": "Close"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_ar.json b/src/components/tab-title/assets/tab-title/t9n/messages_ar.json
new file mode 100644
index 00000000000..86447323701
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_ar.json
@@ -0,0 +1,3 @@
+{
+ "close": "إغلاق"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_bg.json b/src/components/tab-title/assets/tab-title/t9n/messages_bg.json
new file mode 100644
index 00000000000..b9bb24edef9
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_bg.json
@@ -0,0 +1,3 @@
+{
+ "close": "Затваряне"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_bs.json b/src/components/tab-title/assets/tab-title/t9n/messages_bs.json
new file mode 100644
index 00000000000..db94104b416
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_bs.json
@@ -0,0 +1,3 @@
+{
+ "close": "Zatvori"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_ca.json b/src/components/tab-title/assets/tab-title/t9n/messages_ca.json
new file mode 100644
index 00000000000..f41c36ec6c9
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_ca.json
@@ -0,0 +1,3 @@
+{
+ "close": "Tanca"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_cs.json b/src/components/tab-title/assets/tab-title/t9n/messages_cs.json
new file mode 100644
index 00000000000..97b131a500e
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_cs.json
@@ -0,0 +1,3 @@
+{
+ "close": "Zavřít"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_da.json b/src/components/tab-title/assets/tab-title/t9n/messages_da.json
new file mode 100644
index 00000000000..2fd65d6cd22
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_da.json
@@ -0,0 +1,3 @@
+{
+ "close": "Luk"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_de.json b/src/components/tab-title/assets/tab-title/t9n/messages_de.json
new file mode 100644
index 00000000000..f04b9650f38
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_de.json
@@ -0,0 +1,3 @@
+{
+ "close": "Schließen"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_el.json b/src/components/tab-title/assets/tab-title/t9n/messages_el.json
new file mode 100644
index 00000000000..a4330b8a476
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_el.json
@@ -0,0 +1,3 @@
+{
+ "close": "Κλείσιμο"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_en.json b/src/components/tab-title/assets/tab-title/t9n/messages_en.json
new file mode 100644
index 00000000000..0c5bb0e5a1c
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_en.json
@@ -0,0 +1,3 @@
+{
+ "close": "Close"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_es.json b/src/components/tab-title/assets/tab-title/t9n/messages_es.json
new file mode 100644
index 00000000000..32a5e0fa0a5
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_es.json
@@ -0,0 +1,3 @@
+{
+ "close": "Cerrar"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_et.json b/src/components/tab-title/assets/tab-title/t9n/messages_et.json
new file mode 100644
index 00000000000..654e30fb367
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_et.json
@@ -0,0 +1,3 @@
+{
+ "close": "Sule"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_fi.json b/src/components/tab-title/assets/tab-title/t9n/messages_fi.json
new file mode 100644
index 00000000000..9f769e1bbf4
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_fi.json
@@ -0,0 +1,3 @@
+{
+ "close": "Sulje"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_fr.json b/src/components/tab-title/assets/tab-title/t9n/messages_fr.json
new file mode 100644
index 00000000000..fae7179a10f
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_fr.json
@@ -0,0 +1,3 @@
+{
+ "close": "Fermer"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_he.json b/src/components/tab-title/assets/tab-title/t9n/messages_he.json
new file mode 100644
index 00000000000..6be91cee2da
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_he.json
@@ -0,0 +1,3 @@
+{
+ "close": "סגירה"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_hr.json b/src/components/tab-title/assets/tab-title/t9n/messages_hr.json
new file mode 100644
index 00000000000..db94104b416
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_hr.json
@@ -0,0 +1,3 @@
+{
+ "close": "Zatvori"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_hu.json b/src/components/tab-title/assets/tab-title/t9n/messages_hu.json
new file mode 100644
index 00000000000..b4b179d4c64
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_hu.json
@@ -0,0 +1,3 @@
+{
+ "close": "Bezárás"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_id.json b/src/components/tab-title/assets/tab-title/t9n/messages_id.json
new file mode 100644
index 00000000000..b1bc146cc7a
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_id.json
@@ -0,0 +1,3 @@
+{
+ "close": "Tutup"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_it.json b/src/components/tab-title/assets/tab-title/t9n/messages_it.json
new file mode 100644
index 00000000000..40cf2a93cfa
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_it.json
@@ -0,0 +1,3 @@
+{
+ "close": "Chiudi"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_ja.json b/src/components/tab-title/assets/tab-title/t9n/messages_ja.json
new file mode 100644
index 00000000000..93c4744dd42
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_ja.json
@@ -0,0 +1,3 @@
+{
+ "close": "閉じる"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_ko.json b/src/components/tab-title/assets/tab-title/t9n/messages_ko.json
new file mode 100644
index 00000000000..ee041777348
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_ko.json
@@ -0,0 +1,3 @@
+{
+ "close": "닫기"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_lt.json b/src/components/tab-title/assets/tab-title/t9n/messages_lt.json
new file mode 100644
index 00000000000..0b9bcbbfb26
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_lt.json
@@ -0,0 +1,3 @@
+{
+ "close": "Uždaryti"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_lv.json b/src/components/tab-title/assets/tab-title/t9n/messages_lv.json
new file mode 100644
index 00000000000..844b8c630e8
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_lv.json
@@ -0,0 +1,3 @@
+{
+ "close": "Aizvērt"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_nl.json b/src/components/tab-title/assets/tab-title/t9n/messages_nl.json
new file mode 100644
index 00000000000..97cb041c3a4
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_nl.json
@@ -0,0 +1,3 @@
+{
+ "close": "Sluiten"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_no.json b/src/components/tab-title/assets/tab-title/t9n/messages_no.json
new file mode 100644
index 00000000000..ae990c1be53
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_no.json
@@ -0,0 +1,3 @@
+{
+ "close": "Lukk"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_pl.json b/src/components/tab-title/assets/tab-title/t9n/messages_pl.json
new file mode 100644
index 00000000000..6122f93e8f1
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_pl.json
@@ -0,0 +1,3 @@
+{
+ "close": "Zamknij"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_pt-BR.json b/src/components/tab-title/assets/tab-title/t9n/messages_pt-BR.json
new file mode 100644
index 00000000000..7243d9f953f
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_pt-BR.json
@@ -0,0 +1,3 @@
+{
+ "close": "Fechar"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_pt-PT.json b/src/components/tab-title/assets/tab-title/t9n/messages_pt-PT.json
new file mode 100644
index 00000000000..7243d9f953f
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_pt-PT.json
@@ -0,0 +1,3 @@
+{
+ "close": "Fechar"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_ro.json b/src/components/tab-title/assets/tab-title/t9n/messages_ro.json
new file mode 100644
index 00000000000..913e516a1f4
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_ro.json
@@ -0,0 +1,3 @@
+{
+ "close": "Închidere"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_ru.json b/src/components/tab-title/assets/tab-title/t9n/messages_ru.json
new file mode 100644
index 00000000000..eeeebe6d6b5
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_ru.json
@@ -0,0 +1,3 @@
+{
+ "close": "Закрыть"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_sk.json b/src/components/tab-title/assets/tab-title/t9n/messages_sk.json
new file mode 100644
index 00000000000..388831f6738
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_sk.json
@@ -0,0 +1,3 @@
+{
+ "close": "Zatvoriť"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_sl.json b/src/components/tab-title/assets/tab-title/t9n/messages_sl.json
new file mode 100644
index 00000000000..50bc09c72d3
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_sl.json
@@ -0,0 +1,3 @@
+{
+ "close": "Zapri"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_sr.json b/src/components/tab-title/assets/tab-title/t9n/messages_sr.json
new file mode 100644
index 00000000000..db94104b416
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_sr.json
@@ -0,0 +1,3 @@
+{
+ "close": "Zatvori"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_sv.json b/src/components/tab-title/assets/tab-title/t9n/messages_sv.json
new file mode 100644
index 00000000000..9ff8f09568e
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_sv.json
@@ -0,0 +1,3 @@
+{
+ "close": "Stäng"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_th.json b/src/components/tab-title/assets/tab-title/t9n/messages_th.json
new file mode 100644
index 00000000000..1e72a72b05e
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_th.json
@@ -0,0 +1,3 @@
+{
+ "close": "ปิด"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_tr.json b/src/components/tab-title/assets/tab-title/t9n/messages_tr.json
new file mode 100644
index 00000000000..9ed73bb69ca
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_tr.json
@@ -0,0 +1,3 @@
+{
+ "close": "Kapat"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_uk.json b/src/components/tab-title/assets/tab-title/t9n/messages_uk.json
new file mode 100644
index 00000000000..b8f3443d079
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_uk.json
@@ -0,0 +1,3 @@
+{
+ "close": "Закрити"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_vi.json b/src/components/tab-title/assets/tab-title/t9n/messages_vi.json
new file mode 100644
index 00000000000..97ee3048de7
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_vi.json
@@ -0,0 +1,3 @@
+{
+ "close": "Đóng"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_zh-CN.json b/src/components/tab-title/assets/tab-title/t9n/messages_zh-CN.json
new file mode 100644
index 00000000000..74bb1264ef9
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_zh-CN.json
@@ -0,0 +1,3 @@
+{
+ "close": "关闭"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_zh-HK.json b/src/components/tab-title/assets/tab-title/t9n/messages_zh-HK.json
new file mode 100644
index 00000000000..388446c4e18
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_zh-HK.json
@@ -0,0 +1,3 @@
+{
+ "close": "關閉"
+}
diff --git a/src/components/tab-title/assets/tab-title/t9n/messages_zh-TW.json b/src/components/tab-title/assets/tab-title/t9n/messages_zh-TW.json
new file mode 100644
index 00000000000..388446c4e18
--- /dev/null
+++ b/src/components/tab-title/assets/tab-title/t9n/messages_zh-TW.json
@@ -0,0 +1,3 @@
+{
+ "close": "關閉"
+}
diff --git a/src/components/tab-title/resources.ts b/src/components/tab-title/resources.ts
index 7f77684e88e..7f8a5de2bf5 100644
--- a/src/components/tab-title/resources.ts
+++ b/src/components/tab-title/resources.ts
@@ -1,8 +1,14 @@
export const CSS = {
+ closeButton: "close-button",
container: "container",
- containerHasText: "container--has-text",
+ content: "content",
+ contentHasText: "content--has-text",
iconEnd: "icon-end",
iconStart: "icon-start",
iconPresent: "icon-present",
titleIcon: "calcite-tab-title--icon"
};
+
+export const ICONS = {
+ close: "x"
+};
diff --git a/src/components/tab-title/tab-title.e2e.ts b/src/components/tab-title/tab-title.e2e.ts
index 7c04c5856ac..2c14eba1d28 100644
--- a/src/components/tab-title/tab-title.e2e.ts
+++ b/src/components/tab-title/tab-title.e2e.ts
@@ -1,14 +1,48 @@
-import { newE2EPage } from "@stencil/core/testing";
+import { newE2EPage, E2EPage, E2EElement } from "@stencil/core/testing";
import { disabled, HYDRATED_ATTR, renders, hidden } from "../../tests/commonTests";
+import { html } from "../../../support/formatting";
import { CSS } from "./resources";
describe("calcite-tab-title", () => {
const tabTitleHtml = "";
- const iconStartHtml = `calcite-tab-title >>> .${CSS.titleIcon}.${CSS.iconStart}`;
- const iconEndHtml = `calcite-tab-title >>> .${CSS.titleIcon}.${CSS.iconEnd}`;
+ const tabTitleClosableHtml = "";
+ const multiTabTitleClosableMarkup = `
+
+
+ Watercraft
+ Automobiles
+ Aircrafts
+ Bicycles
+
+
+
+ Recommended for coastal use
+
+
+
+
+ A good choice for inland adventure
+
+
+
+
+ Cross continents quickly
+
+
+
+
+ Healthy and gets you from point A to B
+
+
+
+ `;
+ const iconStartSelector = `calcite-tab-title >>> .${CSS.titleIcon}.${CSS.iconStart}`;
+ const iconEndSelector = `calcite-tab-title >>> .${CSS.titleIcon}.${CSS.iconEnd}`;
+ const closeSelector = `calcite-tab-title >>> .${CSS.closeButton}`;
describe("renders", () => {
renders(tabTitleHtml, { display: "block" });
+ renders(multiTabTitleClosableMarkup, { display: "flex" });
});
describe("honors hidden attribute", () => {
@@ -21,8 +55,8 @@ describe("calcite-tab-title", () => {
const page = await newE2EPage();
await page.setContent(`Text`);
const element = await page.find("calcite-tab-title");
- const iconStart = await page.find(iconStartHtml);
- const iconEnd = await page.find(iconEndHtml);
+ const iconStart = await page.find(iconStartSelector);
+ const iconEnd = await page.find(iconEndSelector);
expect(element).toHaveAttribute(HYDRATED_ATTR);
expect(iconStart).not.toBeNull();
expect(iconEnd).toBeNull();
@@ -32,8 +66,8 @@ describe("calcite-tab-title", () => {
const page = await newE2EPage();
await page.setContent(`Text`);
const element = await page.find("calcite-tab-title");
- const iconStart = await page.find(iconStartHtml);
- const iconEnd = await page.find(iconEndHtml);
+ const iconStart = await page.find(iconStartSelector);
+ const iconEnd = await page.find(iconEndSelector);
expect(element).toHaveAttribute(HYDRATED_ATTR);
expect(iconStart).toBeNull();
expect(iconEnd).not.toBeNull();
@@ -43,13 +77,248 @@ describe("calcite-tab-title", () => {
const page = await newE2EPage();
await page.setContent(`Text`);
const element = await page.find("calcite-tab-title");
- const iconStart = await page.find(iconStartHtml);
- const iconEnd = await page.find(iconEndHtml);
+ const iconStart = await page.find(iconStartSelector);
+ const iconEnd = await page.find(iconEndSelector);
expect(element).toHaveAttribute(HYDRATED_ATTR);
expect(iconStart).not.toBeNull();
expect(iconEnd).not.toBeNull();
});
+ describe("basic closing behavior", () => {
+ let page: E2EPage;
+
+ beforeEach(async () => {
+ page = await newE2EPage();
+ await page.setContent(tabTitleClosableHtml);
+ });
+
+ it("clicking on close button closes the tab", async () => {
+ const close = await page.find(closeSelector);
+
+ await close.click();
+ await page.waitForChanges();
+
+ const containerEl = await page.find(`calcite-tab-title >>> .${CSS.container}`);
+ expect(await containerEl.getProperty("hidden")).toBe(true);
+ });
+
+ it("becomes no longer closable when it's the last remaining tab", async () => {
+ page = await newE2EPage();
+ await page.setContent(
+ html`
+
+ Text
+ Text
+
+ `
+ );
+
+ let containerElOne = await page.find(`calcite-tab-title[id='one']`);
+ const closeOne = await page.find(`calcite-tab-title[id='one'] >>> .${CSS.closeButton}`);
+ expect(containerElOne).toHaveAttribute(HYDRATED_ATTR);
+
+ await closeOne.click();
+ await page.waitForChanges();
+
+ containerElOne = await page.find(`calcite-tab-title[id='one']>>> .${CSS.container}`);
+ expect(await containerElOne.getProperty("hidden")).toBe(true);
+
+ const closeTwo = await page.find(`calcite-tab-title[id='two'] >>> .${CSS.closeButton}`);
+ expect(await closeTwo.getProperty("closable")).not.toBe(true);
+ });
+ });
+
+ describe("closing sequence", () => {
+ let page: E2EPage;
+
+ let matchingTabEl: E2EElement;
+ let tabTitleCloseButtonEl: E2EElement;
+
+ const closeTabsInSequenceOfGivenArrayOfIds = async (arrayOfIds: string[]): Promise => {
+ for (let i = 0; i < arrayOfIds.length - 1; i++) {
+ let tabEl: E2EElement;
+ let tabTitleContainerEl: E2EElement;
+
+ const id = arrayOfIds[i];
+
+ tabEl = await page.find(`#${id}`);
+ tabTitleContainerEl = await page.find(`calcite-tab-title[id='${id}'] >>> .${CSS.container}`);
+ const tabTitleCloseButtonEl = await page.find(`calcite-tab-title[id='${id}'] >>> .${CSS.closeButton}`);
+
+ expect(await tabTitleContainerEl.getProperty("hidden")).not.toBe(true);
+
+ await tabTitleCloseButtonEl.click();
+ await page.waitForChanges();
+
+ const tabTitleEl = await page.find(`#${id}`);
+ tabEl = await page.find(`#${id}`);
+ tabTitleContainerEl = await page.find(`calcite-tab-title[id='${id}'] >>> .${CSS.container}`);
+
+ expect(await tabTitleContainerEl.getProperty("hidden")).toBe(true);
+ expect(await tabTitleEl.getProperty("selected")).not.toBe(true);
+ expect(await tabEl.getProperty("selected")).not.toBe(true);
+
+ const nextId = arrayOfIds[i + 1];
+ const nextTabTitleEl = await page.find(`#${nextId}`);
+ const nextTabEl = await page.find(`#${nextId}`);
+ const nextTabTitleContainerEl = await page.find(`calcite-tab-title[id='${nextId}'] >>> .${CSS.container}`);
+
+ expect(await nextTabTitleContainerEl.getProperty("hidden")).not.toBe(true);
+ expect(await nextTabTitleEl.getProperty("selected")).toBe(true);
+ expect(await nextTabEl.getProperty("selected")).toBe(true);
+ }
+ };
+
+ beforeEach(async () => {
+ page = await newE2EPage();
+ await page.setContent(multiTabTitleClosableMarkup);
+ });
+
+ it(`when closing tab-titles in sequence 1 (first selected) through 4,
+ tab-title and corresponding tab become hidden,
+ and selection fallback is the next tab`, async () => {
+ await closeTabsInSequenceOfGivenArrayOfIds(["title1", "title2", "title3", "title4"]);
+ });
+
+ it(`when closing tab-titles in sequence 4 (last selected) through 1,
+ tab-title and corresponding tab become hidden,
+ and selection fallback is the previous tab`, async () => {
+ const arrayOfReversedIds = ["title1", "title2", "title3", "title4"].reverse();
+
+ const bikingTabTitleEl = await page.find(`#title4`);
+ bikingTabTitleEl.setProperty("selected", true);
+
+ await page.waitForChanges();
+
+ await closeTabsInSequenceOfGivenArrayOfIds(arrayOfReversedIds);
+ });
+
+ it(`closing an unselected tab-title does not deselect the current selection`, async () => {
+ const selectedEmbarkTabEl = await page.find("#tab1");
+ const selectedEmbarkTabTitleContainerEl = await page.find(`#title1>>> .${CSS.container}`);
+
+ const carTabTitleContainerEl = await page.find(`#title2 >>> .${CSS.container}`);
+ matchingTabEl = await page.find("#tab2");
+ tabTitleCloseButtonEl = await page.find(`#title2 >>> .${CSS.closeButton}`);
+
+ await tabTitleCloseButtonEl.click();
+ await page.waitForChanges();
+
+ expect(await carTabTitleContainerEl.getProperty("hidden")).toBe(true);
+ expect(await matchingTabEl.getProperty("selected")).not.toBe(true);
+
+ expect(await selectedEmbarkTabTitleContainerEl.getProperty("hidden")).not.toBe(true);
+ expect(await selectedEmbarkTabEl.getProperty("selected")).toBe(true);
+
+ const planeTabTitleContainerEl = await page.find(`#title3 >>> .${CSS.container}`);
+ matchingTabEl = await page.find("#tab3");
+ tabTitleCloseButtonEl = await page.find(`#title3 >>> .${CSS.closeButton}`);
+
+ await tabTitleCloseButtonEl.click();
+ await page.waitForChanges();
+
+ expect(await carTabTitleContainerEl.getProperty("hidden")).toBe(true);
+
+ expect(await planeTabTitleContainerEl.getProperty("hidden")).toBe(true);
+ expect(await matchingTabEl.getProperty("selected")).not.toBe(true);
+
+ expect(await selectedEmbarkTabTitleContainerEl.getProperty("hidden")).not.toBe(true);
+ expect(await selectedEmbarkTabEl.getProperty("selected")).toBe(true);
+
+ const bikingTabTitleContainerEl = await page.find(`#title4 >>> .${CSS.container}`);
+ matchingTabEl = await page.find("#tab4");
+ tabTitleCloseButtonEl = await page.find(`#title4 >>> .${CSS.closeButton}`);
+
+ await tabTitleCloseButtonEl.click();
+ await page.waitForChanges();
+
+ expect(await carTabTitleContainerEl.getProperty("hidden")).toBe(true);
+ expect(await planeTabTitleContainerEl.getProperty("hidden")).toBe(true);
+ expect(await bikingTabTitleContainerEl.getProperty("hidden")).toBe(true);
+ expect(await matchingTabEl.getProperty("selected")).not.toBe(true);
+
+ expect(await selectedEmbarkTabTitleContainerEl.getProperty("hidden")).not.toBe(true);
+ expect(await selectedEmbarkTabEl.getProperty("selected")).toBe(true);
+ });
+
+ it(`case 1: works with randomized closing sequence with mixed selected and not`, async () => {
+ const carTabTitleEl = await page.find(`#title2`);
+ const carTabTitleContainerEl = await page.find(`#title2 >>> .${CSS.container}`);
+ matchingTabEl = await page.find("#tab2");
+
+ carTabTitleEl.setProperty("selected", true);
+ await page.waitForChanges();
+
+ expect(await matchingTabEl.getProperty("selected")).toBe(true);
+
+ const embarkTabTitleContainerEl = await page.find(`#title1 >>> .${CSS.container}`);
+ tabTitleCloseButtonEl = await page.find(`#title1 >>> .${CSS.closeButton}`);
+
+ await tabTitleCloseButtonEl.click();
+ await page.waitForChanges();
+
+ expect(await embarkTabTitleContainerEl.getProperty("hidden")).toBe(true);
+ expect(await carTabTitleContainerEl.getProperty("hidden")).not.toBe(true);
+ expect(await carTabTitleEl.getProperty("selected")).toBe(true);
+ expect(await matchingTabEl.getProperty("selected")).toBe(true);
+
+ const planeTabTitleContainerEl = await page.find(`#title3 >>> .${CSS.container}`);
+ matchingTabEl = await page.find("#tab3");
+ tabTitleCloseButtonEl = await page.find(`#title2 >>> .${CSS.closeButton}`);
+
+ await tabTitleCloseButtonEl.click();
+ await page.waitForChanges();
+
+ expect(await carTabTitleContainerEl.getProperty("hidden")).toBe(true);
+ expect(await planeTabTitleContainerEl.getProperty("hidden")).not.toBe(true);
+ expect(await carTabTitleEl.getProperty("selected")).not.toBe(true);
+ expect(await matchingTabEl.getProperty("selected")).toBe(true);
+ });
+
+ it(`case 2: works with randomized closing sequence with mixed selected and not`, async () => {
+ const carTabTitleEl = await page.find(`#title2`);
+ const carTabTitleContainerEl = await page.find(`#title2 >>> .${CSS.container}`);
+ matchingTabEl = await page.find("#tab2");
+
+ carTabTitleEl.setProperty("selected", true);
+ await page.waitForChanges();
+
+ expect(await matchingTabEl.getProperty("selected")).toBe(true);
+
+ const embarkTabTitleContainerEl = await page.find(`#title1 >>> .${CSS.container}`);
+ tabTitleCloseButtonEl = await page.find(`#title1 >>> .${CSS.closeButton}`);
+
+ await tabTitleCloseButtonEl.click();
+ await page.waitForChanges();
+
+ expect(await embarkTabTitleContainerEl.getProperty("hidden")).toBe(true);
+ expect(await carTabTitleContainerEl.getProperty("hidden")).not.toBe(true);
+ expect(await carTabTitleEl.getProperty("selected")).toBe(true);
+ expect(await matchingTabEl.getProperty("selected")).toBe(true);
+
+ const planeTabTitleEl = await page.find(`#title3`);
+
+ planeTabTitleEl.setProperty("selected", true);
+ await page.waitForChanges();
+
+ expect(await planeTabTitleEl.getProperty("selected")).toBe(true);
+
+ const planeTabTitleContainerEl = await page.find(`#title3 >>> .${CSS.container}`);
+ const bikingTabTitleContainerEl = await page.find(`#title4 >>> .${CSS.container}`);
+ matchingTabEl = await page.find("#title3");
+ tabTitleCloseButtonEl = await page.find(`#title2 >>> .${CSS.closeButton}`);
+
+ await tabTitleCloseButtonEl.click();
+ await page.waitForChanges();
+
+ expect(await planeTabTitleContainerEl.getProperty("hidden")).not.toBe(true);
+ expect(await carTabTitleContainerEl.getProperty("hidden")).toBe(true);
+ expect(await bikingTabTitleContainerEl.getProperty("hidden")).not.toBe(true);
+ expect(await planeTabTitleEl.getProperty("selected")).toBe(true);
+ expect(await matchingTabEl.getProperty("selected")).toBe(true);
+ });
+ });
+
it.skip("emits active event on user interaction only", async () => {
const page = await newE2EPage();
await page.setContent(`Title`);
@@ -108,49 +377,73 @@ describe("calcite-tab-title", () => {
});
describe("scale property", () => {
- it("should inherit small scale from tab-nav", async () => {
- const page = await newE2EPage({
- html: `
- Tab Title
- Tab 2 Title
- `
- });
- const element = await page.find("calcite-tab-title");
- const container = await page.find("calcite-tab-title >>> .container");
- const containerStyles = await container.getComputedStyle();
- expect(element).toEqualAttribute("scale", "s");
- expect(containerStyles.fontSize).toEqual("12px");
- expect(containerStyles.lineHeight).toEqual("16px"); // 1rem
+ let page: E2EPage;
+
+ beforeEach(async () => {
+ page = await newE2EPage();
});
- it("should inherit medium scale from tab-nav", async () => {
- const page = await newE2EPage({
- html: `
- Tab Title
- Tab 2 Title
- `
- });
- const element = await page.find("calcite-tab-title");
- const container = await page.find("calcite-tab-title >>> .container");
- const containerStyles = await container.getComputedStyle();
- expect(element).toEqualAttribute("scale", "m");
- expect(containerStyles.fontSize).toEqual("14px");
- expect(containerStyles.lineHeight).toEqual("16px"); // 1rem
+ 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 () => {
- const page = await newE2EPage({
- html: `
- Tab 1 Title
- Tab 2 Title
- `
- });
- const element = await page.find("calcite-tab-title");
- const container = await page.find("calcite-tab-title >>> .container");
- const containerStyles = await container.getComputedStyle();
- expect(element).toEqualAttribute("scale", "l");
- expect(containerStyles.fontSize).toEqual("16px");
- expect(containerStyles.lineHeight).toEqual("20px"); // 1.25rem
+ 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");
});
});
});
diff --git a/src/components/tab-title/tab-title.scss b/src/components/tab-title/tab-title.scss
index 9c6c6b6c62b..2cd2101d851 100644
--- a/src/components/tab-title/tab-title.scss
+++ b/src/components/tab-title/tab-title.scss
@@ -17,12 +17,20 @@
border-block-start-style: solid;
}
-:host .container {
+:host([closed]) {
+ @apply hidden;
+}
+
+.container {
@apply focus-base;
}
:host(:focus) .container {
@apply focus-inset;
+
+ &:focus-within {
+ @apply focus-base;
+ }
}
:host(:active),
@@ -45,42 +53,46 @@
:host([scale="s"]) {
margin-inline-end: 1rem;
- .container {
+ .content {
@apply text-n2h py-1;
}
}
:host([scale="m"]) {
- .container {
+ .content {
@apply text-n1h py-2;
}
}
:host([scale="l"]) {
margin-inline-end: 1.5rem;
- .container {
+ .content {
@apply text-0h py-2.5;
}
}
.container {
- @apply border-b-color-transparent
- text-color-3
- text-n1h
- transition-default
+ @apply border-b-2
+ border-b-color-transparent
box-border
+ content-center
+ cursor-pointer
flex
h-full
- w-full
- cursor-pointer
- appearance-none
- justify-center
- truncate
- border-b-2
- px-0;
+ justify-between
+ px-0
+ text-color-3
+ text-n1h
+ transition-default
+ w-full;
+
border-block-end-style: solid;
}
+.content {
+ @apply flex items-center justify-center;
+}
+
.calcite-tab-title--icon {
@apply relative
m-0
@@ -91,18 +103,59 @@
}
}
-.container--has-text {
+.content--has-text {
@apply p-1;
}
-.container--has-text .calcite-tab-title--icon.icon-start {
+.content--has-text .calcite-tab-title--icon.icon-start {
margin-inline-end: theme("margin.2");
}
-.container--has-text .calcite-tab-title--icon.icon-end {
+.content--has-text .calcite-tab-title--icon.icon-end {
margin-inline-start: theme("margin.2");
}
+.close-button {
+ @apply appearance-none
+ bg-foreground-1
+ border-none
+ content-center
+ cursor-pointer
+ flex
+ focus-base
+ items-center
+ justify-center
+ self-center
+ text-color-3
+ transition-default;
+
+ block-size: calc(100% - 2px); // fit within top/bottom borders
+ background-color: var(--calcite-button-transparent-1);
+ margin-inline-start: auto;
+
+ &:focus {
+ @apply focus-inset;
+
+ // ⚠️overriding outline-offset should be avoided as it won't honor --calcite-ui-focus-offset-invert
+ outline-offset: -1px;
+ }
+
+ &:focus,
+ &:hover {
+ @apply text-color-1;
+ background-color: var(--calcite-ui-foreground-2);
+ }
+
+ &:active {
+ @apply text-color-1;
+ background-color: var(--calcite-ui-foreground-3);
+ }
+
+ & calcite-icon {
+ color: inherit;
+ }
+}
+
// compensate for spacing when no hastext and two icons
:host([icon-start][icon-end]) {
.calcite-tab-title--icon:first-child {
@@ -137,12 +190,14 @@
}
}
+:host([closable]) .container,
:host([bordered]) .container {
border-block-end-style: unset;
border-inline-start: 1px solid transparent;
border-inline-end: 1px solid transparent;
}
+:host([closable][position="bottom"]) .container,
:host([bordered][position="bottom"]) .container {
border-block-start-style: unset;
}
@@ -153,24 +208,21 @@
}
:host([bordered]) {
- .container {
+ .content {
@apply px-3;
}
}
:host([bordered][scale="s"]) {
- .container {
+ .content {
@apply px-2;
}
}
:host([bordered][scale="l"]) {
- .container {
+ .content {
@apply px-4;
}
- .icon-present {
- padding-block: 11px;
- }
}
@media (forced-colors: active) {
diff --git a/src/components/tab-title/tab-title.tsx b/src/components/tab-title/tab-title.tsx
index 9ed68e80df4..6244ee52cdb 100644
--- a/src/components/tab-title/tab-title.tsx
+++ b/src/components/tab-title/tab-title.tsx
@@ -13,14 +13,27 @@ import {
VNode,
Watch
} from "@stencil/core";
-import { getElementDir, getElementProp, toAriaBoolean } from "../../utils/dom";
+import { getElementDir, getElementProp, toAriaBoolean, nodeListToArray } from "../../utils/dom";
import { guid } from "../../utils/guid";
import { InteractiveComponent, updateHostInteraction } from "../../utils/interactive";
import { createObserver } from "../../utils/observers";
import { FlipContext, Scale } from "../interfaces";
-import { TabChangeEventDetail } from "../tab/interfaces";
-import { CSS } from "./resources";
+import { TabChangeEventDetail, TabCloseEventDetail } from "../tab/interfaces";
+import { CSS, ICONS } from "./resources";
import { TabID, TabLayout, TabPosition } from "../tabs/interfaces";
+import { connectLocalized, disconnectLocalized, LocalizedComponent } from "../../utils/locale";
+import {
+ connectMessages,
+ disconnectMessages,
+ setUpMessages,
+ T9nComponent,
+ updateMessages
+} from "../../utils/t9n";
+import { TabTitleMessages } from "./assets/tab-title/t9n";
+
+/**
+ * Tab-titles are optionally individually closable.
+ */
/**
* @slot - A slot for adding text.
@@ -28,9 +41,10 @@ import { TabID, TabLayout, TabPosition } from "../tabs/interfaces";
@Component({
tag: "calcite-tab-title",
styleUrl: "tab-title.scss",
- shadow: true
+ shadow: true,
+ assetsDirs: ["assets"]
})
-export class TabTitle implements InteractiveComponent {
+export class TabTitle implements InteractiveComponent, LocalizedComponent, T9nComponent {
//--------------------------------------------------------------------------
//
// Element
@@ -59,6 +73,12 @@ export class TabTitle implements InteractiveComponent {
}
}
+ /** When `true`, a close button is added to the component. */
+ @Prop({ reflect: true }) closable = false;
+
+ /** When `true`, does not display or position the component. */
+ @Prop({ reflect: true, mutable: true }) closed = false;
+
/** When `true`, interaction is prevented and the component is displayed with lower opacity. */
@Prop({ reflect: true }) disabled = false;
@@ -98,6 +118,25 @@ export class TabTitle implements InteractiveComponent {
*/
@Prop({ reflect: true }) tab: string;
+ /**
+ * Made into a prop for testing purposes only
+ *
+ * @internal
+ */
+ // eslint-disable-next-line @stencil-community/strict-mutable -- updated by t9n module
+ @Prop({ mutable: true }) messages: TabTitleMessages;
+
+ /**
+ * Use this property to override individual strings used by the component.
+ */
+ // eslint-disable-next-line @stencil-community/strict-mutable -- updated by t9n module
+ @Prop({ mutable: true }) messageOverrides: Partial;
+
+ @Watch("messageOverrides")
+ onMessagesChange(): void {
+ /* wired up by t9n util */
+ }
+
//--------------------------------------------------------------------------
//
// Lifecycle
@@ -105,6 +144,8 @@ export class TabTitle implements InteractiveComponent {
//--------------------------------------------------------------------------
connectedCallback(): void {
+ connectLocalized(this);
+ connectMessages(this);
this.setupTextContentObserver();
this.parentTabNavEl = this.el.closest("calcite-tab-nav");
this.parentTabsEl = this.el.closest("calcite-tabs");
@@ -119,9 +160,12 @@ export class TabTitle implements InteractiveComponent {
})
);
this.resizeObserver?.disconnect();
+ disconnectLocalized(this);
+ disconnectMessages(this);
}
- componentWillLoad(): void {
+ async componentWillLoad(): Promise {
+ await setUpMessages(this);
if (Build.isBrowser) {
this.updateHasText();
}
@@ -145,7 +189,8 @@ export class TabTitle implements InteractiveComponent {
}
render(): VNode {
- const id = this.el.id || this.guid;
+ const { el, closed } = this;
+ const id = el.id || this.guid;
const iconStartEl = (
this.resizeObserver?.observe(el)}
>
- {this.iconStart ? iconStartEl : null}
-
- {this.iconEnd ? iconEndEl : null}
+
+ {this.iconStart ? iconStartEl : null}
+
+ {this.iconEnd ? iconEndEl : null}
+
+ {this.renderCloseButton()}
);
}
+ renderCloseButton(): VNode {
+ const { closable, messages } = this;
+
+ return closable ? (
+
+ ) : null;
+ }
+
async componentDidLoad(): Promise {
this.calciteInternalTabTitleRegister.emit(await this.getTabIdentifier());
}
@@ -241,8 +309,10 @@ export class TabTitle implements InteractiveComponent {
switch (event.key) {
case " ":
case "Enter":
- this.emitActiveTab();
- event.preventDefault();
+ if (!event.composedPath().includes(this.closeButtonEl)) {
+ this.emitActiveTab();
+ event.preventDefault();
+ }
break;
case "ArrowRight":
event.preventDefault();
@@ -291,8 +361,21 @@ export class TabTitle implements InteractiveComponent {
@Event({ cancelable: false }) calciteInternalTabsActivate: EventEmitter;
/**
+ * Fires when a `calcite-tab` is closed.
+ */
+ @Event({ cancelable: false }) calciteTabsClose: EventEmitter;
+
+ /**
+ * Fires when `calcite-tab` is closed (`event.details`).
+ *
+ * @see [TabChangeEventDetail](https://github.com/Esri/calcite-components/blob/master/src/components/tab/interfaces.ts)
* @internal
*/
+ @Event({ cancelable: false }) calciteInternalTabsClose: EventEmitter;
+ /**
+ * @internal
+ */
+
@Event({ cancelable: false }) calciteInternalTabsFocusNext: EventEmitter;
/**
@@ -332,7 +415,9 @@ export class TabTitle implements InteractiveComponent {
@Method()
async getTabIndex(): Promise {
return Array.prototype.indexOf.call(
- this.el.parentElement.querySelectorAll("calcite-tab-title"),
+ nodeListToArray(this.el.parentElement.children).filter((el) =>
+ el.matches("calcite-tab-title")
+ ),
this.el
);
}
@@ -355,6 +440,16 @@ export class TabTitle implements InteractiveComponent {
this.controls = tabIds[titleIds.indexOf(this.el.id)] || null;
}
+ //--------------------------------------------------------------------------
+ //
+ // Private Methods
+ //
+ //--------------------------------------------------------------------------
+
+ private closeClickHandler = (): void => {
+ this.closeTabTitleAndNotify();
+ };
+
//--------------------------------------------------------------------------
//
// Private State/Props
@@ -366,15 +461,26 @@ export class TabTitle implements InteractiveComponent {
@State() controls: string;
+ @State() defaultMessages: TabTitleMessages;
+
+ @State() effectiveLocale: "";
+
+ @Watch("effectiveLocale")
+ effectiveLocaleChange(): void {
+ updateMessages(this, this.effectiveLocale);
+ }
+
/** determine if there is slotted text for styling purposes */
@State() hasText = false;
+ closeButtonEl: HTMLButtonElement;
+
+ containerEl: HTMLDivElement;
+
parentTabNavEl: HTMLCalciteTabNavElement;
parentTabsEl: HTMLCalciteTabsElement;
- containerEl: HTMLDivElement;
-
resizeObserver = createObserver("resize", () => {
this.calciteInternalTabIconChanged.emit();
});
@@ -388,12 +494,10 @@ export class TabTitle implements InteractiveComponent {
}
emitActiveTab(userTriggered = true): void {
- if (this.disabled) {
+ if (this.disabled || this.closed) {
return;
}
-
const payload = { tab: this.tab };
-
this.calciteInternalTabsActivate.emit(payload);
if (userTriggered) {
@@ -402,5 +506,11 @@ export class TabTitle implements InteractiveComponent {
}
}
+ closeTabTitleAndNotify(): void {
+ this.closed = true;
+ this.calciteInternalTabsClose.emit({ tab: this.tab });
+ this.calciteTabsClose.emit();
+ }
+
guid = `calcite-tab-title-${guid()}`;
}
diff --git a/src/components/tab/interfaces.ts b/src/components/tab/interfaces.ts
index 102d494078e..a3e2d0c26f3 100644
--- a/src/components/tab/interfaces.ts
+++ b/src/components/tab/interfaces.ts
@@ -1,6 +1,13 @@
export interface TabChangeEventDetail {
/**
- * The tab that just became selected
+ * The tab ID that just became selected
+ */
+ tab: number | string;
+}
+
+export interface TabCloseEventDetail {
+ /**
+ * The tab ID that just became closed
*/
tab: number | string;
}
diff --git a/src/components/tab/tab.e2e.ts b/src/components/tab/tab.e2e.ts
index f695eea4e23..4057f2163c1 100644
--- a/src/components/tab/tab.e2e.ts
+++ b/src/components/tab/tab.e2e.ts
@@ -3,10 +3,11 @@ import { defaults, renders, hidden } from "../../tests/commonTests";
describe("calcite-tab", () => {
const tabHtml = "A tab";
+ const tabHtmlSelected = "A tab";
describe("renders", () => {
- renders("calcite-tab", { display: "none", visible: false });
- renders("", { display: "block", visible: true });
+ renders(tabHtml, { display: "none", visible: false });
+ renders(tabHtmlSelected, { display: "block", visible: true });
});
describe("honors hidden attribute", () => {
diff --git a/src/components/tabs/tabs.e2e.ts b/src/components/tabs/tabs.e2e.ts
index 93ed61f30e7..62a3f27d974 100644
--- a/src/components/tabs/tabs.e2e.ts
+++ b/src/components/tabs/tabs.e2e.ts
@@ -217,7 +217,7 @@ describe("calcite-tabs", () => {
expect(await page.find("calcite-tabs")).toHaveAttribute("bordered");
});
- it("item selection should work when placed inside shadow DOM (#992)", async () => {
+ it("item selection should work when placed inside shadow DOM", async () => {
const wrappedTabTemplateHTML = html`
@@ -238,7 +238,7 @@ describe("calcite-tabs", () => {
await page.waitForChanges();
const finalSelectedItem = await page.evaluate(
- async (templateHTML: string): Promise<{ titleTab: string; contentTab: string }> => {
+ async (templateHTML: string): Promise<{ tabTitle: string; tab: string }> => {
const wrapperName = "tab-wrapping-component";
customElements.define(
@@ -261,14 +261,14 @@ describe("calcite-tabs", () => {
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
- const titleTab = wrapper.shadowRoot.querySelector("calcite-tab-title[selected]").id;
- const contentTab = wrapper.shadowRoot.querySelector("calcite-tab[selected]").id;
- return { titleTab, contentTab };
+ const tabTitle = wrapper.shadowRoot.querySelector("calcite-tab-title[selected]").id;
+ const tab = wrapper.shadowRoot.querySelector("calcite-tab[selected]").id;
+ return { tabTitle, tab };
},
[wrappedTabTemplateHTML]
);
- expect(finalSelectedItem.titleTab).toBe("title-2");
- expect(finalSelectedItem.contentTab).toBe("tab-2");
+ expect(finalSelectedItem.tabTitle).toBe("title-2");
+ expect(finalSelectedItem.tab).toBe("tab-2");
});
it("item selection should work with nested tabs", async () => {
@@ -300,7 +300,6 @@ describe("calcite-tabs", () => {
const kidB = await page.find("#kidB");
await kidB.click();
-
await page.waitForChanges();
const parentTabA = await page.find("#parentTabA");
diff --git a/src/components/tabs/tabs.stories.ts b/src/components/tabs/tabs.stories.ts
index c2e07477166..dbc13676ec3 100644
--- a/src/components/tabs/tabs.stories.ts
+++ b/src/components/tabs/tabs.stories.ts
@@ -1,5 +1,5 @@
import { select } from "@storybook/addon-knobs";
-import { iconNames, storyFilters } from "../../../.storybook/helpers";
+import { boolean, iconNames, storyFilters } from "../../../.storybook/helpers";
import { placeholderImage } from "../../../.storybook/placeholderImage";
import { modesDarkDefault } from "../../../.storybook/utils";
import { html } from "../../../support/formatting";
@@ -58,6 +58,26 @@ export const bordered = (): string => html`
`;
+export const closable = (): string => 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
+
+`;
+
export const borderedDarkModeRTL_TestOnly = (): string => html`