1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 09:43:23 +00:00

[EC 456] Component Library Content Switching Tabs (#3452)

* [EC-456] Rename bitTabItem -> bitTab

* [EC-456] Use templateRefs or text for tab label content

* [EC-456] Add bit-tab-nav-bar component

* [EC-456] Finish content tab switching and nav tabs

* [EC-456] Undo accidental eslintrc.json change

* [EC-456] Fix directive/component selector naming convention

* [EC-456] Cleanup unnecessary InjectionTokens and simplify template label property

* [EC-456] Cleanup one more unnecessary InjectionToken

* [EC-456] Cleanup tab styles to better match Figma. Add internal tab header component for styling header background according to Figma.

* [EC-456] Add sub-folders for nav, content, and shared tab components/directives

* [EC-456] Code/style cleanup

* [EC-456] Remove underscore from protected members

* [EC-456] Cleanup tab stories and forgotten any type.

* [EC-456] Fix dark theme story tab content text color

* [EC-456] Add missing padding to tab header

* [EC-456] Add tab content padding to align with tab headers

* [EC-456] Move bottom tab border to header to span entire content area

* [EC-456] Force text-main tab label color

* [EC-456] Undo text-main change
This commit is contained in:
Shane Melton
2022-09-26 14:41:51 -07:00
committed by GitHub
parent e8936eb4c6
commit debaef2941
21 changed files with 653 additions and 112 deletions

View File

@@ -0,0 +1,187 @@
import { FocusKeyManager } from "@angular/cdk/a11y";
import { coerceNumberProperty } from "@angular/cdk/coercion";
import {
AfterContentChecked,
AfterContentInit,
AfterViewInit,
Component,
ContentChildren,
EventEmitter,
Input,
OnDestroy,
Output,
QueryList,
ViewChildren,
} from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { TabListItemDirective } from "../shared/tab-list-item.directive";
import { TabComponent } from "./tab.component";
/** Used to generate unique ID's for each tab component */
let nextId = 0;
@Component({
selector: "bit-tab-group",
templateUrl: "./tab-group.component.html",
})
export class TabGroupComponent
implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy
{
private readonly _groupId: number;
private readonly destroy$ = new Subject<void>();
private _indexToSelect: number | null = 0;
/**
* Aria label for the tab list menu
*/
@Input() label = "";
/**
* Keep the content of off-screen tabs in the DOM.
* Useful for keeping <audio> or <video> elements from re-initializing
* after navigating between tabs.
*/
@Input() preserveContent = false;
@ContentChildren(TabComponent) tabs: QueryList<TabComponent>;
@ViewChildren(TabListItemDirective) tabLabels: QueryList<TabListItemDirective>;
/** The index of the active tab. */
@Input()
get selectedIndex(): number | null {
return this._selectedIndex;
}
set selectedIndex(value: number) {
this._indexToSelect = coerceNumberProperty(value, null);
}
private _selectedIndex: number | null = null;
/** Output to enable support for two-way binding on `[(selectedIndex)]` */
@Output() readonly selectedIndexChange: EventEmitter<number> = new EventEmitter<number>();
/** Event emitted when the tab selection has changed. */
@Output() readonly selectedTabChange: EventEmitter<BitTabChangeEvent> =
new EventEmitter<BitTabChangeEvent>();
/**
* Focus key manager for keeping tab controls accessible.
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions
*/
keyManager: FocusKeyManager<TabListItemDirective>;
constructor() {
this._groupId = nextId++;
}
protected getTabContentId(id: number): string {
return `bit-tab-content-${this._groupId}-${id}`;
}
protected getTabLabelId(id: number): string {
return `bit-tab-label-${this._groupId}-${id}`;
}
selectTab(index: number) {
this.selectedIndex = index;
}
/**
* After content is checked, the tab group knows what tabs are defined and which index
* should be currently selected.
*/
ngAfterContentChecked(): void {
const indexToSelect = (this._indexToSelect = this._clampTabIndex(this._indexToSelect));
if (this._selectedIndex != indexToSelect) {
const isFirstRun = this._selectedIndex == null;
if (!isFirstRun) {
this.selectedTabChange.emit({
index: indexToSelect,
tab: this.tabs.toArray()[indexToSelect],
});
}
// These values need to be updated after change detection as
// the checked content may have references to them.
Promise.resolve().then(() => {
this.tabs.forEach((tab, index) => (tab.isActive = index === indexToSelect));
if (!isFirstRun) {
this.selectedIndexChange.emit(indexToSelect);
}
});
// Manually update the _selectedIndex and keyManager active item
this._selectedIndex = indexToSelect;
if (this.keyManager) {
this.keyManager.setActiveItem(indexToSelect);
}
}
}
ngAfterViewInit(): void {
this.keyManager = new FocusKeyManager(this.tabLabels)
.withHorizontalOrientation("ltr")
.withWrap()
.withHomeAndEnd();
}
ngAfterContentInit() {
// Subscribe to any changes in the number of tabs, in order to be able
// to re-render content when new tabs are added or removed.
this.tabs.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
const indexToSelect = this._clampTabIndex(this._indexToSelect);
// If the selected tab didn't explicitly change, keep the previously
// selected tab selected/active
if (indexToSelect === this._selectedIndex) {
const tabs = this.tabs.toArray();
let selectedTab: TabComponent | undefined;
for (let i = 0; i < tabs.length; i++) {
if (tabs[i].isActive) {
// Set both _indexToSelect and _selectedIndex to avoid firing a change
// event which could cause an infinite loop if adding a tab within the
// selectedIndexChange event
this._indexToSelect = this._selectedIndex = i;
selectedTab = tabs[i];
break;
}
}
// No active tab found and a tab does exist means the active tab
// was removed, so a new active tab must be set manually
if (!selectedTab && tabs[indexToSelect]) {
tabs[indexToSelect].isActive = true;
this.selectedTabChange.emit({
index: indexToSelect,
tab: tabs[indexToSelect],
});
}
}
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
private _clampTabIndex(index: number): number {
return Math.min(this.tabs.length - 1, Math.max(index || 0, 0));
}
}
export class BitTabChangeEvent {
/**
* The currently selected tab index
*/
index: number;
/**
* The currently selected tab
*/
tab: TabComponent;
}