File

src/tabs/tab-headers-vertical.component.ts

Description

The TabHeadersVertical component renders tab headers in a vertical orientation. It contains the Tab items and supports keyboard navigation via ArrowUp/ArrowDown/Home/End.

Extends

BaseTabHeader

Implements

AfterContentInit OnChanges OnDestroy OnInit

Metadata

Index

Properties
Methods
Inputs
HostBindings
HostListeners

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

tabs
Type : QueryList<Tab>

List of Tab components.

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.

followFocus
Type : boolean
Inherited from BaseTabHeader
Defined in BaseTabHeader:29

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

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.

HostBindings

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

getSelectedTab
getSelectedTab()
Returns : any
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
onTabFocus
onTabFocus(index: number)
Parameters :
Name Type Optional
index number No
Returns : void
Protected scrollSelectedTabIntoView
scrollSelectedTabIntoView()
Returns : void
selectTab
selectTab(tab: Tab, tabIndex: number)
Parameters :
Name Type Optional
tab Tab No
tabIndex number No
Returns : void
Protected setFirstTab
setFirstTab()
Returns : void
Protected updateOverflowState
updateOverflowState()
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)

allTabHeaders
Type : QueryList<ElementRef>
Decorators :
@ViewChildren('tabItem')
headerContainer
Type : ElementRef<HTMLElement>
Decorators :
@ViewChild('tabList', {static: true})
Inherited from BaseTabHeader
Defined in BaseTabHeader:96
isOverflowingBottom
Default value : false

Whether the tab list is overflowing at the bottom (some tabs are clipped).

isOverflowingTop
Default value : false

Whether the tab list is overflowing at the top (some tabs are clipped).

Private listScrollHandler
Default value : () => {...}
Private resizeObserver
Type : ResizeObserver
tabQuery
Type : QueryList<Tab>
Decorators :
@ContentChildren(Tab)

ContentChild of all the tabs

tabs
Type : QueryList<Tab>
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
currentSelectedTab
Type : number
Inherited from BaseTabHeader
Defined in BaseTabHeader:110

Controls the manual focusing done by tabbing through headings.

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
import {
	Component,
	QueryList,
	Input,
	HostListener,
	HostBinding,
	ViewChild,
	ContentChildren,
	AfterContentInit,
	ViewChildren,
	ElementRef,
	OnChanges,
	SimpleChanges,
	OnDestroy,
	OnInit,
	ChangeDetectorRef,
	Renderer2
} from "@angular/core";
import { EventService } from "carbon-components-angular/utils";
import { I18n } from "carbon-components-angular/i18n";

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

const VERTICAL_TAB_HEIGHT = 64;

/**
 * The `TabHeadersVertical` component renders tab headers in a vertical
 * orientation. It contains the `Tab` items and supports keyboard navigation
 * via ArrowUp/ArrowDown/Home/End.
 */
@Component({
	selector: "cds-tab-headers-vertical, ibm-tab-headers-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>
			<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"
				[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"
				[attr.title]="tab.title || (!tab.headingIsTemplate ? tab.heading : null)"
				(focus)="onTabFocus(i)"
				(click)="selectTab(tab, i)">
				<div class="cds--tabs__nav-item-label-wrapper">
					<span class="cds--tabs__nav-item-label">
						<ng-container *ngIf="!tab.headingIsTemplate">
							{{ tab.heading }}
						</ng-container>
						<ng-template
							*ngIf="tab.headingIsTemplate"
							[ngTemplateOutlet]="tab.heading"
							[ngTemplateOutletContext]="{$implicit: tab.context}">
						</ng-template>
					</span>
				</div>
			</button>
			<ng-container [ngTemplateOutlet]="contentAfter"></ng-container>
		</div>
		<div *ngIf="isOverflowingBottom" class="cds--tab--list-gradient_bottom"></div>
	`
})
export class TabHeadersVertical extends BaseTabHeader implements AfterContentInit, OnChanges, OnDestroy, OnInit {
	/**
	 * List of `Tab` components.
	 */
	// disable the next line because we need to rename the input
	// tslint:disable-next-line
	@Input("tabs") tabInput: QueryList<Tab>;

	/**
	 * i18n strings for the tab list `aria-label` fallback.
	 */
	@Input() translations = this.i18n.get().TABS;

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

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

	/**
	 * ContentChild of all the tabs
	 */
	@ContentChildren(Tab) tabQuery: QueryList<Tab>;
	tabs: QueryList<Tab>;

	@ViewChildren("tabItem") allTabHeaders: QueryList<ElementRef>;

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

	/**
	 * Whether the tab list is overflowing at the top (some tabs are clipped).
	 */
	isOverflowingTop = false;
	/**
	 * Whether the tab list is overflowing at the bottom (some tabs are clipped).
	 */
	isOverflowingBottom = false;

	private resizeObserver: ResizeObserver;

	constructor(
		protected elementRef: ElementRef,
		protected changeDetectorRef: ChangeDetectorRef,
		protected eventService: EventService,
		protected renderer: Renderer2,
		protected i18n: I18n
	) {
		super(elementRef, changeDetectorRef, eventService, renderer);
		this.type = "contained";
	}

	@HostListener("keydown", ["$event"])
	keyboardInput(event: KeyboardEvent) {
		if (!this.tabs) {
			return;
		}
		const tabsArray = this.tabs.toArray();
		const enabledTabs = tabsArray.filter(tab => !tab.disabled);
		if (enabledTabs.length === 0) {
			return;
		}

		const referenceIndex = this.followFocus ?
			this.currentSelectedTab :
			(this.activeIndex !== null ? this.activeIndex : this.currentSelectedTab);
		const currentEnabledIndex = Math.max(0, enabledTabs.indexOf(tabsArray[referenceIndex]));

		let nextEnabledIndex = currentEnabledIndex;
		let handled = false;

		if (event.key === "ArrowDown") {
			nextEnabledIndex = (currentEnabledIndex + 1) % enabledTabs.length;
			handled = true;
		} else if (event.key === "ArrowUp") {
			nextEnabledIndex = (enabledTabs.length + currentEnabledIndex - 1) % enabledTabs.length;
			handled = true;
		} else if (event.key === "Home") {
			nextEnabledIndex = 0;
			handled = true;
		} else if (event.key === "End") {
			nextEnabledIndex = enabledTabs.length - 1;
			handled = true;
		}

		if (handled) {
			event.preventDefault();
			const nextTab = enabledTabs[nextEnabledIndex];
			const nextIndex = tabsArray.indexOf(nextTab);

			if (this.followFocus) {
				this.selectTab(nextTab, nextIndex);
			} else {
				this.activeIndex = nextIndex;
			}
			this.allTabHeaders.toArray()[nextIndex].nativeElement.focus();
			return;
		}

		if ((event.key === " " || event.key === "Spacebar") && !this.followFocus) {
			const focusIndex = this.activeIndex !== null ? this.activeIndex : this.currentSelectedTab;
			this.selectTab(tabsArray[focusIndex], 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(): void {
		this.resizeObserver = new ResizeObserver(() => {
			this.updateOverflowState();
			this.changeDetectorRef.detectChanges();
		});
		this.resizeObserver.observe(this.headerContainer.nativeElement);
		this.headerContainer.nativeElement.addEventListener("scroll", this.listScrollHandler);
	}

	ngOnDestroy(): void {
		this.resizeObserver?.unobserve(this.headerContainer.nativeElement);
		this.headerContainer.nativeElement.removeEventListener("scroll", this.listScrollHandler);
	}

	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.changeDetectorRef.markForCheck();
		});
		this.setFirstTab();
	}

	ngOnChanges(changes: SimpleChanges) {
		if (this.tabs && changes.cacheActive) {
			this.tabs.forEach(tab => tab.cacheActive = this.cacheActive);
		}
	}

	onTabFocus(index: number) {
		if (this.followFocus) {
			this.currentSelectedTab = index;
		} else {
			this.activeIndex = index;
		}
		this.scrollSelectedTabIntoView();
	}

	selectTab(tab: Tab, tabIndex: number) {
		if (tab.disabled) {
			return;
		}
		this.currentSelectedTab = tabIndex;
		this.activeIndex = tabIndex;
		this.tabs.forEach(_tab => _tab.active = false);
		tab.active = true;
		tab.doSelect();
		this.scrollSelectedTabIntoView();
	}

	getSelectedTab(): any {
		const selected = this.tabs.find(tab => tab.active);
		if (selected) {
			return selected;
		}
		return { headingIsTemplate: false, heading: "" };
	}

	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() {
		const container = this.headerContainer?.nativeElement;
		if (!container) {
			return;
		}
		const selectedHeader = this.allTabHeaders?.toArray()[this.currentSelectedTab]?.nativeElement;
		if (!selectedHeader) {
			return;
		}
		const containerRect = container.getBoundingClientRect();
		const selectedRect = selectedHeader.getBoundingClientRect();
		const halfTabHeight = VERTICAL_TAB_HEIGHT / 2;

		if (
			selectedRect.top - halfTabHeight < containerRect.top ||
			selectedRect.top - containerRect.top + VERTICAL_TAB_HEIGHT + halfTabHeight > containerRect.height
		) {
			container.scrollTo({
				top: Math.max(0, (this.currentSelectedTab - 1) * VERTICAL_TAB_HEIGHT),
				behavior: "smooth"
			});
		}
	}

	protected setFirstTab() {
		setTimeout(() => {
			let firstTab = this.tabs.find(tab => tab.active);
			if (!firstTab && this.tabs.first) {
				firstTab = this.tabs.first;
				firstTab.active = true;
			}
			if (firstTab) {
				this.currentSelectedTab = this.tabs.toArray().indexOf(firstTab);
				this.activeIndex = this.currentSelectedTab;
				firstTab.doSelect();
				this.updateOverflowState();
			}
		});
	}
	private listScrollHandler = () => this.updateOverflowState();
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""