Skip to content

fix(tabs): handle tab close events that remove the associated tab-title and tab elements from the DOM #9768

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1b883db
fix(tabs): fix closing behavior when tabs are removed from the DOM
eriklharper Jun 12, 2024
33a7897
Merge branch 'dev' of github.com:Esri/calcite-design-system into erik…
eriklharper Jul 1, 2024
f5bb30c
moving lifecycle members to the bottom of the tabs file
eriklharper Jul 1, 2024
7ae928b
refactoring tab-nav's slotchange handler to use slotChangeGetAssigned…
eriklharper Jul 1, 2024
04dcc8d
Refactoring registryHandler to query for the default slot element's a…
eriklharper Jul 5, 2024
2baeda4
adding tab identifier logs for debugging
eriklharper Jul 12, 2024
8b61305
relocating selectedTitle and selectedTabId watchers to be adjacent to…
eriklharper Jul 15, 2024
575dfb6
removing unused internalTabRegister event
eriklharper Jul 15, 2024
a8a08e8
removing logs
eriklharper Jul 15, 2024
69e59fd
emitting internalTabChange event from selectedTitle watcher for now t…
eriklharper Jul 15, 2024
361eb54
remove redundant setting of selectedTitle from inside the selectedTab…
eriklharper Jul 15, 2024
73db0ff
removing comment
eriklharper Jul 15, 2024
e812bbd
reverting tab-title change no longer needed
eriklharper Jul 15, 2024
a4df2e7
fixing highlight selected title on initial load
eriklharper Jul 15, 2024
59a7d64
Merge branch 'dev' of github.com:Esri/calcite-design-system into erik…
eriklharper Jul 15, 2024
45ce9dd
Merge branch 'dev' of github.com:Esri/calcite-design-system into erik…
eriklharper Jul 16, 2024
4d92c18
removing select-none
eriklharper Jul 16, 2024
9b1a53c
Merge branch 'eriklharper/7155-closable-tab-bug' of github.com:Esri/c…
eriklharper Jul 16, 2024
58384cd
adding dom removal test
eriklharper Jul 16, 2024
e00a764
adding closable tabs to demo page
eriklharper Jul 16, 2024
a001a0a
Merge branch 'eriklharper/7155-closable-tab-bug' of github.com:Esri/c…
eriklharper Jul 16, 2024
3e7f77b
Merge branch 'dev' of github.com:Esri/calcite-design-system into erik…
eriklharper Jul 16, 2024
1a023c5
Merge branch 'dev' of github.com:Esri/calcite-design-system into erik…
eriklharper Jul 16, 2024
cfb1626
component types
eriklharper Jul 16, 2024
81268fd
refactored new dom utils to use a css matches selector instead of jus…
eriklharper Jul 17, 2024
91f7cbb
stop propagating new slot change event
eriklharper Jul 17, 2024
a8dbcdb
renaming registryHandler to updateAriaSettings
eriklharper Jul 17, 2024
c27ccf1
removing need to data-name attributes in e2e test
eriklharper Jul 17, 2024
ce4c4ee
replacing mutation observer logic with slot change handler
eriklharper Jul 18, 2024
568bd7b
Merge branch 'dev' into eriklharper/7155-closable-tab-bug
eriklharper Jul 18, 2024
3243cf9
updating to use generic types where possible
eriklharper Jul 19, 2024
d8b524b
renaming function
eriklharper Jul 19, 2024
2110d0a
Merge branch 'dev' of github.com:Esri/calcite-design-system into erik…
eriklharper Jul 19, 2024
3db06f4
adding spec unit tests for getSlotAssignedElements
eriklharper Jul 23, 2024
376dd40
Merge branch 'dev' into eriklharper/7155-closable-tab-bug
eriklharper Jul 23, 2024
5afaf2e
adding one more test for empty results
eriklharper Jul 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 4 additions & 16 deletions packages/calcite-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ 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 { Element } from "@stencil/core";
import { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces";
import { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n";
import { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent, TableSelectionDisplay } from "./components/table/interfaces";
Expand Down Expand Up @@ -175,6 +176,7 @@ 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 { Element } from "@stencil/core";
export { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces";
export { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n";
export { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent, TableSelectionDisplay } from "./components/table/interfaces";
Expand Down Expand Up @@ -6052,10 +6054,6 @@ export interface CalciteSwitchCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLCalciteSwitchElement;
}
export interface CalciteTabCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLCalciteTabElement;
}
export interface CalciteTabNavCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLCalciteTabNavElement;
Expand Down Expand Up @@ -7492,25 +7490,15 @@ declare global {
prototype: HTMLCalciteSwitchElement;
new (): HTMLCalciteSwitchElement;
};
interface HTMLCalciteTabElementEventMap {
"calciteInternalTabRegister": void;
}
interface HTMLCalciteTabElement extends Components.CalciteTab, HTMLStencilElement {
addEventListener<K extends keyof HTMLCalciteTabElementEventMap>(type: K, listener: (this: HTMLCalciteTabElement, ev: CalciteTabCustomEvent<HTMLCalciteTabElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLCalciteTabElementEventMap>(type: K, listener: (this: HTMLCalciteTabElement, ev: CalciteTabCustomEvent<HTMLCalciteTabElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLCalciteTabElement: {
prototype: HTMLCalciteTabElement;
new (): HTMLCalciteTabElement;
};
interface HTMLCalciteTabNavElementEventMap {
"calciteTabChange": void;
"calciteInternalTabNavSlotChange": Element[];
"calciteInternalTabChange": TabChangeEventDetail;
}
interface HTMLCalciteTabNavElement extends Components.CalciteTabNav, HTMLStencilElement {
Expand Down Expand Up @@ -12867,7 +12855,6 @@ declare namespace LocalJSX {
"value"?: any;
}
interface CalciteTab {
"onCalciteInternalTabRegister"?: (event: CalciteTabCustomEvent<void>) => void;
/**
* Specifies the size of the component inherited from the parent `calcite-tabs`, defaults to `m`.
*/
Expand Down Expand Up @@ -12895,6 +12882,7 @@ declare namespace LocalJSX {
*/
"messages"?: TabNavMessages;
"onCalciteInternalTabChange"?: (event: CalciteTabNavCustomEvent<TabChangeEventDetail>) => void;
"onCalciteInternalTabNavSlotChange"?: (event: CalciteTabNavCustomEvent<Element[]>) => void;
/**
* Emits when the selected `calcite-tab` changes.
*/
Expand Down
62 changes: 36 additions & 26 deletions packages/calcite-components/src/components/tab-nav/tab-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
focusElementInGroup,
FocusElementInGroupDestination,
getElementDir,
slotChangeGetAssignedElements,
} from "../../utils/dom";
import { createObserver } from "../../utils/observers";
import { Scale } from "../interfaces";
Expand Down Expand Up @@ -73,6 +74,14 @@ export class TabNav implements LocalizedComponent, T9nComponent {
*/
@Prop({ mutable: true }) selectedTitle: HTMLCalciteTabTitleElement = null;

@Watch("selectedTitle")
selectedTitleChanged(): void {
this.calciteInternalTabChange.emit({
tab: this.selectedTabId,
});
this.updateActiveIndicator();
}

/**
* Specifies the size of the component inherited from the parent `calcite-tabs`, defaults to `m`.
*
Expand Down Expand Up @@ -126,29 +135,6 @@ export class TabNav implements LocalizedComponent, T9nComponent {
/* wired up by t9n util */
}

@Watch("selectedTabId")
async selectedTabIdChanged(): Promise<void> {
if (
localStorage &&
this.storageId &&
this.selectedTabId !== undefined &&
this.selectedTabId !== null
) {
localStorage.setItem(`calcite-tab-nav-${this.storageId}`, JSON.stringify(this.selectedTabId));
}

this.calciteInternalTabChange.emit({
tab: this.selectedTabId,
});

this.selectedTitle = await this.getTabTitleById(this.selectedTabId);
}

@Watch("selectedTitle")
selectedTitleChanged(): void {
this.updateActiveIndicator();
}

//--------------------------------------------------------------------------
//
// Lifecycle
Expand Down Expand Up @@ -297,6 +283,7 @@ export class TabNav implements LocalizedComponent, T9nComponent {
: this.getIndexOfTabTitle(activatedTabTitle);
event.stopPropagation();

this.selectedTitle = activatedTabTitle;
this.scrollTabTitleIntoView(activatedTabTitle);
}

Expand Down Expand Up @@ -360,9 +347,10 @@ export class TabNav implements LocalizedComponent, T9nComponent {
* @param event
*/
@Listen("calciteInternalTabTitleRegister")
updateTabTitles(event: CustomEvent<TabID>): void {
async updateTabTitles(event: CustomEvent<TabID>): Promise<void> {
if ((event.target as HTMLCalciteTabTitleElement).selected) {
this.selectedTabId = event.detail;
this.selectedTitle = await this.getTabTitleById(this.selectedTabId);
}
}

Expand Down Expand Up @@ -395,6 +383,11 @@ export class TabNav implements LocalizedComponent, T9nComponent {
*/
@Event({ cancelable: false }) calciteTabChange: EventEmitter<void>;

/**
* @internal
*/
@Event() calciteInternalTabNavSlotChange: EventEmitter<Element[]>;

/**
* @internal
*/
Expand Down Expand Up @@ -423,6 +416,22 @@ export class TabNav implements LocalizedComponent, T9nComponent {

@State() private selectedTabId: TabID;

@Watch("selectedTabId")
async selectedTabIdChanged(): Promise<void> {
if (
localStorage &&
this.storageId &&
this.selectedTabId !== undefined &&
this.selectedTabId !== null
) {
localStorage.setItem(`calcite-tab-nav-${this.storageId}`, JSON.stringify(this.selectedTabId));
}

this.calciteInternalTabChange.emit({
tab: this.selectedTabId,
});
}

private activeIndicatorEl: HTMLElement;

private activeIndicatorContainerEl: HTMLDivElement;
Expand Down Expand Up @@ -501,10 +510,11 @@ export class TabNav implements LocalizedComponent, T9nComponent {
private onSlotChange = (event: Event): void => {
this.intersectionObserver?.disconnect();

const slottedChildren = (event.target as HTMLSlotElement).assignedElements();
slottedChildren.forEach((child) => {
const slottedElements = slotChangeGetAssignedElements(event, "calcite-tab-title");
slottedElements.forEach((child) => {
this.intersectionObserver?.observe(child);
});
this.calciteInternalTabNavSlotChange.emit(slottedElements);
};

private storeContainerRef = (el: HTMLDivElement) => (this.containerEl = el);
Expand Down
29 changes: 1 addition & 28 deletions packages/calcite-components/src/components/tab/tab.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,4 @@
import {
Component,
Element,
Event,
EventEmitter,
h,
Host,
Listen,
Method,
Prop,
State,
VNode,
} from "@stencil/core";
import { Component, Element, h, Host, Listen, Method, Prop, State, VNode } from "@stencil/core";
import { nodeListToArray } from "../../utils/dom";
import { guid } from "../../utils/guid";
import { Scale } from "../interfaces";
Expand Down Expand Up @@ -81,10 +69,6 @@ export class Tab {
this.parentTabsEl = this.el.closest("calcite-tabs");
}

componentDidLoad(): void {
this.calciteInternalTabRegister.emit();
}

disconnectedCallback(): void {
// Dispatching to body in order to be listened by other elements that are still connected to the DOM.
document.body?.dispatchEvent(
Expand All @@ -94,17 +78,6 @@ export class Tab {
);
}

//--------------------------------------------------------------------------
//
// Events
//
//--------------------------------------------------------------------------

/**
* @internal
*/
@Event({ cancelable: false }) calciteInternalTabRegister: EventEmitter<void>;

//--------------------------------------------------------------------------
//
// Event Listeners
Expand Down
42 changes: 34 additions & 8 deletions packages/calcite-components/src/components/tabs/tabs.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,15 +374,15 @@ describe("calcite-tabs", () => {
await page.setContent(html`
<calcite-tabs>
<calcite-tab-nav slot="title-group">
<calcite-tab-title id="tab-title-1" closable>Tab 1 Title</calcite-tab-title>
<calcite-tab-title id="tab-title-2" closable>Tab 2 Title</calcite-tab-title>
<calcite-tab-title id="tab-title-3" closable>Tab 3 Title</calcite-tab-title>
<calcite-tab-title id="tab-title-4" closable selected>Tab 4 Title</calcite-tab-title>
<calcite-tab-title id="tab-title-1" data-name="1" closable>Tab 1 Title</calcite-tab-title>
<calcite-tab-title id="tab-title-2" data-name="2" closable>Tab 2 Title</calcite-tab-title>
<calcite-tab-title id="tab-title-3" data-name="3" closable>Tab 3 Title</calcite-tab-title>
<calcite-tab-title id="tab-title-4" data-name="4" closable selected>Tab 4 Title</calcite-tab-title>
</calcite-tab-nav>
<calcite-tab id="tab-1">Tab 1 Content</calcite-tab>
<calcite-tab id="tab-2">Tab 2 Content</calcite-tab>
<calcite-tab id="tab-3">Tab 3 Content</calcite-tab>
<calcite-tab id="tab-4" selected>Tab 4 Content</calcite-tab>
<calcite-tab id="tab-1" data-name="1">Tab 1 Content</calcite-tab>
<calcite-tab id="tab-2" data-name="2">Tab 2 Content</calcite-tab>
<calcite-tab id="tab-3" data-name="3">Tab 3 Content</calcite-tab>
<calcite-tab id="tab-4" data-name="4" selected>Tab 4 Content</calcite-tab>
</calcite-tabs>
`);

Expand Down Expand Up @@ -431,5 +431,31 @@ describe("calcite-tabs", () => {
expect(await allTabs[2].isVisible()).toBe(false);
expect(await allTabs[3].isVisible()).toBe(true);
});

it("should allow selecting the next tab after previous one is closed and removed from DOM", async () => {
type TestWindow = GlobalTestProps<{ selectedTitleTab: string }>;

await page.evaluate(() => {
document.addEventListener("calciteTabChange", (event) => {
(window as TestWindow).selectedTitleTab = (event.target as HTMLCalciteTabNavElement).selectedTitle.innerText;
});
document.addEventListener("calciteTabClose", (event) => {
const closedTabTitleElement = event.target as HTMLCalciteTabTitleElement;
const name = closedTabTitleElement.dataset.name;
closedTabTitleElement.remove();
document.querySelector(`calcite-tab[data-name]=${name}`).remove();
});
});

const tab2 = await page.find("#tab-title-2");

await page.click(`#tab-title-1 >>> .${TabTitleCSS.closeButton}`);
await tab2.click();
await page.waitForChanges();

const selectedTitleOnEmit = await page.evaluate(() => (window as TestWindow).selectedTitleTab);

expect(selectedTitleOnEmit).toBe("Tab 2 Title");
});
});
});
Loading
Loading