UNPKG

carbon-components-angular

Version:
1,289 lines (1,281 loc) 52.2 kB
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: [{