src/tabs/tab-headers.component.ts
The TabHeaders component contains the Tab items and controls scroll functionality
if content has overflow.
AfterContentInit
OnChanges
OnDestroy
OnInit
| selector | cds-tab-headers, ibm-tab-headers |
| template | |
Properties |
|
Methods |
Inputs |
Outputs |
HostBindings |
HostListeners |
Accessors |
constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, eventService: EventService, renderer: Renderer2, i18n: I18n)
|
||||||||||||||||||
|
Defined in src/tabs/tab-headers.component.ts:263
|
||||||||||||||||||
|
Parameters :
|
| tabs | |
Type : QueryList<Tab>
|
|
|
Defined in src/tabs/tab-headers.component.ts:216
|
|
|
List of |
|
| translations | |
Type : any
|
|
Default value : this.i18n.get().TABS
|
|
|
Defined in src/tabs/tab-headers.component.ts:221
|
|
|
i18n strings for overflow controls and the tab list |
|
| ariaLabel | |
Type : string
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:33
|
|
|
Sets the aria label on the nav element. |
|
| ariaLabelledby | |
Type : string
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:37
|
|
|
Sets the aria labelledby on the nav element. |
|
| cacheActive | |
Type : boolean
|
|
Default value : false
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:25
|
|
|
Set to |
|
| contentAfter | |
Type : TemplateRef<any>
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:46
|
|
|
Template projected after tab items inside the tab list. |
|
| contentBefore | |
Type : TemplateRef<any>
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:42
|
|
|
Template projected before tab items inside the tab list. |
|
| dismissable | |
Type : boolean
|
|
Default value : false
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:70
|
|
|
Show a close control on each tab. |
|
| followFocus | |
Type : boolean
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:29
|
|
|
Set to 'true' to have tabs automatically activated and have their content displayed when they receive focus. |
|
| fullWidth | |
Type : boolean
|
|
Default value : false
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:65
|
|
|
Contained only: Evenly sized tabs across the row (must have fewer than 9 tabs). |
|
| iconSize | |
Type : "default" | "lg"
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:60
|
|
|
When using icon-only tabs, icon size: |
|
| scrollDebounceWait | |
Type : number
|
|
Default value : 200
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:80
|
|
|
Debounce (ms) for tab list scroll events; affects overflow chevron updates. |
|
| scrollIntoView | |
Type : boolean
|
|
Default value : false
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:75
|
|
|
Scroll the active tab into view on focus/select. |
|
| theme | |
Type : "dark" | "light"
|
|
Default value : "dark"
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:55
|
|
|
Theme for contained tabs: |
|
| type | |
Type : "line" | "contained"
|
|
Default value : "line"
|
|
|
Inherited from
BaseTabHeader
|
|
|
Defined in
BaseTabHeader:51
|
|
|
Visual style of the tab list: |
|
| tabClose | |
Type : EventEmitter<number>
|
|
|
Defined in src/tabs/tab-headers.component.ts:227
|
|
|
Emits when a tab close control is used (with |
|
| class.cds--tabs--full-width |
Type : boolean
|
|
Defined in src/tabs/tab-headers.component.ts:259
|
| class.cds--tabs--tall |
Type : boolean
|
|
Defined in src/tabs/tab-headers.component.ts:255
|
| class.cds--layout--size-lg |
Type : boolean
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:98
|
| class.cds--tabs |
Type : boolean
|
Default value : true
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:82
|
| class.cds--tabs__icon--default |
Type : boolean
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:92
|
| class.cds--tabs__icon--lg |
Type : boolean
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:95
|
| class.cds--tabs--contained |
Type : boolean
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:83
|
| class.cds--tabs--dismissable |
Type : boolean
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:89
|
| class.cds--tabs--light |
Type : boolean
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:86
|
| blur |
Arguments : '$event'
|
blur(event: FocusEvent)
|
|
Defined in src/tabs/tab-headers.component.ts:348
|
| keydown |
Arguments : '$event'
|
keydown(event)
|
|
Defined in src/tabs/tab-headers.component.ts:298
|
|
Controls the keydown events used for tabbing through the headings. |
| getCloseTitle | ||||||
getCloseTitle(tab: Tab)
|
||||||
|
Defined in src/tabs/tab-headers.component.ts:478
|
||||||
|
Parameters :
Returns :
string
|
| getSelectedTab |
getSelectedTab()
|
|
Defined in src/tabs/tab-headers.component.ts:421
|
|
Returns :
any
|
| handleBlur | ||||||
handleBlur(event: FocusEvent)
|
||||||
Decorators :
@HostListener('blur', ['$event'])
|
||||||
|
Defined in src/tabs/tab-headers.component.ts:348
|
||||||
|
Parameters :
Returns :
void
|
| handleClose |
handleClose(event: Event, tab: Tab, tabIndex: number)
|
|
Defined in src/tabs/tab-headers.component.ts:451
|
|
Emit close index and move focus to a nearby enabled tab.
Returns :
void
|
| handleTabKeyDown |
handleTabKeyDown(event: KeyboardEvent, tab: Tab, index: number)
|
|
Defined in src/tabs/tab-headers.component.ts:363
|
|
Returns :
void
|
| keyboardInput | ||||
keyboardInput(event)
|
||||
Decorators :
@HostListener('keydown', ['$event'])
|
||||
|
Defined in src/tabs/tab-headers.component.ts:298
|
||||
|
Controls the keydown events used for tabbing through the headings.
Parameters :
Returns :
void
|
| ngAfterContentInit |
ngAfterContentInit()
|
|
Defined in src/tabs/tab-headers.component.ts:383
|
|
Returns :
void
|
| ngOnChanges | ||||||
ngOnChanges(changes: SimpleChanges)
|
||||||
|
Defined in src/tabs/tab-headers.component.ts:398
|
||||||
|
Parameters :
Returns :
void
|
| ngOnDestroy |
ngOnDestroy()
|
|
Defined in src/tabs/tab-headers.component.ts:378
|
|
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
Defined in src/tabs/tab-headers.component.ts:369
|
|
Returns :
void
|
| onTabFocus | |||||||||
onTabFocus(ref: HTMLElement, index: number)
|
|||||||||
|
Defined in src/tabs/tab-headers.component.ts:407
|
|||||||||
|
Controls manually focusing tabs.
Parameters :
Returns :
void
|
| Protected scrollTabIntoView | ||||||
scrollTabIntoView(tabEl: HTMLElement | null)
|
||||||
|
Defined in src/tabs/tab-headers.component.ts:486
|
||||||
|
Scroll the given tab element into view if it is not already visible.
Parameters :
Returns :
void
|
| selectTab |
selectTab(ref: HTMLElement, tab: Tab, tabIndex: number)
|
|
Defined in src/tabs/tab-headers.component.ts:432
|
|
Selects
Returns :
void
|
| Protected setFirstTab |
setFirstTab()
|
|
Defined in src/tabs/tab-headers.component.ts:512
|
|
Determines which
Returns :
void
|
| handleOverflowNavClick |
handleOverflowNavClick(direction: number, numOftabs: number)
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:156
|
|
Returns :
void
|
| handleOverflowNavMouseDown | ||||||
handleOverflowNavMouseDown(direction: number)
|
||||||
|
Inherited from
BaseTabHeader
|
||||||
|
Defined in
BaseTabHeader:168
|
||||||
|
Parameters :
Returns :
void
|
| handleOverflowNavMouseUp |
handleOverflowNavMouseUp()
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:193
|
|
Clear intervals/Timeout & reset scroll behavior
Returns :
void
|
| handleScroll |
handleScroll()
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:143
|
|
Returns :
void
|
| activeIndex |
Type : number | null
|
Default value : null
|
|
Defined in src/tabs/tab-headers.component.ts:253
|
|
Focused tab index when |
| allTabHeaders |
Type : QueryList<ElementRef>
|
Decorators :
@ViewChildren('tabItem')
|
|
Defined in src/tabs/tab-headers.component.ts:248
|
|
The DOM element containing the |
| firstVisibleTab |
Type : number
|
Default value : 0
|
|
Defined in src/tabs/tab-headers.component.ts:244
|
|
The index of the first visible tab. |
| headerContainer |
Type : ElementRef<HTMLElement>
|
Decorators :
@ViewChild('tabList', {static: true})
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:232
|
|
Gets the Unordered List element that holds the |
| Private resizeObserver |
Type : ResizeObserver
|
|
Defined in src/tabs/tab-headers.component.ts:263
|
| tabQuery |
Type : QueryList<Tab>
|
Decorators :
@ContentChildren(Tab)
|
|
Defined in src/tabs/tab-headers.component.ts:236
|
|
ContentChild of all the tabs |
| tabs |
Type : QueryList<Tab>
|
|
Defined in src/tabs/tab-headers.component.ts:240
|
|
Set to tabQuery if tabInput is empty |
| Readonly clickMultiplier |
Type : number
|
Default value : 1.5
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:114
|
| currentSelectedTab |
Type : number
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:110
|
|
Controls the manual focusing done by tabbing through headings. |
| Protected longPressInterval |
Type : null
|
Default value : null
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:116
|
| Readonly longPressMultiplier |
Type : number
|
Default value : 3
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:113
|
| Readonly OVERFLOW_BUTTON_OFFSET |
Type : number
|
Default value : 44
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:112
|
| Protected scrollDebounceTimer |
Type : any
|
Default value : null
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:118
|
| tabsClass |
Default value : true
|
Decorators :
@HostBinding('class.cds--tabs')
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:82
|
| Protected tickInterval |
Type : null
|
Default value : null
|
|
Inherited from
BaseTabHeader
|
|
Defined in
BaseTabHeader:117
|
| tallClass |
gettallClass()
|
|
Defined in src/tabs/tab-headers.component.ts:255
|
| fullWidthClass |
getfullWidthClass()
|
|
Defined in src/tabs/tab-headers.component.ts:259
|
| hasSecondaryLabelTabs |
gethasSecondaryLabelTabs()
|
|
Defined in src/tabs/tab-headers.component.ts:275
|
| distributeWidth |
getdistributeWidth()
|
|
Defined in src/tabs/tab-headers.component.ts:285
|
|
True when
Returns :
boolean
|
import {
Component,
QueryList,
Input,
Output,
EventEmitter,
HostListener,
HostBinding,
ViewChild,
ContentChildren,
AfterContentInit,
ViewChildren,
ElementRef,
OnChanges,
SimpleChanges,
OnDestroy,
OnInit,
ChangeDetectorRef,
Renderer2
} from "@angular/core";
import { EventService } from "carbon-components-angular/utils";
import { I18n } from "carbon-components-angular/i18n";
import { BaseTabHeader } from "./base-tab-header.component";
import { Tab } from "./tab.component";
/**
* The `TabHeaders` component contains the `Tab` items and controls scroll functionality
* if content has overflow.
*/
@Component({
selector: "cds-tab-headers, ibm-tab-headers",
template: `
<button
type="button"
(click)="handleOverflowNavClick(-1, tabs.length)"
(pointerdown)="handleOverflowNavMouseDown(-1)"
(pointerup)="handleOverflowNavMouseUp()"
(pointerleave)="handleOverflowNavMouseUp()"
(pointerout)="handleOverflowNavMouseUp()"
class="cds--tab--overflow-nav-button cds--tab--overflow-nav-button--previous"
[ngClass]="{
'cds--tab--overflow-nav-button--hidden': leftOverflowNavButtonHidden
}"
[attr.aria-hidden]="leftOverflowNavButtonHidden"
[attr.tabindex]="-1"
[attr.aria-label]="translations.BUTTON_ARIA_LEFT"
[attr.title]="translations.BUTTON_ARIA_LEFT">
<svg
focusable="false"
preserveAspectRatio="xMidYMid meet"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
width="16"
height="16"
viewBox="0 0 16 16"
aria-hidden="true">
<path d="M5 8L10 3 10.7 3.7 6.4 8 10.7 12.3 10 13z"></path>
</svg>
</button>
<div
#tabList
class="cds--tab--list"
role="tablist"
[attr.aria-label]="ariaLabel || translations.HEADER_ARIA_LABEL"
[attr.aria-labelledby]="ariaLabelledby || null"
(scroll)="handleScroll()">
<ng-container [ngTemplateOutlet]="contentBefore"></ng-container>
<ng-container *ngFor="let tab of tabs; let i = index;">
<cds-tooltip
*ngIf="tab.iconOnly; else inlineTabItem"
align="bottom"
[autoAlign]="true"
class="cds--icon-tooltip"
[description]="tab.iconLabel"
[enterDelayMs]="tab.enterDelayMs ?? 100"
[leaveDelayMs]="tab.leaveDelayMs ?? 300"
[isOpen]="tab.isTooltipOpen"
[disabled]="tab.disabled">
<ng-container *ngTemplateOutlet="tabItemTpl; context: { tab: tab, i: i }"></ng-container>
</cds-tooltip>
<ng-template #inlineTabItem>
<ng-container *ngTemplateOutlet="tabItemTpl; context: { tab: tab, i: i }"></ng-container>
</ng-template>
<div
*ngIf="dismissable"
class="cds--tabs__nav-item--close">
<button
type="button"
[attr.tabindex]="-1"
[attr.aria-disabled]="tab.disabled"
[attr.aria-hidden]="!(tab.active && !tab.disabled)"
[disabled]="tab.disabled"
class="cds--tabs__nav-item--close-icon"
[ngClass]="{
'cds--tabs__nav-item--close-icon--selected': tab.active,
'cds--tabs__nav-item--close-icon--disabled': tab.disabled
}"
[attr.title]="getCloseTitle(tab)"
(click)="handleClose($event, tab, i)">
<svg
focusable="false"
preserveAspectRatio="xMidYMid meet"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
width="16"
height="16"
viewBox="0 0 32 32"
[attr.aria-label]="tab.closeButtonAriaLabel"
[attr.aria-hidden]="!(tab.active && !tab.disabled)">
<path d="M17.4141 16L24 9.4141 22.5859 8 16 14.5859 9.4143 8 8 9.4141 14.5859 16 8 22.5859 9.4143 24 16 17.4141 22.5859 24 24 22.5859 17.4141 16z"></path>
</svg>
</button>
</div>
</ng-container>
<ng-container [ngTemplateOutlet]="contentAfter"></ng-container>
</div>
<ng-template #tabItemTpl let-tab="tab" let-i="i">
<button
#tabItem
role="tab"
[attr.aria-selected]="tab.active"
[attr.tabindex]="(tab.active?0:-1)"
[attr.aria-controls]="tab.id"
[attr.aria-disabled]="tab.disabled"
[attr.aria-label]="tab.iconOnly ? tab.iconLabel : null"
[disabled]="tab.disabled"
[ngClass]="{
'cds--tabs__nav-item--selected': tab.active,
'cds--tabs__nav-item--disabled': tab.disabled,
'cds--tabs__nav-item--icon-only': tab.iconOnly,
'cds--tabs__nav-item--icon-only__20': tab.iconOnly && iconSize === 'lg'
}"
class="cds--tabs__nav-item cds--tabs__nav-link"
type="button"
draggable="false"
id="{{tab.id}}-header"
[attr.title]="tab.iconOnly ? tab.iconLabel : (tab.title || (!tab.headingIsTemplate ? tab.heading : null))"
(focus)="onTabFocus(tabItem, i)"
(keydown)="handleTabKeyDown($event, tab, i)"
(click)="selectTab(tabItem, tab, i)">
<ng-container *ngIf="tab.iconOnly; else labeledTab">
<ng-container [ngTemplateOutlet]="tab.icon"></ng-container>
<span
*ngIf="!tab.disabled && tab.badgeIndicator"
class="cds--badge-indicator"
aria-hidden="true">
</span>
</ng-container>
<ng-template #labeledTab>
<div class="cds--tabs__nav-item-label-wrapper">
<div *ngIf="dismissable && tab.icon" class="cds--tabs__nav-item--icon-left">
<ng-container [ngTemplateOutlet]="tab.icon"></ng-container>
</div>
<span class="cds--tabs__nav-item-label">
<ng-container *ngIf="!tab.headingIsTemplate">
{{ tab.heading }}
</ng-container>
<ng-template
*ngIf="tab.headingIsTemplate"
[ngTemplateOutlet]="tab.heading"
[ngTemplateOutletContext]="{$implicit: tab.context}">
</ng-template>
</span>
<div
*ngIf="!dismissable && tab.icon"
class="cds--tabs__nav-item--icon">
<ng-container [ngTemplateOutlet]="tab.icon"></ng-container>
</div>
</div>
<div
*ngIf="hasSecondaryLabelTabs && tab.secondaryLabel"
class="cds--tabs__nav-item-secondary-label"
[attr.title]="tab.secondaryLabel">
{{ tab.secondaryLabel }}
</div>
</ng-template>
</button>
</ng-template>
<button
type="button"
(click)="handleOverflowNavClick(1, tabs.length)"
(pointerdown)="handleOverflowNavMouseDown(1)"
(pointerup)="handleOverflowNavMouseUp()"
(pointerleave)="handleOverflowNavMouseUp()"
(pointerout)="handleOverflowNavMouseUp()"
class="cds--tab--overflow-nav-button cds--tab--overflow-nav-button--next"
[ngClass]="{
'cds--tab--overflow-nav-button--hidden': rightOverflowNavButtonHidden
}"
[attr.aria-hidden]="rightOverflowNavButtonHidden"
[attr.tabindex]="-1"
[attr.aria-label]="translations.BUTTON_ARIA_RIGHT"
[attr.title]="translations.BUTTON_ARIA_RIGHT">
<svg
focusable="false"
preserveAspectRatio="xMidYMid meet"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
width="16"
height="16"
viewBox="0 0 16 16"
aria-hidden="true">
<path d="M11 8L6 13 5.3 12.3 9.6 8 5.3 3.7 6 3z"></path>
</svg>
</button>
`
})
export class TabHeaders extends BaseTabHeader implements AfterContentInit, OnChanges, OnDestroy, OnInit {
/**
* List of `Tab` components.
*/
// disable the next line because we need to rename the input
// tslint:disable-next-line
@Input("tabs") tabInput: QueryList<Tab>;
/**
* i18n strings for overflow controls and the tab list `aria-label` fallback.
*/
@Input() translations = this.i18n.get().TABS;
/**
* Emits when a tab close control is used (with `dismissable`).
* The emitted value is the tab index.
*/
@Output() tabClose: EventEmitter<number> = new EventEmitter<number>();
/**
* Gets the Unordered List element that holds the `Tab` headings from the view DOM.
*/
@ViewChild("tabList", { static: true }) headerContainer: ElementRef<HTMLElement>;
/**
* ContentChild of all the tabs
*/
@ContentChildren(Tab) tabQuery: QueryList<Tab>;
/**
* Set to tabQuery if tabInput is empty
*/
tabs: QueryList<Tab>;
/**
* The index of the first visible tab.
*/
firstVisibleTab = 0;
/**
* The DOM element containing the `Tab` headings displayed.
*/
@ViewChildren("tabItem") allTabHeaders: QueryList<ElementRef>;
/**
* Focused tab index when `followFocus` is false (manual activation).
*/
activeIndex: number | null = null;
@HostBinding("class.cds--tabs--tall") get tallClass() {
return this.hasSecondaryLabelTabs;
}
@HostBinding("class.cds--tabs--full-width") get fullWidthClass() {
return this.distributeWidth;
}
private resizeObserver: ResizeObserver;
constructor(
protected elementRef: ElementRef,
protected changeDetectorRef: ChangeDetectorRef,
protected eventService: EventService,
protected renderer: Renderer2,
protected i18n: I18n
) {
super(elementRef, changeDetectorRef, eventService, renderer);
}
get hasSecondaryLabelTabs(): boolean {
if (!this.tabs || this.type !== "contained") {
return false;
}
return this.tabs.toArray().some(tab => typeof tab.secondaryLabel !== "undefined" && tab.secondaryLabel !== null);
}
/**
* True when `fullWidth` applies (contained, fewer than 9 tabs).
*/
get distributeWidth(): boolean {
return (
this.fullWidth &&
this.type === "contained" &&
(this.tabs ? this.tabs.length < 9 : false)
);
}
// keyboard accessibility
/**
* Controls the keydown events used for tabbing through the headings.
*/
@HostListener("keydown", ["$event"])
keyboardInput(event) {
const tabsArray = this.tabs.toArray();
const enabledTabs = tabsArray.filter(tab => !tab.disabled);
if (enabledTabs.length === 0) {
return;
}
const referenceIndex = this.followFocus ?
this.currentSelectedTab :
(this.activeIndex !== null ? this.activeIndex : this.currentSelectedTab);
const currentEnabledIndex = Math.max(0, enabledTabs.indexOf(tabsArray[referenceIndex]));
let nextEnabledIndex = currentEnabledIndex;
let handled = false;
if (event.key === "ArrowRight") {
nextEnabledIndex = (currentEnabledIndex + 1) % enabledTabs.length;
handled = true;
} else if (event.key === "ArrowLeft") {
nextEnabledIndex = (enabledTabs.length + currentEnabledIndex - 1) % enabledTabs.length;
handled = true;
} else if (event.key === "Home") {
nextEnabledIndex = 0;
handled = true;
} else if (event.key === "End") {
nextEnabledIndex = enabledTabs.length - 1;
handled = true;
}
if (handled) {
event.preventDefault();
const nextTab = enabledTabs[nextEnabledIndex];
const nextIndex = tabsArray.indexOf(nextTab);
if (this.followFocus) {
this.selectTab(this.allTabHeaders.toArray()[nextIndex].nativeElement, nextTab, nextIndex);
} else {
this.activeIndex = nextIndex;
}
this.allTabHeaders.toArray()[nextIndex].nativeElement.focus();
return;
}
if ((event.key === " " || event.key === "Spacebar") && !this.followFocus) {
const focusIndex = this.activeIndex !== null ? this.activeIndex : this.currentSelectedTab;
this.selectTab(this.allTabHeaders.toArray()[focusIndex].nativeElement, tabsArray[focusIndex], focusIndex);
}
}
@HostListener("blur", ["$event"])
handleBlur(event: FocusEvent) {
const relatedTarget = event.relatedTarget as Node | null;
const container = this.headerContainer?.nativeElement;
if (container && relatedTarget && container.contains(relatedTarget)) {
return;
}
// Reset active index to selected tab index when followFocus is false
if (!this.followFocus) {
this.activeIndex = this.currentSelectedTab;
}
}
/**
* `Delete` closes dismissable tabs.
*/
handleTabKeyDown(event: KeyboardEvent, tab: Tab, index: number) {
if (this.dismissable && event.key === "Delete") {
this.handleClose(event, tab, index);
}
}
ngOnInit(): void {
// Update scroll on resize
this.resizeObserver = new ResizeObserver(() => {
// Need to explicitly trigger change detection since this runs outside Angular zone
this.changeDetectorRef.detectChanges();
});
this.resizeObserver.observe(this.headerContainer.nativeElement);
}
ngOnDestroy(): void {
this.resizeObserver?.unobserve(this.headerContainer.nativeElement);
clearTimeout(this.scrollDebounceTimer);
}
ngAfterContentInit() {
if (!this.tabInput) {
this.tabs = this.tabQuery;
} else {
this.tabs = this.tabInput;
}
this.tabs.forEach(tab => tab.cacheActive = this.cacheActive);
this.tabs.changes.subscribe(() => {
this.setFirstTab();
this.changeDetectorRef.markForCheck();
});
this.setFirstTab();
}
ngOnChanges(changes: SimpleChanges) {
if (this.tabs && changes.cacheActive) {
this.tabs.forEach(tab => tab.cacheActive = this.cacheActive);
}
}
/**
* Controls manually focusing tabs.
*/
onTabFocus(ref: HTMLElement, index: number) {
if (this.followFocus) {
this.currentSelectedTab = index;
} else {
this.activeIndex = index;
}
// reset scroll left because we're already handling it
this.headerContainer.nativeElement.parentElement.scrollLeft = 0;
if (this.scrollIntoView) {
this.scrollTabIntoView(this.allTabHeaders.toArray()[index]?.nativeElement);
}
}
getSelectedTab(): any {
const selected = this.tabs.find(tab => tab.active);
if (selected) {
return selected;
}
return { headingIsTemplate: false, heading: "" };
}
/**
* Selects `Tab` 'tab' and moves it into view on the view DOM if it is not already.
*/
selectTab(ref: HTMLElement, tab: Tab, tabIndex: number) {
if (tab.disabled) {
return;
}
this.currentSelectedTab = tabIndex;
this.activeIndex = tabIndex;
this.tabs.forEach(_tab => _tab.active = false);
tab.active = true;
tab.doSelect();
if (this.scrollIntoView) {
this.scrollTabIntoView(this.allTabHeaders.toArray()[tabIndex]?.nativeElement);
}
}
/**
* Emit close index and move focus to a nearby enabled tab.
*/
handleClose(event: Event, tab: Tab, tabIndex: number) {
event.stopPropagation();
if (tab.disabled) {
return;
}
tab.tabClose.emit();
this.tabClose.emit(tabIndex);
// Move focus to a neighboring enabled tab (next-then-previous).
const headers = this.allTabHeaders?.toArray() ?? [];
const findNextEnabled = (start: number, step: number) => {
let i = start;
while (i >= 0 && i < headers.length) {
const candidate = this.tabs.toArray()[i];
if (candidate && !candidate.disabled && i !== tabIndex) {
return headers[i]?.nativeElement;
}
i += step;
}
return null;
};
const nextEl = findNextEnabled(tabIndex + 1, 1) || findNextEnabled(tabIndex - 1, -1);
if (nextEl) {
(nextEl as HTMLElement).focus();
}
}
getCloseTitle(tab: Tab): string {
const label = !tab.headingIsTemplate && typeof tab.heading === "string" ? ` ${tab.heading}` : "";
return `Remove${label} tab`;
}
/**
* Scroll the given tab element into view if it is not already visible.
*/
protected scrollTabIntoView(tabEl: HTMLElement | null) {
if (!tabEl || !this.headerContainer?.nativeElement) {
return;
}
const container = this.headerContainer.nativeElement;
if (container.scrollWidth <= container.clientWidth) {
return;
}
const buttonWidth = this.OVERFLOW_BUTTON_OFFSET;
const tabWidth = tabEl.getBoundingClientRect().width;
const start = tabEl.offsetLeft;
const end = start + tabWidth;
const visibleStart = container.scrollLeft + buttonWidth;
const visibleEnd = container.scrollLeft + container.clientWidth - buttonWidth;
if (start < visibleStart) {
container.scrollLeft = start - buttonWidth;
} else if (end > visibleEnd) {
container.scrollLeft = end + buttonWidth - container.clientWidth;
}
}
/**
* Determines which `Tab` is initially selected.
*/
protected setFirstTab() {
setTimeout(() => {
let firstTab = this.tabs.find(tab => tab.active);
if (!firstTab && this.tabs.first) {
firstTab = this.tabs.first;
firstTab.active = true;
}
if (firstTab) {
this.currentSelectedTab = this.tabs.toArray().indexOf(firstTab);
this.activeIndex = this.currentSelectedTab;
firstTab.doSelect();
}
});
}
}