File

src/tabs/tab-header-group-vertical.component.ts

Description

Vertical tab header group: same children as cds-tab-header-group, with up/down (and Home/End) keys, gradient overflow, and always-contained type.

Example :
<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>

Extends

BaseTabHeader

Implements

AfterContentInit OnChanges OnInit OnDestroy

Metadata

Index

Properties
Methods
Inputs
Outputs
HostBindings
HostListeners
Accessors

Constructor

constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, eventService: EventService, renderer: Renderer2, i18n: I18n)
Parameters :
Name Type Optional
elementRef ElementRef No
changeDetectorRef ChangeDetectorRef No
eventService EventService No
renderer Renderer2 No
i18n I18n No

Inputs

followFocus
Type : boolean
Default value : true
Inherited from BaseTabHeader
Defined in BaseTabHeader:83

Set to 'true' to have tabs automatically activated and have their content displayed when they receive focus.

isNavigation
Type : boolean
Default value : false

When true, sets each tab panel tabindex to -1 for navigation-style usage.

translations
Type : any
Default value : this.i18n.get().TABS

i18n strings for the tab list aria-label fallback.

ariaLabel
Type : string
Inherited from BaseTabHeader
Defined in BaseTabHeader:33

Sets the aria label on the nav element.

ariaLabelledby
Type : string
Inherited from BaseTabHeader
Defined in BaseTabHeader:37

Sets the aria labelledby on the nav element.

cacheActive
Type : boolean
Default value : false
Inherited from BaseTabHeader
Defined in BaseTabHeader:25

Set to true to have Tab items cached and not reloaded on tab switching. Duplicated from cds-tabs to support standalone headers.

contentAfter
Type : TemplateRef<any>
Inherited from BaseTabHeader
Defined in BaseTabHeader:46

Template projected after tab items inside the tab list.

contentBefore
Type : TemplateRef<any>
Inherited from BaseTabHeader
Defined in BaseTabHeader:42

Template projected before tab items inside the tab list.

dismissable
Type : boolean
Default value : false
Inherited from BaseTabHeader
Defined in BaseTabHeader:70

Show a close control on each tab.

fullWidth
Type : boolean
Default value : false
Inherited from BaseTabHeader
Defined in BaseTabHeader:65

Contained only: Evenly sized tabs across the row (must have fewer than 9 tabs).

iconSize
Type : "default" | "lg"
Inherited from BaseTabHeader
Defined in BaseTabHeader:60

When using icon-only tabs, icon size: default (16px) or lg (20px).

scrollDebounceWait
Type : number
Default value : 200
Inherited from BaseTabHeader
Defined in BaseTabHeader:80

Debounce (ms) for tab list scroll events; affects overflow chevron updates.

scrollIntoView
Type : boolean
Default value : false
Inherited from BaseTabHeader
Defined in BaseTabHeader:75

Scroll the active tab into view on focus/select.

theme
Type : "dark" | "light"
Default value : "dark"
Inherited from BaseTabHeader
Defined in BaseTabHeader:55

Theme for contained tabs: dark or light.

type
Type : "line" | "contained"
Default value : "line"
Inherited from BaseTabHeader
Defined in BaseTabHeader:51

Visual style of the tab list: line or contained.

Outputs

tabClose
Type : EventEmitter

Fires with tab index when a close control is used (with dismissable).

HostBindings

class.cds--tabs--tall
Type : boolean

We use taller rows when any header has a secondary label.

class.cds--tabs--vertical
Type : boolean
Default value : true
class.cds--layout--size-lg
Type : boolean
Inherited from BaseTabHeader
Defined in BaseTabHeader:98
class.cds--tabs
Type : boolean
Default value : true
Inherited from BaseTabHeader
Defined in BaseTabHeader:82
class.cds--tabs__icon--default
Type : boolean
Inherited from BaseTabHeader
Defined in BaseTabHeader:92
class.cds--tabs__icon--lg
Type : boolean
Inherited from BaseTabHeader
Defined in BaseTabHeader:95
class.cds--tabs--contained
Type : boolean
Inherited from BaseTabHeader
Defined in BaseTabHeader:83
class.cds--tabs--dismissable
Type : boolean
Inherited from BaseTabHeader
Defined in BaseTabHeader:89
class.cds--tabs--light
Type : boolean
Inherited from BaseTabHeader
Defined in BaseTabHeader:86

HostListeners

blur
Arguments : '$event'
blur(event: FocusEvent)
keydown
Arguments : '$event'
keydown(event: KeyboardEvent)

Methods

Private applyHeaderInputs
applyHeaderInputs()
Returns : void
handleBlur
handleBlur(event: FocusEvent)
Decorators :
@HostListener('blur', ['$event'])
Parameters :
Name Type Optional
event FocusEvent No
Returns : void
keyboardInput
keyboardInput(event: KeyboardEvent)
Decorators :
@HostListener('keydown', ['$event'])
Parameters :
Name Type Optional
event KeyboardEvent No
Returns : void
ngAfterContentInit
ngAfterContentInit()
Returns : void
ngOnChanges
ngOnChanges(changes: SimpleChanges)
Parameters :
Name Type Optional
changes SimpleChanges No
Returns : void
ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()
Returns : void
Protected scrollSelectedTabIntoView
scrollSelectedTabIntoView()
Returns : void
Protected updateOverflowState
updateOverflowState()
Returns : void
Private wireSubscriptions
wireSubscriptions()
Returns : void
handleOverflowNavClick
handleOverflowNavClick(direction: number, numOftabs: number)
Inherited from BaseTabHeader
Defined in BaseTabHeader:156
Parameters :
Name Type Optional Default value
direction number No
numOftabs number No 0
Returns : void
handleOverflowNavMouseDown
handleOverflowNavMouseDown(direction: number)
Inherited from BaseTabHeader
Defined in BaseTabHeader:168
Parameters :
Name Type Optional
direction number No
Returns : void
handleOverflowNavMouseUp
handleOverflowNavMouseUp()
Inherited from BaseTabHeader
Defined in BaseTabHeader:193

Clear intervals/Timeout & reset scroll behavior

Returns : void
handleScroll
handleScroll()
Inherited from BaseTabHeader
Defined in BaseTabHeader:143
Returns : void

Properties

activeIndex
Type : number | null
Default value : null

Focused tab index when followFocus is false (manual activation).

Private boundListScrollHandler
Type : function
Private closeSubscriptionTracker
Default value : new Subscription()
currentSelectedTab
Type : number
Default value : 0
Inherited from BaseTabHeader
Defined in BaseTabHeader:98

Index of the selected tab for keyboard logic

headerContainer
Type : ElementRef<HTMLElement>
Decorators :
@ViewChild('tabList', {static: true})
Inherited from BaseTabHeader
Defined in BaseTabHeader:91
isOverflowingBottom
Default value : false
isOverflowingTop
Default value : false
Private resizeObserver
Type : ResizeObserver | null
Default value : null
Private selectedSubscriptionTracker
Default value : new Subscription()
tabHeaderQuery
Type : QueryList<TabHeaderBase>
Decorators :
@ContentChildren(TabHeaderBase)

ContentChildren of all the tab headers (both directive and component forms — see TabHeaderBase).

verticalClass
Default value : true
Decorators :
@HostBinding('class.cds--tabs--vertical')
Readonly clickMultiplier
Type : number
Default value : 1.5
Inherited from BaseTabHeader
Defined in BaseTabHeader:114
Protected longPressInterval
Type : null
Default value : null
Inherited from BaseTabHeader
Defined in BaseTabHeader:116
Readonly longPressMultiplier
Type : number
Default value : 3
Inherited from BaseTabHeader
Defined in BaseTabHeader:113
Readonly OVERFLOW_BUTTON_OFFSET
Type : number
Default value : 44
Inherited from BaseTabHeader
Defined in BaseTabHeader:112
Protected scrollDebounceTimer
Type : any
Default value : null
Inherited from BaseTabHeader
Defined in BaseTabHeader:118
tabsClass
Default value : true
Decorators :
@HostBinding('class.cds--tabs')
Inherited from BaseTabHeader
Defined in BaseTabHeader:82
Protected tickInterval
Type : null
Default value : null
Inherited from BaseTabHeader
Defined in BaseTabHeader:117

Accessors

tallClass
gettallClass()

We use taller rows when any header has a secondary label.

Returns : boolean
hasSecondaryLabelTabs
gethasSecondaryLabelTabs()
import {
	AfterContentInit,
	ChangeDetectorRef,
	Component,
	ContentChildren,
	ElementRef,
	EventEmitter,
	HostBinding,
	HostListener,
	Input,
	OnChanges,
	OnDestroy,
	OnInit,
	Output,
	QueryList,
	Renderer2,
	SimpleChanges,
	ViewChild
} from "@angular/core";
import { Subscription } from "rxjs";
import { EventService } from "carbon-components-angular/utils";
import { I18n } from "carbon-components-angular/i18n";

import { BaseTabHeader } from "./base-tab-header.component";
import { TabHeaderBase } from "./tab-header.directive";

const VERTICAL_TAB_HEIGHT = 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>
 * ```
 */
@Component({
	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>
	`
})
export class TabHeaderGroupVertical
	extends BaseTabHeader
	implements AfterContentInit, OnChanges, OnInit, OnDestroy {
	/**
	 * i18n strings for the tab list `aria-label` fallback.
	 */
	@Input() translations = this.i18n.get().TABS;

	/**
	 * When `true`, sets each tab panel `tabindex` to `-1` for navigation-style usage.
	 */
	@Input() isNavigation = false;

	/**
	 * Fires with tab index when a close control is used (with `dismissable`).
	 */
	@Output() tabClose = new EventEmitter<number>();

	/**
	 * Set to 'true' to have tabs automatically activated and have their content displayed when they receive focus.
	 */
	@Input() followFocus = true;

	/**
	 * ContentChildren of all the tab headers (both directive and component
	 * forms — see `TabHeaderBase`).
	 */
	@ContentChildren(TabHeaderBase) tabHeaderQuery: QueryList<TabHeaderBase>;

	@ViewChild("tabList", { static: true }) headerContainer: ElementRef<HTMLElement>;

	@HostBinding("class.cds--tabs--vertical") verticalClass = true;

	/**
	 * Index of the selected tab for keyboard logic
	 */
	currentSelectedTab = 0;

	/**
	 * Focused tab index when `followFocus` is false (manual activation).
	 */
	activeIndex: number | null = null;

	isOverflowingTop = false;
	isOverflowingBottom = false;

	/**
	 * We use taller rows when any header has a secondary label.
	 */
	@HostBinding("class.cds--tabs--tall") get tallClass(): boolean {
		return this.hasSecondaryLabelTabs;
	}

	get hasSecondaryLabelTabs(): boolean {
		if (!this.tabHeaderQuery) {
			return false;
		}
		return this.tabHeaderQuery
			.toArray()
			.some((h) => h.secondaryLabel != null && h.secondaryLabel !== "");
	}

	private selectedSubscriptionTracker = new Subscription();
	private closeSubscriptionTracker = new Subscription();

	private resizeObserver: ResizeObserver | null = null;
	private boundListScrollHandler: () => void;

	constructor(
		protected elementRef: ElementRef,
		protected changeDetectorRef: ChangeDetectorRef,
		protected eventService: EventService,
		protected renderer: Renderer2,
		protected i18n: I18n
	) {
		super(elementRef, changeDetectorRef, eventService, renderer);
		this.type = "contained";
		// Cache a stable reference for add/removeEventListener.
		this.boundListScrollHandler = () => this.updateOverflowState();
	}

	@HostListener("keydown", ["$event"])
	keyboardInput(event: KeyboardEvent) {
		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;
		}
	}

	@HostListener("blur", ["$event"])
	handleBlur(event: FocusEvent) {
		const relatedTarget = event.relatedTarget as Node | null;
		const container = this.headerContainer?.nativeElement;
		if (container && relatedTarget && container.contains(relatedTarget)) {
			return;
		}
		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: SimpleChanges) {
		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);
			}
		}
	}

	protected updateOverflowState() {
		const element = this.headerContainer?.nativeElement;
		if (!element) {
			return;
		}
		const halfTabHeight = VERTICAL_TAB_HEIGHT / 2;
		this.isOverflowingBottom =
			element.scrollTop + element.clientHeight + halfTabHeight <= element.scrollHeight;
		this.isOverflowingTop = element.scrollTop > halfTabHeight;
		this.changeDetectorRef.markForCheck();
	}

	protected 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),
			behavior: "smooth"
		});
	}

	private applyHeaderInputs() {
		this.tabHeaderQuery.toArray().forEach((header) => {
			header.cacheActive = this.cacheActive;
			header.dismissable = this.dismissable;
			header.paneTabIndex = this.isNavigation ? null : 0;
		});
	}

	private 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);
				})
			);
		});
	}
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""