From efd7189440c59509aa33f211267eedccd50bf3c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:32:04 +0000 Subject: [PATCH 1/4] Initial plan From d745c3b0359e743f642e9e706a689a1aece72070 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:41:57 +0000 Subject: [PATCH 2/4] fix(tabs): allow tab selection without content and add tests Co-authored-by: Lipata <2621802+Lipata@users.noreply.github.com> --- .../src/lib/tabs/tab-header.directive.ts | 4 +- .../src/lib/tabs/tabs/tabs.component.spec.ts | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/tabs/tab-header.directive.ts b/projects/igniteui-angular/src/lib/tabs/tab-header.directive.ts index 772a5918609..965c00248b2 100644 --- a/projects/igniteui-angular/src/lib/tabs/tab-header.directive.ts +++ b/projects/igniteui-angular/src/lib/tabs/tab-header.directive.ts @@ -40,9 +40,7 @@ export abstract class IgxTabHeaderDirective implements IgxTabHeaderBase { /** @hidden */ @HostListener('click') public onClick() { - if (this.tab.panelComponent) { - this.tabs.selectTab(this.tab, true); - } + this.tabs.selectTab(this.tab, true); } /** @hidden */ diff --git a/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts b/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts index 6abb3ebd87a..71aca0cd41d 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts +++ b/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts @@ -865,6 +865,35 @@ describe('IgxTabs', () => { fixture.detectChanges(); expect(tabsComp.selectedIndicator.nativeElement.style.visibility).toBe('hidden'); }); + + it('should allow clicking on tabs without content to select them', fakeAsync(() => { + tick(100); + fixture.detectChanges(); + + // Initially Tab 2 should be selected (based on the test component) + expect(tabItems[1].selected).toBe(true); + expect(tabsComp.selectedIndex).toBe(1); + + // Click on Tab 1 (which has no content) + headerElements[0].dispatchEvent(new Event('click', { bubbles: true })); + tick(200); + fixture.detectChanges(); + + // Tab 1 should now be selected + expect(tabsComp.selectedIndex).toBe(0); + expect(tabItems[0].selected).toBe(true); + expect(tabItems[1].selected).toBe(false); + + // Click on Tab 3 (which also has no content) + headerElements[2].dispatchEvent(new Event('click', { bubbles: true })); + tick(200); + fixture.detectChanges(); + + // Tab 3 should now be selected + expect(tabsComp.selectedIndex).toBe(2); + expect(tabItems[2].selected).toBe(true); + expect(tabItems[0].selected).toBe(false); + })); }); describe('Tabs-only Mode With Initial Selection Set on Tabs Component Tests', () => { @@ -895,6 +924,26 @@ describe('IgxTabs', () => { expect(headerElements[2].classList.contains(tabItemSelectedCssClass)).toBe(true); }); + it('should allow keyboard navigation for tabs without content', fakeAsync(() => { + fixture.detectChanges(); + tick(100); + + // Initially Tab 3 should be selected (selectedIndex = 2) + expect(tabsComp.selectedIndex).toBe(2); + expect(tabItems[2].selected).toBe(true); + + // Focus on Tab 3 and navigate left + headerElements[2].focus(); + headerElements[2].dispatchEvent(KEY_LEFT_EVENT); + tick(200); + fixture.detectChanges(); + + // Should move to Tab 2 + expect(tabsComp.selectedIndex).toBe(1); + expect(tabItems[1].selected).toBe(true); + expect(tabItems[2].selected).toBe(false); + })); + }); describe('Events', () => { From 0e1c7f26d4a955e98551b8259051a2144646a225 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:45:39 +0000 Subject: [PATCH 3/4] docs(tabs): update README to document tabs without content behavior Co-authored-by: Lipata <2621802+Lipata@users.noreply.github.com> --- .../src/lib/tabs/tabs/README.md | 33 +++++++++++++-- .../src/lib/tabs/tabs/tabs.component.spec.ts | 41 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/tabs/tabs/README.md b/projects/igniteui-angular/src/lib/tabs/tabs/README.md index 42bd2c0cee4..f6da2a04100 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs/README.md +++ b/projects/igniteui-angular/src/lib/tabs/tabs/README.md @@ -3,11 +3,11 @@ ## Description _igx-tabs component allows you to add a tabs component with tab items, positioned at the top, and item content in your application. The tabs in Ignite UI for Angular can be composed with the following components and directives:_ -- *igx-tab-item* - single content area that holds header and content components +- *igx-tab-item* - single content area that holds header and optionally content components - *igx-tab-header* - holds the title and/or icon of the item and you can add them with `igxTabHeaderIcon` and `igxTabHeaderLabel` -- *igx-tab-content* - represents the wrapper of the content that needs to be displayed +- *igx-tab-content* - represents the wrapper of the content that needs to be displayed (optional) -Each item (`igx-tab-item`) contains header (`igx-tab-header`) and content (`igx-tab-content`). When a tab is clicked, the associated content is selected and visualized into a single container. There should always be a selected tab. Only one tab can be selected at a time. +Each item (`igx-tab-item`) contains a header (`igx-tab-header`) and optionally content (`igx-tab-content`). When a tab is clicked, the associated content is selected and visualized into a single container. If a tab has no content, it can still be selected and used for navigation purposes. There should always be a selected tab. Only one tab can be selected at a time. A walkthrough of how to get started can be found [here](https://www.infragistics.com/products/ignite-ui-angular/angular/components/tabs). ---------- @@ -59,6 +59,33 @@ A walkthrough of how to get started can be found [here](https://www.infragistics +## Tabs without content (Navigation-only) + +For navigation purposes, you can create tabs without content: + + + + + home + Home + + + + + + info + About + + + + + + contact_page + Contact + + + + # API Summary diff --git a/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts b/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts index 71aca0cd41d..6a61099ba78 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts +++ b/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts @@ -944,6 +944,47 @@ describe('IgxTabs', () => { expect(tabItems[2].selected).toBe(false); })); + it('should fire events when clicking on tabs without content', fakeAsync(() => { + // Set up a clean test with the first test group (tab 2 initially selected) + fixture = TestBed.createComponent(TabsTabsOnlyModeTest1Component); + tabsComp = fixture.componentInstance.tabs; + fixture.detectChanges(); + tabItems = tabsComp.items.toArray(); + headerElements = tabItems.map(item => item.headerComponent.nativeElement); + + const indexChangingSpy = spyOn(tabsComp.selectedIndexChanging, 'emit'); + const indexChangeSpy = spyOn(tabsComp.selectedIndexChange, 'emit'); + const itemChangeSpy = spyOn(tabsComp.selectedItemChange, 'emit'); + + tick(100); + fixture.detectChanges(); + + // Initially Tab 2 should be selected (index 1) + expect(tabsComp.selectedIndex).toBe(1); + + // Click on Tab 1 (which has no content) + headerElements[0].dispatchEvent(new Event('click', { bubbles: true })); + tick(200); + fixture.detectChanges(); + + // Tab should be selected + expect(tabsComp.selectedIndex).toBe(0); + + // Events should have fired + expect(indexChangingSpy).toHaveBeenCalledWith({ + owner: tabsComp, + cancel: false, + oldIndex: 1, + newIndex: 0 + }); + expect(indexChangeSpy).toHaveBeenCalledWith(0); + expect(itemChangeSpy).toHaveBeenCalledWith({ + owner: tabsComp, + oldItem: tabItems[1], + newItem: tabItems[0] + }); + })); + }); describe('Events', () => { From 9828a7bbe29898da981bf35c2de952dac25e8fe1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:50:02 +0000 Subject: [PATCH 4/4] revert: remove changes to move to existing PR #16068 Co-authored-by: Lipata <2621802+Lipata@users.noreply.github.com> --- .../src/lib/tabs/tab-header.directive.ts | 4 +- .../src/lib/tabs/tabs/README.md | 33 +------ .../src/lib/tabs/tabs/tabs.component.spec.ts | 90 ------------------- 3 files changed, 6 insertions(+), 121 deletions(-) diff --git a/projects/igniteui-angular/src/lib/tabs/tab-header.directive.ts b/projects/igniteui-angular/src/lib/tabs/tab-header.directive.ts index 965c00248b2..772a5918609 100644 --- a/projects/igniteui-angular/src/lib/tabs/tab-header.directive.ts +++ b/projects/igniteui-angular/src/lib/tabs/tab-header.directive.ts @@ -40,7 +40,9 @@ export abstract class IgxTabHeaderDirective implements IgxTabHeaderBase { /** @hidden */ @HostListener('click') public onClick() { - this.tabs.selectTab(this.tab, true); + if (this.tab.panelComponent) { + this.tabs.selectTab(this.tab, true); + } } /** @hidden */ diff --git a/projects/igniteui-angular/src/lib/tabs/tabs/README.md b/projects/igniteui-angular/src/lib/tabs/tabs/README.md index f6da2a04100..42bd2c0cee4 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs/README.md +++ b/projects/igniteui-angular/src/lib/tabs/tabs/README.md @@ -3,11 +3,11 @@ ## Description _igx-tabs component allows you to add a tabs component with tab items, positioned at the top, and item content in your application. The tabs in Ignite UI for Angular can be composed with the following components and directives:_ -- *igx-tab-item* - single content area that holds header and optionally content components +- *igx-tab-item* - single content area that holds header and content components - *igx-tab-header* - holds the title and/or icon of the item and you can add them with `igxTabHeaderIcon` and `igxTabHeaderLabel` -- *igx-tab-content* - represents the wrapper of the content that needs to be displayed (optional) +- *igx-tab-content* - represents the wrapper of the content that needs to be displayed -Each item (`igx-tab-item`) contains a header (`igx-tab-header`) and optionally content (`igx-tab-content`). When a tab is clicked, the associated content is selected and visualized into a single container. If a tab has no content, it can still be selected and used for navigation purposes. There should always be a selected tab. Only one tab can be selected at a time. +Each item (`igx-tab-item`) contains header (`igx-tab-header`) and content (`igx-tab-content`). When a tab is clicked, the associated content is selected and visualized into a single container. There should always be a selected tab. Only one tab can be selected at a time. A walkthrough of how to get started can be found [here](https://www.infragistics.com/products/ignite-ui-angular/angular/components/tabs). ---------- @@ -59,33 +59,6 @@ A walkthrough of how to get started can be found [here](https://www.infragistics -## Tabs without content (Navigation-only) - -For navigation purposes, you can create tabs without content: - - - - - home - Home - - - - - - info - About - - - - - - contact_page - Contact - - - - # API Summary diff --git a/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts b/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts index 6a61099ba78..6abb3ebd87a 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts +++ b/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts @@ -865,35 +865,6 @@ describe('IgxTabs', () => { fixture.detectChanges(); expect(tabsComp.selectedIndicator.nativeElement.style.visibility).toBe('hidden'); }); - - it('should allow clicking on tabs without content to select them', fakeAsync(() => { - tick(100); - fixture.detectChanges(); - - // Initially Tab 2 should be selected (based on the test component) - expect(tabItems[1].selected).toBe(true); - expect(tabsComp.selectedIndex).toBe(1); - - // Click on Tab 1 (which has no content) - headerElements[0].dispatchEvent(new Event('click', { bubbles: true })); - tick(200); - fixture.detectChanges(); - - // Tab 1 should now be selected - expect(tabsComp.selectedIndex).toBe(0); - expect(tabItems[0].selected).toBe(true); - expect(tabItems[1].selected).toBe(false); - - // Click on Tab 3 (which also has no content) - headerElements[2].dispatchEvent(new Event('click', { bubbles: true })); - tick(200); - fixture.detectChanges(); - - // Tab 3 should now be selected - expect(tabsComp.selectedIndex).toBe(2); - expect(tabItems[2].selected).toBe(true); - expect(tabItems[0].selected).toBe(false); - })); }); describe('Tabs-only Mode With Initial Selection Set on Tabs Component Tests', () => { @@ -924,67 +895,6 @@ describe('IgxTabs', () => { expect(headerElements[2].classList.contains(tabItemSelectedCssClass)).toBe(true); }); - it('should allow keyboard navigation for tabs without content', fakeAsync(() => { - fixture.detectChanges(); - tick(100); - - // Initially Tab 3 should be selected (selectedIndex = 2) - expect(tabsComp.selectedIndex).toBe(2); - expect(tabItems[2].selected).toBe(true); - - // Focus on Tab 3 and navigate left - headerElements[2].focus(); - headerElements[2].dispatchEvent(KEY_LEFT_EVENT); - tick(200); - fixture.detectChanges(); - - // Should move to Tab 2 - expect(tabsComp.selectedIndex).toBe(1); - expect(tabItems[1].selected).toBe(true); - expect(tabItems[2].selected).toBe(false); - })); - - it('should fire events when clicking on tabs without content', fakeAsync(() => { - // Set up a clean test with the first test group (tab 2 initially selected) - fixture = TestBed.createComponent(TabsTabsOnlyModeTest1Component); - tabsComp = fixture.componentInstance.tabs; - fixture.detectChanges(); - tabItems = tabsComp.items.toArray(); - headerElements = tabItems.map(item => item.headerComponent.nativeElement); - - const indexChangingSpy = spyOn(tabsComp.selectedIndexChanging, 'emit'); - const indexChangeSpy = spyOn(tabsComp.selectedIndexChange, 'emit'); - const itemChangeSpy = spyOn(tabsComp.selectedItemChange, 'emit'); - - tick(100); - fixture.detectChanges(); - - // Initially Tab 2 should be selected (index 1) - expect(tabsComp.selectedIndex).toBe(1); - - // Click on Tab 1 (which has no content) - headerElements[0].dispatchEvent(new Event('click', { bubbles: true })); - tick(200); - fixture.detectChanges(); - - // Tab should be selected - expect(tabsComp.selectedIndex).toBe(0); - - // Events should have fired - expect(indexChangingSpy).toHaveBeenCalledWith({ - owner: tabsComp, - cancel: false, - oldIndex: 1, - newIndex: 0 - }); - expect(indexChangeSpy).toHaveBeenCalledWith(0); - expect(itemChangeSpy).toHaveBeenCalledWith({ - owner: tabsComp, - oldItem: tabItems[1], - newItem: tabItems[0] - }); - })); - }); describe('Events', () => {