diff --git a/projects/igniteui-angular/src/lib/tabs/tab-item.component.ts b/projects/igniteui-angular/src/lib/tabs/tab-item.component.ts index df3d323b4f5..3f051d4e3e6 100644 --- a/projects/igniteui-angular/src/lib/tabs/tab-item.component.ts +++ b/projects/igniteui-angular/src/lib/tabs/tab-item.component.ts @@ -18,7 +18,7 @@ import { IgxTabItemTemplateDirective } from './tabs.directives'; templateUrl: 'tab-item.component.html' }) -export class IgxTabItemComponent implements IgxTabItemBase { +export class IgxTabItemComponent extends IgxTabItemBase { /** * Gets the group associated with the tab. @@ -84,6 +84,7 @@ export class IgxTabItemComponent implements IgxTabItemBase { private _disabled = false; constructor(private _tabs: IgxTabsBase, private _element: ElementRef) { + super(); this._nativeTabItem = _element; } @@ -158,9 +159,7 @@ export class IgxTabItemComponent implements IgxTabItemBase { @HostListener('window:resize', ['$event']) public onResize(event) { if (this.isSelected) { - this._tabs.selectedIndicator.nativeElement.style.visibility = 'visible'; - this._tabs.selectedIndicator.nativeElement.style.width = `${this.nativeTabItem.nativeElement.offsetWidth}px`; - this._tabs.selectedIndicator.nativeElement.style.transform = `translate(${this.nativeTabItem.nativeElement.offsetLeft}px)`; + this._tabs.transformIndicatorAnimation(this.nativeTabItem.nativeElement); } } @@ -241,63 +240,36 @@ export class IgxTabItemComponent implements IgxTabItemBase { return this.relatedGroup ? this.relatedGroup.isSelected : this._isSelected; } set isSelected(newValue: boolean) { - if (this.relatedGroup) { - this.relatedGroup.isSelected = newValue; - } else if (this._isSelected !== newValue) { - this._isSelected = newValue; - if (this._isSelected) { - this.select(); - } + if (!this.disabled && this.isSelected !== newValue) { + this._tabs.performSelectionChange(newValue ? this : null); } } /** * @hidden */ - get index(): number { - if (this._tabs.tabs) { - return this._tabs.tabs.toArray().indexOf(this); + public select(): void { + if (!this.disabled && !this.isSelected) { + this._tabs.performSelectionChange(this); } } /** * @hidden */ - public select(focusDelay = 200): void { - if (this.relatedGroup) { - this.relatedGroup.select(focusDelay); - } else { - this._isSelected = true; - this._tabs.onTabItemSelected.emit({ tab: this, group: null }); - this.handleTabSelectionAnimation(); - } - } - - private handleTabSelectionAnimation(): void { - const tabElement = this.nativeTabItem.nativeElement; - - // Scroll to the left - if (tabElement.offsetLeft < this._tabs.offset) { - this._tabs.scrollElement(tabElement, false); - } - - // Scroll to the right - const viewPortOffsetWidth = this._tabs.viewPort.nativeElement.offsetWidth; - const delta = (tabElement.offsetLeft + tabElement.offsetWidth) - (viewPortOffsetWidth + this._tabs.offset); - // Fix for IE 11, a difference is accumulated from the widths calculations - if (delta > 1) { - this._tabs.scrollElement(tabElement, true); + get index(): number { + if (this._tabs.tabs) { + return this._tabs.tabs.toArray().indexOf(this); } - - this.transformIndicatorAnimation(tabElement); + return -1; } - private transformIndicatorAnimation(element: HTMLElement): void { - if (this._tabs && this._tabs.selectedIndicator) { - this._tabs.selectedIndicator.nativeElement.style.visibility = `visible`; - this._tabs.selectedIndicator.nativeElement.style.width = `${element.offsetWidth}px`; - this._tabs.selectedIndicator.nativeElement.style.transform = `translate(${element.offsetLeft}px)`; - } + /** + * @hidden + */ + public setSelectedInternal(newValue: boolean) { + this._isSelected = newValue; + this.tabindex = newValue ? 0 : -1; } private onKeyDown(isLeftArrow: boolean, index = null): void { @@ -308,7 +280,7 @@ export class IgxTabItemComponent implements IgxTabItemBase { : (this._tabs.selectedIndex === tabsArray.length - 1) ? 0 : this._tabs.selectedIndex + 1; } const tab = tabsArray[index]; - tab.select(200); + tab.select(); } /** @@ -330,4 +302,5 @@ export class IgxTabItemComponent implements IgxTabItemBase { public get context(): any { return this.relatedGroup ? this.relatedGroup : this; } + } diff --git a/projects/igniteui-angular/src/lib/tabs/tabs-group.component.ts b/projects/igniteui-angular/src/lib/tabs/tabs-group.component.ts index 365df64bcfd..454735f867e 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs-group.component.ts +++ b/projects/igniteui-angular/src/lib/tabs/tabs-group.component.ts @@ -19,12 +19,7 @@ import { IgxTabsBase, IgxTabsGroupBase } from './tabs.common'; templateUrl: 'tabs-group.component.html' }) -export class IgxTabsGroupComponent implements IgxTabsGroupBase, AfterContentInit, AfterViewChecked { - - /** - * @hidden - */ - private _isSelected = false; +export class IgxTabsGroupComponent extends IgxTabsGroupBase implements AfterContentInit, AfterViewChecked { /** * An @Input property that allows you to enable/disable the `IgxTabGroupComponent`. @@ -68,12 +63,8 @@ export class IgxTabsGroupComponent implements IgxTabsGroupBase, AfterContentInit return this._isSelected; } public set isSelected(newValue: boolean) { - if (this._isSelected !== newValue) { - if (newValue) { - this.select(); - } else { - this._isSelected = newValue; - } + if (!this.disabled && this.isSelected !== newValue) { + this._tabs.performSelectionChange(newValue ? this.relatedTab : null); } } @@ -84,8 +75,10 @@ export class IgxTabsGroupComponent implements IgxTabsGroupBase, AfterContentInit protected tabTemplate: IgxTabItemTemplateDirective; private _tabTemplate: TemplateRef; + private _isSelected = false; constructor(private _tabs: IgxTabsBase, private _element: ElementRef) { + super(); } /** @@ -103,7 +96,7 @@ export class IgxTabsGroupComponent implements IgxTabsGroupBase, AfterContentInit @HostListener('window:resize', ['$event']) public onResize(event) { if (this.isSelected) { - this.transformContentAnimation(0); + this._tabs.transformContentAnimation(this.relatedTab, 0); } } @@ -137,6 +130,7 @@ export class IgxTabsGroupComponent implements IgxTabsGroupBase, AfterContentInit if (this._tabs.groups) { return this._tabs.groups.toArray().indexOf(this); } + return -1; } /** @@ -168,12 +162,6 @@ export class IgxTabsGroupComponent implements IgxTabsGroupBase, AfterContentInit public ngAfterViewChecked() { this._element.nativeElement.setAttribute('aria-labelledby', `igx-tab-item-${this.index}`); this._element.nativeElement.setAttribute('id', `igx-tabs__group-${this.index}`); - - if (this.isSelected) { - const tabItem = this.relatedTab.nativeTabItem.nativeElement; - this.transformContentAnimation(0); - this.transformIndicatorAnimation(tabItem); - } } /** @@ -186,53 +174,18 @@ export class IgxTabsGroupComponent implements IgxTabsGroupBase, AfterContentInit * this.tab.select(); *} *``` - * @param focusDelay A number representing the expected delay. */ - public select(focusDelay = 200): void { - if (this.disabled || this.isSelected) { - return; - } - - this._isSelected = true; - this.relatedTab.tabindex = 0; - - if (focusDelay !== 0) { - setTimeout(() => { - this.relatedTab.nativeTabItem.nativeElement.focus(); - }, focusDelay); + public select(): void { + if (!this.disabled && !this.isSelected) { + this._tabs.performSelectionChange(this.relatedTab); } - this.handleSelection(); - this._tabs.onTabItemSelected.emit({ tab: this._tabs.tabs.toArray()[this.index], group: this }); } - private handleSelection(): void { - const tabElement = this.relatedTab.nativeTabItem.nativeElement; - - // Scroll to the left - if (tabElement.offsetLeft < this._tabs.offset) { - this._tabs.scrollElement(tabElement, false); - } - - // Scroll to the right - const viewPortOffsetWidth = this._tabs.viewPort.nativeElement.offsetWidth; - const delta = (tabElement.offsetLeft + tabElement.offsetWidth) - (viewPortOffsetWidth + this._tabs.offset); - // Fix for IE 11, a difference is accumulated from the widths calculations - if (delta > 1) { - this._tabs.scrollElement(tabElement, true); - } - - this.transformContentAnimation(0.2); - this.transformIndicatorAnimation(tabElement); - } - - private transformContentAnimation(duration: number): void { - const contentOffset = this._tabs.tabsContainer.nativeElement.offsetWidth * this.index; - this._tabs.contentsContainer.nativeElement.style.transitionDuration = `${duration}s`; - this._tabs.contentsContainer.nativeElement.style.transform = `translate(${-contentOffset}px)`; + /** + * @hidden + */ + public setSelectedInternal(newValue: boolean) { + this._isSelected = newValue; } - private transformIndicatorAnimation(element: HTMLElement): void { - this._tabs.selectedIndicator.nativeElement.style.width = `${element.offsetWidth}px`; - this._tabs.selectedIndicator.nativeElement.style.transform = `translate(${element.offsetLeft}px)`; - } } diff --git a/projects/igniteui-angular/src/lib/tabs/tabs.common.ts b/projects/igniteui-angular/src/lib/tabs/tabs.common.ts index da71385da44..bb186b57ea9 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs.common.ts +++ b/projects/igniteui-angular/src/lib/tabs/tabs.common.ts @@ -15,15 +15,21 @@ export abstract class IgxTabsBase { onTabItemSelected: EventEmitter<{}>; // TODO: Define event arg interface! hasContentTabs: boolean; scrollElement(element: any, scrollRight: boolean) {} + performSelectionChange(newTab: IgxTabItemBase) {} + transformContentAnimation(tab: IgxTabItemBase, duration: number) {} + transformIndicatorAnimation(element: HTMLElement) {} } /** @hidden */ -export interface IgxTabItemBase { +export abstract class IgxTabItemBase { nativeTabItem: ElementRef; - select(focusDelay?: number); + get index(): number { return 0; } + select(): void {} + setSelectedInternal(newValue: boolean) {} } /** @hidden */ -export interface IgxTabsGroupBase { - select(focusDelay?: number); +export abstract class IgxTabsGroupBase { + select(): void {} + setSelectedInternal(newValue: boolean) {} } diff --git a/projects/igniteui-angular/src/lib/tabs/tabs.component.spec.ts b/projects/igniteui-angular/src/lib/tabs/tabs.component.spec.ts index 042a7707168..5a39eb20050 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs.component.spec.ts +++ b/projects/igniteui-angular/src/lib/tabs/tabs.component.spec.ts @@ -531,11 +531,13 @@ describe('IgxTabs', () => { }); describe('Tabs-only Mode With Initial Selection Set on TabItems Tests', () => { + let router; let fixture; let tabsComp; let theTabs; beforeEach(async(() => { + router = TestBed.get(Router); fixture = TestBed.createComponent(TabsTabsOnlyModeTest1Component); tabsComp = fixture.componentInstance.tabs; fixture.detectChanges(); @@ -576,6 +578,13 @@ describe('IgxTabs', () => { expect(theTabs[2].nativeTabItem.nativeElement.classList.contains(tabItemSelectedCssClass)).toBe(true); }); + it('should hide the selection indicator when no tab item is selected', () => { + expect(tabsComp.selectedIndicator.nativeElement.style.visibility).toBe('visible'); + theTabs[1].isSelected = false; + fixture.detectChanges(); + expect(tabsComp.selectedIndicator.nativeElement.style.visibility).toBe('hidden'); + }); + }); describe('Tabs-only Mode With Initial Selection Set on Tabs Component Tests', () => { diff --git a/projects/igniteui-angular/src/lib/tabs/tabs.component.ts b/projects/igniteui-angular/src/lib/tabs/tabs.component.ts index 3b001681d50..cea0771974a 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs.component.ts +++ b/projects/igniteui-angular/src/lib/tabs/tabs.component.ts @@ -23,7 +23,7 @@ import { IgxIconModule } from '../icon/index'; import { IgxTabItemComponent } from './tab-item.component'; import { IgxTabsGroupComponent } from './tabs-group.component'; import { IgxLeftButtonStyleDirective, IgxRightButtonStyleDirective, IgxTabItemTemplateDirective } from './tabs.directives'; -import { IgxTabsBase } from './tabs.common'; +import { IgxTabsBase, IgxTabItemBase } from './tabs.common'; export enum TabsType { FIXED = 'fixed', @@ -71,8 +71,14 @@ export class IgxTabsComponent implements IgxTabsBase, AfterViewInit, OnDestroy { public set selectedIndex(index: number) { const newIndex = typeof index !== 'number' ? parseInt(index, 10) : index; if (this._selectedIndex !== newIndex) { - this._selectedIndex = newIndex; - this.setSelectedGroup(); + if (this.tabs && this.tabs.length > 0) { + const newTab = this.tabs.toArray()[newIndex]; + if (newTab) { + this.performSelectionChange(newTab); + } + } else { + this._selectedIndex = newIndex; + } } } @@ -109,7 +115,8 @@ export class IgxTabsComponent implements IgxTabsBase, AfterViewInit, OnDestroy { * } * ``` */ - @Output() public onTabItemDeselected = new EventEmitter(); + @Output() + public onTabItemDeselected = new EventEmitter(); /** * Emitted when a tab item is selected. @@ -126,7 +133,8 @@ export class IgxTabsComponent implements IgxTabsBase, AfterViewInit, OnDestroy { * } * ``` */ - @Output() public onTabItemSelected = new EventEmitter(); + @Output() + public onTabItemSelected = new EventEmitter(); /** * @hidden @@ -242,40 +250,6 @@ export class IgxTabsComponent implements IgxTabsBase, AfterViewInit, OnDestroy { return `${css} ${this.class}`; } - /** - * @hidden - */ - @HostListener('onTabItemSelected', ['$event']) - public selectedGroupHandler(args) { - if (this.hasContentTabs) { - const theTabsArray = this.tabs.toArray(); - if (this.selectedIndex !== -1 && this.selectedIndex !== args.tab.index && theTabsArray[this.selectedIndex] !== undefined) { - theTabsArray[this.selectedIndex].isSelected = false; - this.onTabItemDeselected.emit({ tab: theTabsArray[this.selectedIndex], groups: null }); - } - this.selectedIndex = args.tab.index; - } else { - const prevSelectedIndex = this.selectedIndex; - if (prevSelectedIndex !== -1 && this.groups && this.groups.toArray()[prevSelectedIndex] !== undefined) { - this.onTabItemDeselected.emit( - { - tab: this.groups.toArray()[prevSelectedIndex].relatedTab, - group: this.groups.toArray()[prevSelectedIndex] - }); - } - if (args.group) { - this.selectedIndex = args.group.index; - } - if (this.groups) { - this.groups.forEach((p) => { - if (p.index !== this.selectedIndex) { - this.deselectGroup(p); - } - }); - } - } - } - /** * @hidden */ @@ -332,7 +306,12 @@ export class IgxTabsComponent implements IgxTabsBase, AfterViewInit, OnDestroy { } requestAnimationFrame(() => { - this.setSelectedGroup(); + const newTab = this.tabs.toArray()[this._selectedIndex]; + if (newTab) { + this.performSelection(newTab); + } else { + this.hideIndicator(); + } }); this._groupChanges$ = this.groups.changes.subscribe(() => { @@ -349,62 +328,19 @@ export class IgxTabsComponent implements IgxTabsBase, AfterViewInit, OnDestroy { } } - private setSelectedGroup(): void { + private resetSelectionOnCollectionChanged(): void { requestAnimationFrame(() => { - if (this.hasContentTabs) { - if (this.selectedIndex < 0 || this.selectedIndex >= this.contentTabs.length) { - this.selectedIndicator.nativeElement.style.visibility = 'hidden'; - } else { - this.selectGroupByIndex(this.selectedIndex); - } + const currentTab = this.tabs.toArray()[this.selectedIndex]; + if (currentTab) { + this.performSelectionChange(currentTab); + } else if (this.selectedIndex >= this.tabs.length) { + this.performSelectionChange(this.tabs.last); } else { - if (this.selectedIndex < 0 || this.selectedIndex >= this.groups.length) { - this._selectedIndex = 0; - } - this.selectGroupByIndex(this.selectedIndex); + this.hideIndicator(); } }); } - private resetSelectionOnCollectionChanged(): void { - setTimeout(() => { - if (this.groups.toArray()[this.selectedIndex] !== undefined) { - // persist the selected index and applied it to the new collection - this.selectGroupByIndex(this.selectedIndex); - } else { - if (this.selectedIndex >= this.groups.length) { - // in case the selected index is no longer valid, select the last group in the new collection - this.selectGroupByIndex(this.groups.length - 1); - } - } - }, 0); - } - - private selectGroupByIndex(selectedIndex: number): void { - if (this.hasContentTabs) { - const aTab = this.tabs.toArray()[selectedIndex]; - if (aTab) { - aTab.select(); - } - } else { - const selectableGroups = this.groups.filter((selectableGroup) => !selectableGroup.disabled); - const group = selectableGroups[selectedIndex]; - if (group) { - group.select(0); - } - } - } - - private deselectGroup(group: IgxTabsGroupComponent): void { - // Cannot deselect the selected tab - this will mean that there will be not selected tab left - if (group.disabled || this.selectedTabItem.index === group.index) { - return; - } - - group.isSelected = false; - group.relatedTab.tabindex = -1; - } - private scroll(scrollRight: boolean): void { const tabsArray = this.tabs.toArray(); for (const tab of tabsArray) { @@ -422,6 +358,104 @@ export class IgxTabsComponent implements IgxTabsBase, AfterViewInit, OnDestroy { } } } + + /** + * @hidden + */ + public performSelectionChange(newTab: IgxTabItemBase): void { + const oldTab = this.selectedTabItem; + if (oldTab) { + this.performDeselection(oldTab); + } + if (newTab) { + this.performSelection(newTab); + } else { + // if there is no new selected tab hide the selection indicator + this.hideIndicator(); + } + } + + private performDeselection(oldTab: IgxTabItemBase): void { + oldTab.setSelectedInternal(false); + const oldTabRelatedGroup = this.groups.toArray()[oldTab.index]; + if (oldTabRelatedGroup) { + oldTabRelatedGroup.setSelectedInternal(false); + } + this._selectedIndex = -1; + this.onTabItemDeselected.emit({ tab: oldTab, group: oldTabRelatedGroup }); + } + + private performSelection(newTab: IgxTabItemBase): void { + newTab.setSelectedInternal(true); + this._selectedIndex = newTab.index; + + let newTabRelatedGroup = null; + if (!this.hasContentTabs && this.groups) { + newTabRelatedGroup = this.groups.toArray()[newTab.index]; + if (newTabRelatedGroup) { + newTabRelatedGroup.setSelectedInternal(true); + } + } + + this.onTabItemSelected.emit({ tab: newTab, group: newTabRelatedGroup }); + + requestAnimationFrame(() => { + // bring the new selected tab into view if it is not + this.bringNewTabIntoView(newTab); + // animate the new selection indicator + this.transformIndicatorAnimation(newTab.nativeTabItem.nativeElement); + // animate the new tab's group content + if (!this.hasContentTabs) { + this.transformContentAnimation(newTab, 0.2); + } + }); + } + + private bringNewTabIntoView(newTab: IgxTabItemBase): void { + const tabNativeElement = newTab.nativeTabItem.nativeElement; + + // Scroll left if there is need + if (tabNativeElement.offsetLeft < this.offset) { + this.scrollElement(tabNativeElement, false); + } + + // Scroll right if there is need + const viewPortOffsetWidth = this.viewPort.nativeElement.offsetWidth; + const delta = (tabNativeElement.offsetLeft + tabNativeElement.offsetWidth) - (viewPortOffsetWidth + this.offset); + + // Fix for IE 11, a difference is accumulated from the widths calculations + if (delta > 1) { + this.scrollElement(tabNativeElement, true); + } + } + + /** + * @hidden + */ + // animation for the new panel/group (not needed for tab only mode) + public transformContentAnimation(tab: IgxTabItemBase, duration: number): void { + const contentOffset = this.tabsContainer.nativeElement.offsetWidth * tab.index; + this.contentsContainer.nativeElement.style.transitionDuration = `${duration}s`; + this.contentsContainer.nativeElement.style.transform = `translate(${-contentOffset}px)`; + } + + /** + * @hidden + */ + public transformIndicatorAnimation(element: HTMLElement): void { + if (this.selectedIndicator) { + this.selectedIndicator.nativeElement.style.visibility = 'visible'; + this.selectedIndicator.nativeElement.style.width = `${element.offsetWidth}px`; + this.selectedIndicator.nativeElement.style.transform = `translate(${element.offsetLeft}px)`; + } + } + + public hideIndicator(): void { + if (this.selectedIndicator) { + this.selectedIndicator.nativeElement.style.visibility = 'hidden'; + } + } + } /** diff --git a/src/app/tabs-routing/tabs-routing.sample.html b/src/app/tabs-routing/tabs-routing.sample.html index 2e49bad0a25..ed88cd43a4b 100644 --- a/src/app/tabs-routing/tabs-routing.sample.html +++ b/src/app/tabs-routing/tabs-routing.sample.html @@ -7,6 +7,11 @@

Routing sample

+ + + + +