carbon-components-angular
Version:
Next generation components
1,289 lines (1,281 loc) • 52.2 kB
JavaScript
import * as i0 from '@angular/core';
import { Component, Input, HostBinding, ViewChild, EventEmitter, Directive, Output, 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 i1$1 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i2 from 'carbon-components-angular/i18n';
import { I18nModule } from 'carbon-components-angular/i18n';
/**
* 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.
* Duplicate from `n-tabs` to support standalone headers
*/
this.cacheActive = false;
this.type = "line";
this.theme = "dark";
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;
}
get containedClass() {
return this.type === "contained";
}
get themeClass() {
return this.theme === "light";
}
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() {
this.changeDetectorRef.markForCheck();
}
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" }, host: { properties: { "class.cds--tabs": "this.tabsClass", "class.cds--tabs--contained": "this.containedClass", "class.cds--tabs--light": "this.themeClass" } }, 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
}], tabsClass: [{
type: HostBinding,
args: ["class.cds--tabs"]
}], containedClass: [{
type: HostBinding,
args: ["class.cds--tabs--contained"]
}], themeClass: [{
type: HostBinding,
args: ["class.cds--tabs--light"]
}], headerContainer: [{
type: ViewChild,
args: ["tabList", { static: true }]
}] } });
class TabHeader {
constructor(host) {
this.host = host;
/**
* Indicates whether the `Tab` is active/selected.
* Determines whether it's `TabPanel` is rendered.
*/
this.active = false;
/**
* Indicates whether or not the `Tab` item is disabled.
*/
this.disabled = false;
this.type = "button";
this.ariaSelected = this.active;
this.ariaDisabled = this.disabled;
this.navItem = true;
this.navLink = true;
/**
* Value 'selected' to be emitted after a new `Tab` is selected.
*/
this.selected = new EventEmitter();
this._cacheActive = false;
}
get tabIndex() {
return this.active ? 0 : -1;
}
get isSelected() {
return this.active;
}
get isDisabled() {
return this.disabled;
}
/**
* 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;
}
}
set paneTabIndex(tabIndex) {
if (this.paneReference) {
this.paneReference.tabIndex = tabIndex;
}
}
get cacheActive() {
return this._cacheActive;
}
onClick() {
this.selectTab();
}
ngAfterViewInit() {
setTimeout(() => {
this.title = this.title ? this.title : this.host.nativeElement.textContent;
});
}
selectTab() {
this.focus();
if (!this.disabled) {
this.selected.emit();
this.active = true;
if (this.paneReference) {
this.paneReference.active = true;
}
}
}
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]", inputs: { cacheActive: "cacheActive", paneTabIndex: "paneTabIndex", active: "active", disabled: "disabled", paneReference: "paneReference", title: "title" }, outputs: { selected: "selected" }, host: { listeners: { "click": "onClick()" }, 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.title" } }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeader, decorators: [{
type: Directive,
args: [{
selector: "[cdsTabHeader], [ibmTabHeader]"
}]
}], 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"]
}], cacheActive: [{
type: Input
}], paneTabIndex: [{
type: Input
}], active: [{
type: Input
}], disabled: [{
type: Input
}], 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"]
}], paneReference: [{
type: Input
}], title: [{
type: HostBinding,
args: ["attr.title"]
}, {
type: Input
}], selected: [{
type: Output
}], onClick: [{
type: HostListener,
args: ["click"]
}] } });
class TabHeaderGroup extends BaseTabHeader {
constructor(elementRef, changeDetectorRef, eventService, renderer) {
super(elementRef, changeDetectorRef, eventService, renderer);
this.elementRef = elementRef;
this.changeDetectorRef = changeDetectorRef;
this.eventService = eventService;
this.renderer = renderer;
this.isNavigation = false;
/**
* Keeps track of all the subscriptions to the tab header selection events.
*/
this.selectedSubscriptionTracker = new Subscription();
/**
* Controls the manual focusing done by tabbing through headings.
*/
this.currentSelectedTab = 0;
}
// keyboard accessibility
/**
* Controls the keydown events used for tabbing through the headings.
*/
keyboardInput(event) {
let 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() {
this.selectedSubscriptionTracker.unsubscribe();
if (this.tabHeaderQuery) {
this.tabHeaderQuery.toArray()
.forEach(tabHeader => {
tabHeader.cacheActive = this.cacheActive;
tabHeader.paneTabIndex = this.isNavigation ? null : 0;
});
}
const selectedSubscriptions = this.tabHeaderQuery.toArray().forEach(tabHeader => {
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.selectedSubscriptionTracker.add(selectedSubscriptions);
setTimeout(() => this.tabHeaderQuery.toArray()[this.currentSelectedTab].selectTab());
}
ngOnChanges(changes) {
if (this.tabHeaderQuery) {
if (changes.cacheActive) {
this.tabHeaderQuery.toArray().forEach(tabHeader => tabHeader.cacheActive = this.cacheActive);
}
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: ""
};
}
}
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 }], 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: { isNavigation: "isNavigation" }, host: { listeners: { "keydown": "keyboardInput($event)" } }, queries: [{ propertyName: "tabHeaderQuery", predicate: TabHeader }], 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
}"
(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"
(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
}"
(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: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.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
}"
(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"
(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
}"
(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 }]; }, propDecorators: { isNavigation: [{
type: Input
}], tabHeaderQuery: [{
type: ContentChildren,
args: [TabHeader]
}], headerContainer: [{
type: ViewChild,
args: ["tabList", { static: true }]
}], keyboardInput: [{
type: HostListener,
args: ["keydown", ["$event"]]
}] } });
/**
* The `Tab` component is a child of the `Tabs` component.
* It represents one `Tab` item and its content within a panel of other `Tab` items.
*
*
* `Tab` takes a string or `TemplateRef` for the header, and any content for the body of the tab.
* Disabled states should be handled by the application (ie. switch to the tab, but display some
* indication as to _why_ the tab is disabled).
*
* When the tab is selected the `select` output will be triggered.
* The `select` output will also be triggered for the active tab when the tabs are loaded or updated.
*
*
* Tab with string header:
*
* ```html
* <cds-tab heading='tab1'>
* tab 1 content
* </cds-tab>
* ```
*
* Tab with custom header:
*
* ```html
* <ng-template #tabHeading>
* <svg cdsIcon="facebook"
* size="sm"
* style="margin-right: 7px;">
* </svg>
* Hello Tab 1
* </ng-template>
* <cds-tabs>
* <cds-tab [heading]="tabHeading">
* Tab 1 content <svg cdsIcon="alert" size="lg"></svg>
* </cds-tab>
* <cds-tab heading='Tab2'>
* Tab 2 content
* </cds-tab>
* <cds-tab heading='Tab3'>
* Tab 3 content
* </cds-tab>
* </cds-tabs>
* ```
*/
class Tab {
constructor() {
/**
* Boolean value reflects if the `Tab` is using a custom template for the heading.
* Default value is false.
*/
this.headingIsTemplate = false;
/**
* Indicates whether the `Tab` is active/selected.
* Determines whether it's `TabPanel` is rendered.
*/
this.active = false;
/**
* Indicates whether or not the `Tab` item is disabled.
*/
this.disabled = false;
this.tabIndex = 0;
/**
* Sets the id of the `Tab`. Will be uniquely generated if not provided.
*/
this.id = `n-tab-${Tab.counter++}`;
/**
* Value 'selected' to be emitted after a new `Tab` is selected.
*/
this.selected = new EventEmitter();
/**
* Used to set the id property on the element.
*/
this.attrClass = this.id;
this._cacheActive = false;
}
/**
* Set to true to have Tab items cached and not reloaded on tab switching.
*/
set cacheActive(shouldCache) {
this._cacheActive = shouldCache;
}
get cacheActive() {
return this._cacheActive;
}
/**
* Checks for custom heading template on initialization and updates the value
* of the boolean 'headingIsTemplate'.
*/
ngOnInit() {
if (this.heading instanceof TemplateRef) {
this.headingIsTemplate = true;
}
}
/**
* Emit the status of the `Tab`, specifically 'select' and 'selected' properties.
*/
doSelect() {
this.selected.emit();
}
/**
* Returns value indicating whether this `Tab` should be rendered in a `TabPanel`.
*/
shouldRender() {
return this.active || this.cacheActive;
}
isTemplate(value) {
return value instanceof TemplateRef;
}
}
Tab.counter = 0;
Tab.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: Tab, deps: [], target: i0.ɵɵFactoryTarget.Component });
Tab.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: Tab, selector: "cds-tab, ibm-tab", inputs: { heading: "heading", title: "title", context: "context", active: "active", disabled: "disabled", tabIndex: "tabIndex", id: "id", cacheActive: "cacheActive", tabContent: "tabContent", templateContext: "templateContext" }, outputs: { selected: "selected" }, host: { properties: { "attr.id": "this.attrClass" } }, ngImport: i0, template: `
<div
[attr.tabindex]="tabIndex"
role="tabpanel"
*ngIf="shouldRender()"
class="cds--tab-content"
[ngStyle]="{'display': active ? null : 'none'}"
[attr.aria-labelledby]="id + '-header'"
aria-live="polite">
<ng-template
*ngIf="isTemplate(tabContent)"
[ngTemplateOutlet]="tabContent"
[ngTemplateOutletContext]="{ $implicit: templateContext }">
</ng-template>
<ng-content></ng-content>
</div>
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: Tab, decorators: [{
type: Component,
args: [{
selector: "cds-tab, ibm-tab",
template: `
<div
[attr.tabindex]="tabIndex"
role="tabpanel"
*ngIf="shouldRender()"
class="cds--tab-content"
[ngStyle]="{'display': active ? null : 'none'}"
[attr.aria-labelledby]="id + '-header'"
aria-live="polite">
<ng-template
*ngIf="isTemplate(tabContent)"
[ngTemplateOutlet]="tabContent"
[ngTemplateOutletContext]="{ $implicit: templateContext }">
</ng-template>
<ng-content></ng-content>
</div>
`
}]
}], propDecorators: { heading: [{
type: Input
}], title: [{
type: Input
}], context: [{
type: Input
}], active: [{
type: Input
}], disabled: [{
type: Input
}], tabIndex: [{
type: Input
}], id: [{
type: Input
}], cacheActive: [{
type: Input
}], tabContent: [{
type: Input
}], templateContext: [{
type: Input
}], selected: [{
type: Output
}], attrClass: [{
type: HostBinding,
args: ["attr.id"]
}] } });
/**
* The `TabHeaders` component contains the `Tab` items and controls scroll functionality
* if content has overflow.
*/
class TabHeaders 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;
this.translations = this.i18n.get().TABS;
/**
* The index of the first visible tab.
*/
this.firstVisibleTab = 0;
}
// keyboard accessibility
/**
* Controls the keydown events used for tabbing through the headings.
*/
keyboardInput(event) {
let tabsArray = this.tabs.toArray();
if (event.key === "ArrowRight") {
if (this.currentSelectedTab < this.allTabHeaders.length - 1) {
event.preventDefault();
if (this.followFocus) {
this.selectTab(event.target, tabsArray[this.currentSelectedTab + 1], this.currentSelectedTab);
}
this.allTabHeaders.toArray()[this.currentSelectedTab + 1].nativeElement.focus();
}
else {
event.preventDefault();
if (this.followFocus) {
this.selectTab(event.target, tabsArray[0], 0);
}
this.allTabHeaders.first.nativeElement.focus();
}
}
if (event.key === "ArrowLeft") {
if (this.currentSelectedTab > 0) {
event.preventDefault();
if (this.followFocus) {
this.selectTab(event.target, tabsArray[this.currentSelectedTab - 1], this.currentSelectedTab);
}
this.allTabHeaders.toArray()[this.currentSelectedTab - 1].nativeElement.focus();
}
else {
event.preventDefault();
if (this.followFocus) {
this.selectTab(event.target, tabsArray[this.allTabHeaders.length - 1], this.allTabHeaders.length);
}
this.allTabHeaders.toArray()[this.allTabHeaders.length - 1].nativeElement.focus();
}
}
if (event.key === "Home") {
event.preventDefault();
if (this.followFocus) {
this.selectTab(event.target, tabsArray[0], 0);
}
this.allTabHeaders.toArray()[0].nativeElement.focus();
}
if (event.key === "End") {
event.preventDefault();
if (this.followFocus) {
this.selectTab(event.target, tabsArray[this.allTabHeaders.length - 1], this.allTabHeaders.length);
}
this.allTabHeaders.toArray()[this.allTabHeaders.length - 1].nativeElement.focus();
}
if ((event.key === " " || event.key === "Spacebar") && !this.followFocus) {
this.selectTab(event.target, tabsArray[this.currentSelectedTab], this.currentSelectedTab);
}
}
ngOnInit() {
// 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() {
this.resizeObserver.unobserve(this.headerContainer.nativeElement);
}
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.setFirstTab();
}
ngOnChanges(changes) {
if (this.tabs && changes.cacheActive) {
this.tabs.forEach(tab => tab.cacheActive = this.cacheActive);
}
}
/**
* Controls manually focusing tabs.
*/
onTabFocus(ref, index) {
this.currentSelectedTab = index;
// reset scroll left because we're already handling it
this.headerContainer.nativeElement.parentElement.scrollLeft = 0;
}
getSelectedTab() {
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, tab, tabIndex) {
if (tab.disabled) {
return;
}
this.currentSelectedTab = tabIndex;
this.tabs.forEach(_tab => _tab.active = false);
tab.active = true;
tab.doSelect();
}
/**
* Determines which `Tab` is initially selected.
*/
setFirstTab() {
setTimeout(() => {
let firstTab = this.tabs.find(tab => tab.active);
if (!firstTab && this.tabs.first) {
firstTab = this.tabs.first;
firstTab.active = true;
}
if (firstTab) {
firstTab.doSelect();
}
});
}
}
TabHeaders.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeaders, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }, { token: i1.EventService }, { token: i0.Renderer2 }, { token: i2.I18n }], target: i0.ɵɵFactoryTarget.Component });
TabHeaders.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TabHeaders, selector: "cds-tab-headers, ibm-tab-headers", inputs: { tabInput: ["tabs", "tabInput"], translations: "translations" }, host: { listeners: { "keydown": "keyboardInput($event)" } }, queries: [{ propertyName: "tabQuery", predicate: Tab }], viewQueries: [{ propertyName: "headerContainer", first: true, predicate: ["tabList"], descendants: true, static: true }, { propertyName: "allTabHeaders", predicate: ["tabItem"], descendants: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, 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
}"
[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"
(scroll)="handleScroll()">
<ng-container [ngTemplateOutlet]="contentBefore"></ng-container>
<button
*ngFor="let tab of tabs; let i = index;"
#tabItem
role="tab"
[attr.aria-selected]="tab.active"
[attr.tabindex]="(tab.active?0:-1)"
[attr.aria-controls]="tab.id"
[attr.aria-disabled]="tab.disabled"
[ngClass]="{
'cds--tabs__nav-item--selected': tab.active,
'cds--tabs__nav-item--disabled': tab.disabled
}"
class="cds--tabs__nav-item cds--tabs__nav-link"
type="button"
draggable="false"
id="{{tab.id}}-header"
(focus)="onTabFocus(tabItem, i)"
(click)="selectTab(tabItem, tab, i)">
<ng-container *ngIf="!tab.headingIsTemplate">
{{ tab.heading }}
</ng-container>
<ng-template
*ngIf="tab.headingIsTemplate"
[ngTemplateOutlet]="tab.heading"
[ngTemplateOutletContext]="{$implicit: tab.context}">
</ng-template>
</button>
<ng-container [ngTemplateOutlet]="contentAfter"></ng-container>
</div>
<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
}"
[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>
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabHeaders, decorators: [{
type: Component,
args: [{
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
}"
[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"
(scroll)="handleScroll()">
<ng-container [ngTemplateOutlet]="contentBefore"></ng-container>
<button
*ngFor="let tab of tabs; let i = index;"
#tabItem
role="tab"
[attr.aria-selected]="tab.active"
[attr.tabindex]="(tab.active?0:-1)"
[attr.aria-controls]="tab.id"
[attr.aria-disabled]="tab.disabled"
[ngClass]="{
'cds--tabs__nav-item--selected': tab.active,
'cds--tabs__nav-item--disabled': tab.disabled
}"
class="cds--tabs__nav-item cds--tabs__nav-link"
type="button"
draggable="false"
id="{{tab.id}}-header"
(focus)="onTabFocus(tabItem, i)"
(click)="selectTab(tabItem, tab, i)">
<ng-container *ngIf="!tab.headingIsTemplate">
{{ tab.heading }}
</ng-container>
<ng-template
*ngIf="tab.headingIsTemplate"
[ngTemplateOutlet]="tab.heading"
[ngTemplateOutletContext]="{$implicit: tab.context}">
</ng-template>
</button>
<ng-container [ngTemplateOutlet]="contentAfter"></ng-container>
</div>
<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
}"
[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>
`
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }, { type: i1.EventService }, { type: i0.Renderer2 }, { type: i2.I18n }]; }, propDecorators: { tabInput: [{
type: Input,
args: ["tabs"]
}], translations: [{
type: Input
}], headerContainer: [{
type: ViewChild,
args: ["tabList", { static: true }]
}], tabQuery: [{
type: ContentChildren,
args: [Tab]
}], allTabHeaders: [{
type: ViewChildren,
args: ["tabItem"]
}], keyboardInput: [{
type: HostListener,
args: ["keydown", ["$event"]]
}] } });
/**
* Skeleton component for tabs
*/
class TabSkeleton {
constructor() {
/**
* Set to `true` to put tabs in a loading state.
*/
this.skeleton = true;
this.tabs = true;
this.numOfSkeletonTabs = new Array(5);
}
/**
* Set number of skeleton tabs to render, default is 5
*/
set numOftabs(num) {
this.numOfSkeletonTabs = new Array(num);
}
}
TabSkeleton.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
TabSkeleton.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TabSkeleton, selector: "cds-tabs-skeleton, ibm-tabs-skeleton", inputs: { numOftabs: "numOftabs" }, host: { properties: { "class.cds--skeleton": "this.skeleton", "class.cds--tabs": "this.tabs" } }, ngImport: i0, template: `
<ul class="cds--tabs__nav">
<li
*ngFor="let i of numOfSkeletonTabs"
class="cds--tabs__nav-item">
<div class="cds--tabs__nav-link">
<span></span>
</div>
</li>
</ul>
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TabSkeleton, decorators: [{
type: Component,
args: [{
selector: "cds-tabs-skeleton, ibm-tabs-skeleton",
template: `
<ul class="cds--tabs__nav">
<li
*ngFor="let i of numOfSkeletonTabs"
class="cds--tabs__nav-item">
<div class="cds--tabs__nav-link">
<span></span>
</div>
</li>
</ul>
`
}]
}], propDecorators: { numOftabs: [{
type: Input
}], skeleton: [{
type: HostBinding,
args: ["class.cds--skeleton"]
}], tabs: [{
type: HostBinding,
args: ["class.cds--tabs"]
}] } });
/**
* Build out your application's tabs using this component.
* This is the parent of the `Tab` and `TabHeader` components.
*
* [See demo](../../?path=/story/components-tabs--basic)
*
* `Tabs` expects a set of `n-tab` elements
*
* ```html
* <cds-tabs>
* <cds-tab heading='tab1'>
* tab 1 content
* </cds-tab>
* <cds-tab heading='tab1'>
* tab 2 content
* </cds-tab>
* <!-- ... -->
* <cds-tab heading='tab1'>
* tab n content
* </cds-tab>
* </cds-tabs>
* ```
*/
class Tabs {
constructor() {
/**
* Takes either the string value 'top' or 'bottom' to place TabHeader
* relative to the `TabPanel`s.
*/
this.position = "top";
/**
* Set to 'true' to have `Tab` items cached and not reloaded on tab switching.
*/
this.cacheActive = false;
/**
* Set to 'true' to have tabs automatically activated and have their content displayed when they receive focus.
*/
this.followFocus = true;
/**
* Set to `true` to have the tabIndex of the all tabpanels be -1.
*/
this.isNavigation = false;
/**
* Sets the type of the `TabHeader`s
*/
this.type = "line";
/**
* Sets the theme of `TabHeader`s
*/
this.theme = "dark";
/**
* Set state of tabs to skeleton
*/
this.skeleton = false;
}
/**
* After content is initialized update `Tab`s to cache (if turned on) and set the initial
* selected Tab item.
*/
ngAfterContentInit() {
if (this.tabHeaders) {
this.tabHeaders.cacheActive = this.cacheActive;
}
this.tabs.forEach(tab => {
tab.tabIndex = this.isNavigation ? null : 0;
});
}
ngOnChanges(changes) {
if (this.tabHeaders && changes.cacheActive) {
this.tabHeaders.cacheActive = this.cacheActive;
}
if (this.tabs && changes.isNavigation) {
this.tabs.forEach(tab => {
tab.tabIndex = this.isNavigation ? null : 0;
});
}
}
/**
* true if the n-tab's are passed directly to the component as children
*/
hasTabHeaders() {
return this.tabs.length > 0;
}
}
Tabs.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: Tabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
Tabs.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: Tabs, selector: "cds-tabs, ibm-tabs", inputs: { position: "position", cacheActive: "cacheActive", followFocus: "followFocus", isNavigation: "isNavigation", ariaLabel: "ariaLabel", ariaLabelledby: "ariaLabelledby", type: "type", theme: "theme", skeleton: "skeleton" }, queries: [{ propertyName: "tabHeaders", first: true, predicate: TabHeaders, descendants: true }, { propertyName: "tabs", predicate: Tab }], usesOnChanges: true, ngImport: i0, template: `
<ng-container *ngIf="skeleton">
<cds-tabs-skeleton></cds-tabs-skeleton>
</ng-container>
<ng-container *ngIf="!skeleton">
<cds-tab-headers
*ngIf="hasTabHeaders() && position === 'top'"
[theme]="theme"
[tabs]="tabs"
[followFocus]="followFocus"
[cacheActive]="cacheActive"
[contentBefore]="before"
[contentAfter]="after"
[ariaLabel]="ariaLabel"
[ariaLabelledby]="ariaLabelledby"
[type]="type">
</cds-tab-headers>
<ng-content></ng-content>
<ng-template #before>
<ng-content select="[before]"></ng-content>
</ng-template>
<ng-template #after>
<ng-content select="[after]"></ng-content>
</ng-template>
<cds-tab-headers
*ngIf="hasTabHeaders() && position === 'bottom'"
[tabs]="tabs"
[cacheActive]="cacheActive"
[type]="type">
</cds-tab-headers>
</ng-container>
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: TabHeaders, selector: "cds-tab-headers, ibm-tab-headers", inputs: ["tabs", "translations"] }, { kind: "component", type: TabSkeleton, selector: "cds-tabs-skeleton, ibm-tabs-skeleton", inputs: ["numOftabs"] }] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: Tabs, decorators: [{
type: Component,
args: [{
selector: "cds-tabs, ibm-tabs",
template: `
<ng-container *ngIf="skeleton">
<cds-tabs-skeleton></cds-tabs-skeleton>
</ng-container>
<ng-container *ngIf="!skeleton">
<cds-tab-headers
*ngIf="hasTabHeaders() && position === 'top'"
[theme]="theme"
[tabs]="tabs"
[followFocus]="followFocus"
[cacheActive]="cacheActive"
[contentBefore]="before"
[contentAfter]="after"
[ariaLabel]="ariaLabel"
[ariaLabelledby]="ariaLabelledby"
[type]="type">
</cds-tab-headers>
<ng-content></ng-content>
<ng-template #before>
<ng-content select="[before]"></ng-content>
</ng-template>
<ng-template #after>
<ng-content select="[after]"></ng-content>
</ng-template>
<cds-tab-headers
*ngIf="hasTabHeaders() && position === 'bottom'"
[tabs]="tabs"
[cacheActive]="cacheActive"
[type]="type">
</cds-tab-headers>
</ng-container>
`
}]
}], propDecorators: { position: [{
type: Input
}], cacheActive: [{
type: Input
}], followFocus: [{
type: Input
}], isNavigation: [{
type: Input
}], ariaLabel: [{
type: Input
}], ariaLabelledby: [{
type: Input
}], type: [{
type: Input
}], theme: [{
type: Input
}], skeleton: [{
type: Input
}], tabs: [{