carbon-components-angular
Version:
Next generation components
1,183 lines (1,178 loc) • 130 kB
JavaScript
import * as i0 from '@angular/core';
import { Component, Input, HostBinding, ViewChild, EventEmitter, Directive, Output, forwardRef, HostListener, ContentChildren, TemplateRef, ViewChildren, ContentChild, NgModule } from '@angular/core';
import * as i1 from 'carbon-components-angular/utils';
import { UtilsModule } from 'carbon-components-angular/utils';
import { Subscription } from 'rxjs';
import * as i2 from 'carbon-components-angular/i18n';
import { I18nModule } from 'carbon-components-angular/i18n';
import * as i3 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i2$1 from 'carbon-components-angular/tooltip';
import { TooltipModule } from 'carbon-components-angular/tooltip';
/**
* There are two ways to create a tab, this class is a collection of features
* & metadata required by both.
*/
class BaseTabHeader {
constructor(elementRef, changeDetectorRef, eventService, renderer) {
this.elementRef = elementRef;
this.changeDetectorRef = changeDetectorRef;
this.eventService = eventService;
this.renderer = renderer;
/**
* Set to `true` to have `Tab` items cached and not reloaded on tab switching.
* Duplicated from `cds-tabs` to support standalone headers.
*/
this.cacheActive = false;
/**
* Visual style of the tab list: `line` or `contained`.
*/
this.type = "line";
/**
* Theme for contained tabs: `dark` or `light`.
*/
this.theme = "dark";
/**
* **Contained only**: Evenly sized tabs across the row (**must** have fewer than 9 tabs).
*/
this.fullWidth = false;
/**
* Show a close control on each tab.
*/
this.dismissable = false;
/**
* Scroll the active tab into view on focus/select.
*/
this.scrollIntoView = false;
/**
* Debounce (ms) for tab list scroll events; affects overflow chevron updates.
*/
this.scrollDebounceWait = 200;
this.tabsClass = true;
// width of the overflow buttons
this.OVERFLOW_BUTTON_OFFSET = 44;
this.longPressMultiplier = 3;
this.clickMultiplier = 1.5;
this.longPressInterval = null;
this.tickInterval = null;
this.scrollDebounceTimer = null;
}
get containedClass() {
return this.type === "contained";
}
get themeClass() {
return this.theme === "light";
}
get dismissableClass() {
return this.dismissable;
}
get iconSizeDefaultClass() {
return this.iconSize === "default";
}
get iconSizeLgClass() {
return this.iconSize === "lg";
}
get layoutSizeLgClass() {
return this.iconSize === "lg";
}
get hasHorizontalOverflow() {
const tabList = this.headerContainer.nativeElement;
return tabList.scrollWidth > tabList.clientWidth;
}
get leftOverflowNavButtonHidden() {
const tabList = this.headerContainer.nativeElement;
return !this.hasHorizontalOverflow || !tabList.scrollLeft;
}
get rightOverflowNavButtonHidden() {
const tabList = this.headerContainer.nativeElement;
return !this.hasHorizontalOverflow ||
(tabList.scrollLeft + tabList.clientWidth) === tabList.scrollWidth;
}
handleScroll() {
// Debounce the change detection trigger so the scroll arrow visibility
// updates do not fire on every scroll tick.
if (this.scrollDebounceWait <= 0) {
this.changeDetectorRef.markForCheck();
return;
}
clearTimeout(this.scrollDebounceTimer);
this.scrollDebounceTimer = setTimeout(() => {
this.changeDetectorRef.markForCheck();
}, this.scrollDebounceWait);
}
handleOverflowNavClick(direction, numOftabs = 0) {
const tabList = this.headerContainer.nativeElement;
const { clientWidth, scrollLeft, scrollWidth } = tabList;
if (direction > 0) {
tabList.scrollLeft = Math.min(scrollLeft + (scrollWidth / numOftabs) * this.clickMultiplier, scrollWidth - clientWidth);
}
else if (direction < 0) {
tabList.scrollLeft = Math.max(scrollLeft - (scrollWidth / numOftabs) * this.clickMultiplier, 0);
}
}
handleOverflowNavMouseDown(direction) {
const tabList = this.headerContainer.nativeElement;
this.longPressInterval = setTimeout(() => {
// Manually overriding scroll behvior to `auto` to make animation work correctly
this.renderer.setStyle(tabList, "scroll-behavior", "auto");
this.tickInterval = setInterval(() => {
tabList.scrollLeft += (direction * this.longPressMultiplier);
// clear interval if scroll reaches left or right edge
if (this.leftOverflowNavButtonHidden || this.rightOverflowNavButtonHidden) {
return () => {
clearInterval(this.tickInterval);
this.handleOverflowNavMouseUp();
};
}
});
return () => clearInterval(this.longPressInterval);
}, 500);
}
/**
* Clear intervals/Timeout & reset scroll behavior
*/
handleOverflowNavMouseUp() {
clearInterval(this.tickInterval);
clearTimeout(this.longPressInterval);
// Reset scroll behavior
this.renderer.setStyle(this.headerContainer.nativeElement, "scroll-behavior", "smooth");
}
}
BaseTabHeader.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: BaseTabHeader, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }, { token: i1.EventService }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component });
BaseTabHeader.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: BaseTabHeader, selector: "ng-component", inputs: { cacheActive: "cacheActive", followFocus: "followFocus", ariaLabel: "ariaLabel", ariaLabelledby: "ariaLabelledby", contentBefore: "contentBefore", contentAfter: "contentAfter", type: "type", theme: "theme", iconSize: "iconSize", fullWidth: "fullWidth", dismissable: "dismissable", scrollIntoView: "scrollIntoView", scrollDebounceWait: "scrollDebounceWait" }, host: { properties: { "class.cds--tabs": "this.tabsClass", "class.cds--tabs--contained": "this.containedClass", "class.cds--tabs--light": "this.themeClass", "class.cds--tabs--dismissable": "this.dismissableClass", "class.cds--tabs__icon--default": "this.iconSizeDefaultClass", "class.cds--tabs__icon--lg": "this.iconSizeLgClass", "class.cds--layout--size-lg": "this.layoutSizeLgClass" } }, viewQueries: [{ propertyName: "headerContainer", first: true, predicate: ["tabList"], descendants: true, static: true }], ngImport: i0, template: "", isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: BaseTabHeader, decorators: [{
type: Component,
args: [{
template: ""
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }, { type: i1.EventService }, { type: i0.Renderer2 }]; }, propDecorators: { cacheActive: [{
type: Input
}], followFocus: [{
type: Input
}], ariaLabel: [{
type: Input
}], ariaLabelledby: [{
type: Input
}], contentBefore: [{
type: Input
}], contentAfter: [{
type: Input
}], type: [{
type: Input
}], theme: [{
type: Input
}], iconSize: [{
type: Input
}], fullWidth: [{
type: Input
}], dismissable: [{
type: Input
}], scrollIntoView: [{
type: Input
}], scrollDebounceWait: [{
type: Input
}], tabsClass: [{
type: HostBinding,
args: ["class.cds--tabs"]
}], containedClass: [{
type: HostBinding,
args: ["class.cds--tabs--contained"]
}], themeClass: [{
type: HostBinding,
args: ["class.cds--tabs--light"]
}], dismissableClass: [{
type: HostBinding,
args: ["class.cds--tabs--dismissable"]
}], iconSizeDefaultClass: [{
type: HostBinding,
args: ["class.cds--tabs__icon--default"]
}], iconSizeLgClass: [{
type: HostBinding,
args: ["class.cds--tabs__icon--lg"]
}], layoutSizeLgClass: [{
type: HostBinding,
args: ["class.cds--layout--size-lg"]
}], headerContainer: [{
type: ViewChild,
args: ["tabList", { static: true }]
}] } });
/**
* Shared inputs, outputs, and selection logic for `[cdsTabHeader]`
* and `cds-tab-header` as we prepare for deprecation.
* Groups use `@ContentChildren(TabHeaderBase)` so both forms appear in DOM order,
* subclasses supply the template and host behavior.
*/
// eslint-disable-next-line @angular-eslint/directive-class-suffix -- abstract base class, not a directive instance
class TabHeaderBase {
constructor() {
/**
* Selected tab; controls whether the linked pane content is shown.
*/
this.active = false;
/**
* Indicates whether or not the `Tab` item is disabled.
*/
this.disabled = false;
/**
* Set to `true` to render this tab header as dismissable.
*/
this.dismissable = false;
/**
* Emits when this header becomes selected.
*/
this.selected = new EventEmitter();
/**
* Emits when this tabs's close button is pressed.
*/
this.tabClose = new EventEmitter();
this._cacheActive = false;
}
/**
* Set to 'true' to have pane reference cached and not reloaded on tab switching.
*/
set cacheActive(shouldCache) {
this._cacheActive = shouldCache;
// Updates the pane references associated with the tab header when cache active is changed.
if (this.paneReference) {
this.paneReference.cacheActive = this.cacheActive;
}
}
get cacheActive() {
return this._cacheActive;
}
/**
* Sets `tabIndex` on the linked `Tab` pane when the pane reference is set.
*/
set paneTabIndex(tabIndex) {
if (this.paneReference) {
this.paneReference.tabIndex = tabIndex;
}
}
/**
* Activates the linked pane and emits `selected`.
*/
selectTab() {
this.focus();
if (!this.disabled) {
this.selected.emit();
this.active = true;
if (this.paneReference) {
this.paneReference.active = true;
}
}
}
}
TabHeaderBase.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeaderBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
TabHeaderBase.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.3.0", type: TabHeaderBase, inputs: { cacheActive: "cacheActive", paneTabIndex: "paneTabIndex", active: "active", disabled: "disabled", icon: "icon", secondaryLabel: "secondaryLabel", dismissable: "dismissable", paneReference: "paneReference", title: "title" }, outputs: { selected: "selected", tabClose: "tabClose" }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeaderBase, decorators: [{
type: Directive
}], propDecorators: { cacheActive: [{
type: Input
}], paneTabIndex: [{
type: Input
}], active: [{
type: Input
}], disabled: [{
type: Input
}], icon: [{
type: Input
}], secondaryLabel: [{
type: Input
}], dismissable: [{
type: Input
}], paneReference: [{
type: Input
}], title: [{
type: Input
}], selected: [{
type: Output
}], tabClose: [{
type: Output
}] } });
/**
* Tab header as an attribute on a focusable host inside `cds-tab-header-group`.
*
* @deprecated as of v5.
* Prefer `cds-tab-header` for icons, secondary labels, dismissable close, and icon-only tabs.
*/
class TabHeader extends TabHeaderBase {
constructor(host) {
super();
this.host = host;
this.type = "button";
this.navItem = true;
this.navLink = true;
}
get tabIndex() {
return this.active ? 0 : -1;
}
get isSelected() {
return this.active;
}
get isDisabled() {
return this.disabled;
}
get ariaSelected() {
return this.active;
}
get ariaDisabled() {
return this.disabled;
}
get hostTitle() {
return this.title ?? null;
}
onClick() {
this.selectTab();
}
onKeyDown(event) {
if (this.dismissable && event.key === "Delete") {
event.stopPropagation();
this.tabClose.emit();
}
}
ngAfterViewInit() {
setTimeout(() => {
this.title = this.title ? this.title : this.host.nativeElement.textContent;
});
}
focus() {
this.host.nativeElement.focus();
}
}
TabHeader.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeader, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
TabHeader.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.3.0", type: TabHeader, selector: "[cdsTabHeader], [ibmTabHeader]", host: { listeners: { "click": "onClick()", "keydown": "onKeyDown($event)" }, properties: { "attr.tabIndex": "this.tabIndex", "class.cds--tabs__nav-item--selected": "this.isSelected", "class.cds--tabs__nav-item--disabled": "this.isDisabled", "attr.type": "this.type", "attr.aria-selected": "this.ariaSelected", "attr.aria-disabled": "this.ariaDisabled", "class.cds--tabs__nav-item": "this.navItem", "class.cds--tabs__nav-link": "this.navLink", "attr.title": "this.hostTitle" } }, providers: [
// tslint:disable-next-line:no-forward-ref
{ provide: TabHeaderBase, useExisting: forwardRef(() => TabHeader) }
], usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeader, decorators: [{
type: Directive,
args: [{
selector: "[cdsTabHeader], [ibmTabHeader]",
providers: [
// tslint:disable-next-line:no-forward-ref
{ provide: TabHeaderBase, useExisting: forwardRef(() => TabHeader) }
]
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }]; }, propDecorators: { tabIndex: [{
type: HostBinding,
args: ["attr.tabIndex"]
}], isSelected: [{
type: HostBinding,
args: ["class.cds--tabs__nav-item--selected"]
}], isDisabled: [{
type: HostBinding,
args: ["class.cds--tabs__nav-item--disabled"]
}], type: [{
type: HostBinding,
args: ["attr.type"]
}], ariaSelected: [{
type: HostBinding,
args: ["attr.aria-selected"]
}], ariaDisabled: [{
type: HostBinding,
args: ["attr.aria-disabled"]
}], navItem: [{
type: HostBinding,
args: ["class.cds--tabs__nav-item"]
}], navLink: [{
type: HostBinding,
args: ["class.cds--tabs__nav-link"]
}], hostTitle: [{
type: HostBinding,
args: ["attr.title"]
}], onClick: [{
type: HostListener,
args: ["click"]
}], onKeyDown: [{
type: HostListener,
args: ["keydown", ["$event"]]
}] } });
class TabHeaderGroup extends BaseTabHeader {
constructor(elementRef, changeDetectorRef, eventService, renderer, i18n) {
super(elementRef, changeDetectorRef, eventService, renderer);
this.elementRef = elementRef;
this.changeDetectorRef = changeDetectorRef;
this.eventService = eventService;
this.renderer = renderer;
this.i18n = i18n;
/**
* i18n strings for overflow controls and the tab list `aria-label` fallback.
*/
this.translations = this.i18n.get().TABS;
/**
* When `true`, sets each tab panel `tabindex` to `-1` for navigation-style usage.
*/
this.isNavigation = false;
/**
* Emits when a tab close control is used (with `dismissable`).
* The emitted value is the tab index.
*/
this.tabClose = new EventEmitter();
this.selectedSubscriptionTracker = new Subscription();
this.closeSubscriptionTracker = new Subscription();
/**
* Index of the selected tab for keyboard logic.
*/
this.currentSelectedTab = 0;
/**
* Focused tab index when `followFocus` is false (manual activation).
*/
this.activeIndex = null;
}
get fullWidthClass() {
return this.distributeWidth;
}
/**
* We use taller rows when any header has a secondary label.
*/
get tallClass() {
return this.hasSecondaryLabelTabs;
}
get hasSecondaryLabelTabs() {
if (!this.tabHeaderQuery || this.type !== "contained") {
return false;
}
return this.tabHeaderQuery.toArray().some(h => h.secondaryLabel != null &&
String(h.secondaryLabel).trim() !== "");
}
/**
* True when `fullWidth` applies (contained, fewer than 9 headers).
*/
get distributeWidth() {
return (this.fullWidth &&
this.type === "contained" &&
(this.tabHeaderQuery ? this.tabHeaderQuery.length < 9 : false));
}
keyboardInput(event) {
const tabHeadersArray = this.tabHeaderQuery.toArray();
if (event.key === "ArrowRight") {
if (this.currentSelectedTab < tabHeadersArray.length - 1) {
event.preventDefault();
if (this.followFocus && !tabHeadersArray[this.currentSelectedTab + 1].disabled) {
tabHeadersArray[this.currentSelectedTab + 1].selectTab();
}
else {
tabHeadersArray[this.currentSelectedTab + 1].focus();
this.currentSelectedTab++;
}
}
else {
event.preventDefault();
if (this.followFocus && !tabHeadersArray[0].disabled) {
tabHeadersArray[0].selectTab();
}
else {
tabHeadersArray[0].focus();
this.currentSelectedTab = 0;
}
}
}
if (event.key === "ArrowLeft") {
if (this.currentSelectedTab > 0) {
event.preventDefault();
if (this.followFocus && !tabHeadersArray[this.currentSelectedTab - 1].disabled) {
tabHeadersArray[this.currentSelectedTab - 1].selectTab();
}
else {
tabHeadersArray[this.currentSelectedTab - 1].focus();
this.currentSelectedTab--;
}
}
else {
event.preventDefault();
if (this.followFocus && !tabHeadersArray[tabHeadersArray.length - 1].disabled) {
tabHeadersArray[tabHeadersArray.length - 1].selectTab();
}
else {
tabHeadersArray[tabHeadersArray.length - 1].focus();
this.currentSelectedTab = tabHeadersArray.length - 1;
}
}
}
if (event.key === "Home") {
event.preventDefault();
if (this.followFocus && !tabHeadersArray[0].disabled) {
tabHeadersArray[0].selectTab();
}
else {
tabHeadersArray[0].focus();
this.currentSelectedTab = 0;
}
}
if (event.key === "End") {
event.preventDefault();
if (this.followFocus && !tabHeadersArray[tabHeadersArray.length - 1].disabled) {
tabHeadersArray[tabHeadersArray.length - 1].selectTab();
}
else {
tabHeadersArray[tabHeadersArray.length - 1].focus();
this.currentSelectedTab = tabHeadersArray.length - 1;
}
}
if ((event.key === " ") && !this.followFocus) {
tabHeadersArray[this.currentSelectedTab].selectTab();
}
}
ngOnInit() {
this.eventService.on(window, "resize", () => this.handleScroll());
}
ngAfterContentInit() {
// Reallocate trackers because subscriptions are permanently closed after unsubscribe
this.selectedSubscriptionTracker.unsubscribe();
this.closeSubscriptionTracker.unsubscribe();
this.selectedSubscriptionTracker = new Subscription();
this.closeSubscriptionTracker = new Subscription();
if (this.tabHeaderQuery) {
this.tabHeaderQuery.toArray()
.forEach(tabHeader => {
tabHeader.cacheActive = this.cacheActive;
tabHeader.dismissable = this.dismissable;
tabHeader.paneTabIndex = this.isNavigation ? null : 0;
});
}
const headersArray = this.tabHeaderQuery.toArray();
headersArray.forEach(tabHeader => {
this.selectedSubscriptionTracker.add(tabHeader.selected.subscribe(() => {
this.currentSelectedTab = this.tabHeaderQuery.toArray().indexOf(tabHeader);
// The Filter takes the current selected tab out, then all other headers are
// deactivated and their associated pane references are also deactivated.
this.tabHeaderQuery.toArray().filter(header => header !== tabHeader)
.forEach(filteredHeader => {
filteredHeader.active = false;
if (filteredHeader.paneReference) {
filteredHeader.paneReference.active = false;
}
});
}));
this.closeSubscriptionTracker.add(tabHeader.tabClose.subscribe(() => {
const index = this.tabHeaderQuery.toArray().indexOf(tabHeader);
this.tabClose.emit(index);
}));
});
this.setFirstTab();
}
ngOnDestroy() {
this.selectedSubscriptionTracker.unsubscribe();
this.closeSubscriptionTracker.unsubscribe();
clearTimeout(this.scrollDebounceTimer);
}
ngOnChanges(changes) {
if (this.tabHeaderQuery) {
if (changes.cacheActive) {
this.tabHeaderQuery.toArray().forEach(tabHeader => tabHeader.cacheActive = this.cacheActive);
}
if (changes.dismissable) {
this.tabHeaderQuery.toArray().forEach(tabHeader => tabHeader.dismissable = this.dismissable);
}
if (changes.isNavigation) {
this.tabHeaderQuery.toArray()
.forEach(tabHeader => tabHeader.paneTabIndex = this.isNavigation ? null : 0);
}
}
}
getSelectedTab() {
const selected = this.tabHeaderQuery.toArray()[this.currentSelectedTab];
if (selected) {
return selected;
}
return {
headingIsTemplate: false,
heading: ""
};
}
/**
* Determines which `Tab` is initially selected.
*/
setFirstTab() {
setTimeout(() => {
const headers = this.tabHeaderQuery.toArray();
let selectedHeader = headers.find(h => h.active || h.paneReference?.active);
if (!selectedHeader && headers.length > 0) {
selectedHeader = headers[0];
}
if (selectedHeader) {
selectedHeader.selectTab();
this.activeIndex = this.currentSelectedTab;
this.changeDetectorRef.markForCheck();
}
});
}
}
TabHeaderGroup.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeaderGroup, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }, { token: i1.EventService }, { token: i0.Renderer2 }, { token: i2.I18n }], target: i0.ɵɵFactoryTarget.Component });
TabHeaderGroup.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TabHeaderGroup, selector: "cds-tab-header-group, ibm-tab-header-group", inputs: { translations: "translations", isNavigation: "isNavigation" }, outputs: { tabClose: "tabClose" }, host: { listeners: { "keydown": "keyboardInput($event)" }, properties: { "class.cds--tabs--full-width": "this.fullWidthClass", "class.cds--tabs--tall": "this.tallClass" } }, queries: [{ propertyName: "tabHeaderQuery", predicate: TabHeaderBase }], viewQueries: [{ propertyName: "headerContainer", first: true, predicate: ["tabList"], descendants: true, static: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
<button
type="button"
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"
(click)="handleOverflowNavClick(-1, tabHeaderQuery.length)"
(pointerdown)="handleOverflowNavMouseDown(-1)"
(pointerup)="handleOverflowNavMouseUp()"
(pointerleave)="handleOverflowNavMouseUp()"
(pointerout)="handleOverflowNavMouseUp()"
(pointercancel)="handleOverflowNavMouseUp()">
<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
class="cds--tab--list"
role="tablist"
[attr.aria-label]="ariaLabel || translations.HEADER_ARIA_LABEL"
[attr.aria-labelledby]="ariaLabelledby || null"
(scroll)="handleScroll()"
#tabList>
<ng-container [ngTemplateOutlet]="contentBefore"></ng-container>
<ng-content></ng-content>
<ng-container [ngTemplateOutlet]="contentAfter"></ng-container>
</div>
<button
type="button"
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"
(click)="handleOverflowNavClick(1, tabHeaderQuery.length)"
(pointerdown)="handleOverflowNavMouseDown(1)"
(pointerup)="handleOverflowNavMouseUp()"
(pointerleave)="handleOverflowNavMouseUp()"
(pointerout)="handleOverflowNavMouseUp()"
(pointercancel)="handleOverflowNavMouseUp()">
<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>
`, isInline: true, dependencies: [{ kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeaderGroup, decorators: [{
type: Component,
args: [{
selector: "cds-tab-header-group, ibm-tab-header-group",
template: `
<button
type="button"
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"
(click)="handleOverflowNavClick(-1, tabHeaderQuery.length)"
(pointerdown)="handleOverflowNavMouseDown(-1)"
(pointerup)="handleOverflowNavMouseUp()"
(pointerleave)="handleOverflowNavMouseUp()"
(pointerout)="handleOverflowNavMouseUp()"
(pointercancel)="handleOverflowNavMouseUp()">
<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
class="cds--tab--list"
role="tablist"
[attr.aria-label]="ariaLabel || translations.HEADER_ARIA_LABEL"
[attr.aria-labelledby]="ariaLabelledby || null"
(scroll)="handleScroll()"
#tabList>
<ng-container [ngTemplateOutlet]="contentBefore"></ng-container>
<ng-content></ng-content>
<ng-container [ngTemplateOutlet]="contentAfter"></ng-container>
</div>
<button
type="button"
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"
(click)="handleOverflowNavClick(1, tabHeaderQuery.length)"
(pointerdown)="handleOverflowNavMouseDown(1)"
(pointerup)="handleOverflowNavMouseUp()"
(pointerleave)="handleOverflowNavMouseUp()"
(pointerout)="handleOverflowNavMouseUp()"
(pointercancel)="handleOverflowNavMouseUp()">
<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>
`
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }, { type: i1.EventService }, { type: i0.Renderer2 }, { type: i2.I18n }]; }, propDecorators: { fullWidthClass: [{
type: HostBinding,
args: ["class.cds--tabs--full-width"]
}], tallClass: [{
type: HostBinding,
args: ["class.cds--tabs--tall"]
}], translations: [{
type: Input
}], isNavigation: [{
type: Input
}], tabClose: [{
type: Output
}], tabHeaderQuery: [{
type: ContentChildren,
args: [TabHeaderBase]
}], headerContainer: [{
type: ViewChild,
args: ["tabList", { static: true }]
}], keyboardInput: [{
type: HostListener,
args: ["keydown", ["$event"]]
}] } });
const VERTICAL_TAB_HEIGHT$1 = 64;
/**
* Vertical tab header group: same children as `cds-tab-header-group`, with
* up/down (and Home/End) keys, gradient overflow, and always-contained type.
*
*
* ```html
* <cds-tabs-vertical-grouped height="400px">
* <cds-tab-header-group-vertical>
* <cds-tab-header [paneReference]="a">A</cds-tab-header>
* <cds-tab-header [paneReference]="b">B</cds-tab-header>
* </cds-tab-header-group-vertical>
* <cds-tab #a>...</cds-tab>
* <cds-tab #b>...</cds-tab>
* </cds-tabs-vertical-grouped>
* ```
*/
class TabHeaderGroupVertical extends BaseTabHeader {
constructor(elementRef, changeDetectorRef, eventService, renderer, i18n) {
super(elementRef, changeDetectorRef, eventService, renderer);
this.elementRef = elementRef;
this.changeDetectorRef = changeDetectorRef;
this.eventService = eventService;
this.renderer = renderer;
this.i18n = i18n;
/**
* i18n strings for the tab list `aria-label` fallback.
*/
this.translations = this.i18n.get().TABS;
/**
* When `true`, sets each tab panel `tabindex` to `-1` for navigation-style usage.
*/
this.isNavigation = false;
/**
* Fires with tab index when a close control is used (with `dismissable`).
*/
this.tabClose = new EventEmitter();
/**
* Set to 'true' to have tabs automatically activated and have their content displayed when they receive focus.
*/
this.followFocus = true;
this.verticalClass = true;
/**
* Index of the selected tab for keyboard logic
*/
this.currentSelectedTab = 0;
/**
* Focused tab index when `followFocus` is false (manual activation).
*/
this.activeIndex = null;
this.isOverflowingTop = false;
this.isOverflowingBottom = false;
this.selectedSubscriptionTracker = new Subscription();
this.closeSubscriptionTracker = new Subscription();
this.resizeObserver = null;
this.type = "contained";
// Cache a stable reference for add/removeEventListener.
this.boundListScrollHandler = () => this.updateOverflowState();
}
/**
* We use taller rows when any header has a secondary label.
*/
get tallClass() {
return this.hasSecondaryLabelTabs;
}
get hasSecondaryLabelTabs() {
if (!this.tabHeaderQuery) {
return false;
}
return this.tabHeaderQuery
.toArray()
.some((h) => h.secondaryLabel != null && h.secondaryLabel !== "");
}
keyboardInput(event) {
if (!this.tabHeaderQuery) {
return;
}
const tabHeadersArray = this.tabHeaderQuery.toArray();
const enabledHeaders = tabHeadersArray.filter((h) => !h.disabled);
if (enabledHeaders.length === 0) {
return;
}
const referenceIndex = this.followFocus
? this.currentSelectedTab
: (this.activeIndex !== null ? this.activeIndex : this.currentSelectedTab);
const currentEnabledIndex = Math.max(0, enabledHeaders.indexOf(tabHeadersArray[referenceIndex]));
let nextEnabledIndex = currentEnabledIndex;
let handled = false;
if (event.key === "ArrowDown") {
nextEnabledIndex = (currentEnabledIndex + 1) % enabledHeaders.length;
handled = true;
}
else if (event.key === "ArrowUp") {
nextEnabledIndex = (enabledHeaders.length + currentEnabledIndex - 1) % enabledHeaders.length;
handled = true;
}
else if (event.key === "Home") {
nextEnabledIndex = 0;
handled = true;
}
else if (event.key === "End") {
nextEnabledIndex = enabledHeaders.length - 1;
handled = true;
}
if (handled) {
event.preventDefault();
const nextHeader = enabledHeaders[nextEnabledIndex];
const nextIndex = tabHeadersArray.indexOf(nextHeader);
if (this.followFocus) {
nextHeader.selectTab();
this.currentSelectedTab = nextIndex;
}
else {
nextHeader.focus();
this.activeIndex = nextIndex;
}
return;
}
if ((event.key === " " || event.key === "Spacebar") && !this.followFocus) {
const focusIndex = this.activeIndex !== null ? this.activeIndex : this.currentSelectedTab;
tabHeadersArray[focusIndex].selectTab();
this.currentSelectedTab = focusIndex;
}
}
handleBlur(event) {
const relatedTarget = event.relatedTarget;
const container = this.headerContainer?.nativeElement;
if (container && relatedTarget && container.contains(relatedTarget)) {
return;
}
if (!this.followFocus) {
this.activeIndex = this.currentSelectedTab;
}
}
ngOnInit() {
this.resizeObserver = new ResizeObserver(() => {
this.updateOverflowState();
this.changeDetectorRef.detectChanges();
});
this.resizeObserver.observe(this.headerContainer.nativeElement);
this.headerContainer.nativeElement.addEventListener("scroll", this.boundListScrollHandler);
}
ngOnDestroy() {
this.selectedSubscriptionTracker.unsubscribe();
this.closeSubscriptionTracker.unsubscribe();
this.resizeObserver?.unobserve(this.headerContainer.nativeElement);
this.resizeObserver = null;
this.headerContainer.nativeElement.removeEventListener("scroll", this.boundListScrollHandler);
}
ngAfterContentInit() {
// Reallocate trackers because subscriptions are permanently closed after unsubscribe
this.selectedSubscriptionTracker.unsubscribe();
this.closeSubscriptionTracker.unsubscribe();
this.selectedSubscriptionTracker = new Subscription();
this.closeSubscriptionTracker = new Subscription();
this.applyHeaderInputs();
this.wireSubscriptions();
this.tabHeaderQuery.changes.subscribe(() => {
// Re-wire when the projected list changes.
this.selectedSubscriptionTracker.unsubscribe();
this.closeSubscriptionTracker.unsubscribe();
this.selectedSubscriptionTracker = new Subscription();
this.closeSubscriptionTracker = new Subscription();
this.applyHeaderInputs();
this.wireSubscriptions();
this.changeDetectorRef.markForCheck();
});
setTimeout(() => {
const headers = this.tabHeaderQuery.toArray();
const activeIdx = headers.findIndex(h => h.active || h.paneReference?.active);
const initialIndex = activeIdx >= 0 ? activeIdx : 0;
this.currentSelectedTab = initialIndex;
this.activeIndex = initialIndex;
headers[initialIndex]?.selectTab();
this.updateOverflowState();
});
}
ngOnChanges(changes) {
if (this.tabHeaderQuery) {
if (changes.cacheActive) {
this.tabHeaderQuery.toArray().forEach(h => h.cacheActive = this.cacheActive);
}
if (changes.dismissable) {
this.tabHeaderQuery.toArray().forEach(h => h.dismissable = this.dismissable);
}
if (changes.isNavigation) {
this.tabHeaderQuery.toArray()
.forEach(h => h.paneTabIndex = this.isNavigation ? null : 0);
}
}
}
updateOverflowState() {
const element = this.headerContainer?.nativeElement;
if (!element) {
return;
}
const halfTabHeight = VERTICAL_TAB_HEIGHT$1 / 2;
this.isOverflowingBottom =
element.scrollTop + element.clientHeight + halfTabHeight <= element.scrollHeight;
this.isOverflowingTop = element.scrollTop > halfTabHeight;
this.changeDetectorRef.markForCheck();
}
scrollSelectedTabIntoView() {
if (!this.scrollIntoView) {
return;
}
const container = this.headerContainer?.nativeElement;
if (!container) {
return;
}
container.scrollTo({
top: Math.max(0, (this.currentSelectedTab - 1) * VERTICAL_TAB_HEIGHT$1),
behavior: "smooth"
});
}
applyHeaderInputs() {
this.tabHeaderQuery.toArray().forEach((header) => {
header.cacheActive = this.cacheActive;
header.dismissable = this.dismissable;
header.paneTabIndex = this.isNavigation ? null : 0;
});
}
wireSubscriptions() {
this.tabHeaderQuery.toArray().forEach((header) => {
this.selectedSubscriptionTracker.add(header.selected.subscribe(() => {
this.currentSelectedTab = this.tabHeaderQuery
.toArray()
.indexOf(header);
this.tabHeaderQuery
.toArray()
.filter((h) => h !== header)
.forEach((other) => {
other.active = false;
if (other.paneReference) {
other.paneReference.active = false;
}
});
this.scrollSelectedTabIntoView();
}));
this.closeSubscriptionTracker.add(header.tabClose.subscribe(() => {
const index = this.tabHeaderQuery.toArray().indexOf(header);
this.tabClose.emit(index);
}));
});
}
}
TabHeaderGroupVertical.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeaderGroupVertical, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }, { token: i1.EventService }, { token: i0.Renderer2 }, { token: i2.I18n }], target: i0.ɵɵFactoryTarget.Component });
TabHeaderGroupVertical.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TabHeaderGroupVertical, selector: "cds-tab-header-group-vertical, ibm-tab-header-group-vertical", inputs: { translations: "translations", isNavigation: "isNavigation", followFocus: "followFocus" }, outputs: { tabClose: "tabClose" }, host: { listeners: { "keydown": "keyboardInput($event)", "blur": "handleBlur($event)" }, properties: { "class.cds--tabs--vertical": "this.verticalClass", "class.cds--tabs--tall": "this.tallClass" } }, queries: [{ propertyName: "tabHeaderQuery", predicate: TabHeaderBase }], viewQueries: [{ propertyName: "headerContainer", first: true, predicate: ["tabList"], descendants: true, static: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
<div *ngIf="isOverflowingTop" class="cds--tab--list-gradient_top"></div>
<div
#tabList
class="cds--tab--list"
role="tablist"
[attr.aria-label]="ariaLabel || translations.HEADER_ARIA_LABEL"
[attr.aria-labelledby]="ariaLabelledby || null">
<ng-container [ngTemplateOutlet]="contentBefore"></ng-container>
<ng-content></ng-content>
<ng-container [ngTemplateOutlet]="contentAfter"></ng-container>
</div>
<div *ngIf="isOverflowingBottom" class="cds--tab--list-gradient_bottom"></div>
`, isInline: true, dependencies: [{ kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeaderGroupVertical, decorators: [{
type: Component,
args: [{
selector: "cds-tab-header-group-vertical, ibm-tab-header-group-vertical",
template: `
<div *ngIf="isOverflowingTop" class="cds--tab--list-gradient_top"></div>
<div
#tabList
class="cds--tab--list"
role="tablist"
[attr.aria-label]="ariaLabel || translations.HEADER_ARIA_LABEL"
[attr.aria-labelledby]="ariaLabelledby || null">
<ng-container [ngTemplateOutlet]="contentBefore"></ng-container>
<ng-content></ng-content>
<ng-container [ngTemplateOutlet]="contentAfter"></ng-container>
</div>
<div *ngIf="isOverflowingBottom" class="cds--tab--list-gradient_bottom"></div>
`
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }, { type: i1.EventService }, { type: i0.Renderer2 }, { type: i2.I18n }]; }, propDecorators: { translations: [{
type: Input
}], isNavigation: [{
type: Input
}], tabClose: [{
type: Output
}], followFocus: [{
type: Input
}], tabHeaderQuery: [{
type: ContentChildren,
args: [TabHeaderBase]
}], headerContainer: [{
type: ViewChild,
args: ["tabList", { static: true }]
}], verticalClass: [{
type: HostBinding,
args: ["class.cds--tabs--vertical"]
}], tallClass: [{
type: HostBinding,
args: ["class.cds--tabs--tall"]
}], keyboardInput: [{
type: HostListener,
args: ["keydown", ["$event"]]
}], handleBlur: [{
type: HostListener,
args: ["blur", ["$event"]]
}] } });
/**
* Tab header with template for label, optional icon, secondary label, badge, and dismissable close.
*
* ```html
* <cds-tab-header-group>
* <cds-tab-header [paneReference]="c1">Dashboard</cds-tab-header>
* <cds-tab-header [paneReference]="c2" [icon]="iconTpl" secondaryLabel="(1/4)">
* Monitoring
* </cds-tab-header>
* </cds-tab-header-group>
* <cds-tab #c1>...</cds-tab>
* <cds-tab #c2>...</cds-tab>
* ```
*/
class TabHeaderComponent extends TabHeaderBase {
constructor() {
super(...arguments);
/**
* Icon-only tab: set `icon` and `iconLabel`.
*/
this.iconOnly = false;
/**
* **Preview**: Icon-only tabs — show a notification dot on the icon.
*/
this.badgeIndicator = false;
/**
* Icon-only tabs: icon size `default` (16px) or `lg` (20px); usually set on the parent group.
*/
this.iconSize = "default";
/**
* `aria-label` for the dismissable close button.
*/
this.closeButtonAriaLabel = "Press delete to remove tab";
/**
* Icon-only tabs: open the tooltip on first render.
*/
this.isTooltipOpen = false;
this.displayContents = "contents";
}
ngAfterViewInit() {
// Mirror the deprecated directive's title-fallback behavior, but read
// from the inner rendered button rather than the `display: contents` host.
setTimeout(() => {
if (!this.title && this.tabButton?.nativeElement) {
const text = this.tabButton.nativeElement.textContent?.trim();
if (text) {
this.title = text;
}
}
});
}
/**
* Focus the rendered tab button (not the host).
*/
focus() {
this.tabButton?.nativeElement?.focus();
}
onTabButtonClick() {
this.selectTab();
}
/**
* `Delete` closes dismissable tabs when focus is on the tab.
*/
onTabButtonKeyDown(event) {
if (this.dismissable && event.key === "Delete") {
event.stopPropagation();
this.tabClose.emit();
}
}
/**
* Close button click; stops propagation so the tab does not activate.
*/
onClose(event) {
event.stopPropagation();
if (this.disabled) {
return;
}
this.tabClose.emit();
}
get resolvedTitle() {
if (this.iconOnly) {
return this.iconLabel || null;
}
return this.title || null;
}
get closeButtonTitle() {
const label = this.tabButton?.nativeElement?.textContent?.trim();
return label ? `Remove ${label} tab` : "Remove tab";
}
}
TabHeaderComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeaderComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
TabHeaderComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TabHeaderComponent, selector: "cds-tab-header, ibm-tab-header", inputs: { iconOnly: "iconOnly", iconLabel: "iconLabel", badgeIndicator: "badgeIndicator", iconSize: "iconSize", closeButtonAriaLabel: "closeButtonAriaLabel", enterDelayMs: "enterDelayMs", leaveDelayMs: "leaveDelayMs", isTooltipOpen: "isTooltipOpen" }, host: { properties: { "style.display": "this.displayContents" } }, providers: [
// tslint:disable-next-line:no-forward-ref
{ provide: TabHeaderBase, useExisting: forwardRef(() => TabHeaderComponent) }
], viewQueries: [{ propertyName: "tabButton", first: true, predicate: ["tabButton"], descendants: true }], usesInheritance: true, ngImport: i0, template: `
<cds-tooltip
*ngIf="iconOnly; else plainButton"
align="bottom"
[autoAlign]="true"
class="cds--icon-tooltip"
[description]="iconLabel"
[enterDelayMs]="enterDelayMs"
[leaveDelayMs]="leaveDelayMs"
[isOpen]="isTooltipOpen"
[disabled]="disabled">