UNPKG

carbon-components-angular

Version:
1,183 lines (1,178 loc) 130 kB
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">