diff --git a/libs/components/src/tabs/index.ts b/libs/components/src/tabs/index.ts index 9b45ff1d43b..e89b4294459 100644 --- a/libs/components/src/tabs/index.ts +++ b/libs/components/src/tabs/index.ts @@ -1,3 +1,5 @@ export * from "./tabs.module"; -export * from "./tab-group.component"; -export * from "./tab-item.component"; +export * from "./tab-group/tab-group.component"; +export * from "./tab-group/tab.component"; +export * from "./tab-nav-bar/tab-nav-bar.component"; +export * from "./tab-nav-bar/tab-link.component"; diff --git a/libs/components/src/tabs/shared/tab-header.component.ts b/libs/components/src/tabs/shared/tab-header.component.ts new file mode 100644 index 00000000000..4712df0549a --- /dev/null +++ b/libs/components/src/tabs/shared/tab-header.component.ts @@ -0,0 +1,14 @@ +import { Component } from "@angular/core"; + +/** + * Component used for styling the tab header/background for both content and navigation tabs + */ +@Component({ + selector: "bit-tab-header", + host: { + class: + "tw-h-16 tw-pl-4 tw-bg-background-alt tw-flex tw-items-end tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300", + }, + template: ``, +}) +export class TabHeaderComponent {} diff --git a/libs/components/src/tabs/shared/tab-list-container.directive.ts b/libs/components/src/tabs/shared/tab-list-container.directive.ts new file mode 100644 index 00000000000..1cf8a762d58 --- /dev/null +++ b/libs/components/src/tabs/shared/tab-list-container.directive.ts @@ -0,0 +1,12 @@ +import { Directive } from "@angular/core"; + +/** + * Directive used for styling the container for bit tab labels + */ +@Directive({ + selector: "[bitTabListContainer]", + host: { + class: "tw-inline-flex tw-flex-wrap tw-leading-5", + }, +}) +export class TabListContainerDirective {} diff --git a/libs/components/src/tabs/shared/tab-list-item.directive.ts b/libs/components/src/tabs/shared/tab-list-item.directive.ts new file mode 100644 index 00000000000..d96b7adbc84 --- /dev/null +++ b/libs/components/src/tabs/shared/tab-list-item.directive.ts @@ -0,0 +1,85 @@ +import { FocusableOption } from "@angular/cdk/a11y"; +import { Directive, ElementRef, HostBinding, Input } from "@angular/core"; + +/** + * Directive used for styling tab header items for both nav links (anchor tags) + * and content tabs (button tags) + */ +@Directive({ selector: "[bitTabListItem]" }) +export class TabListItemDirective implements FocusableOption { + @Input() active: boolean; + @Input() disabled: boolean; + + @HostBinding("attr.disabled") + get disabledAttr() { + return this.disabled || null; // native disabled attr must be null when false + } + + constructor(private elementRef: ElementRef) {} + + focus() { + this.elementRef.nativeElement.focus(); + } + + click() { + this.elementRef.nativeElement.click(); + } + + @HostBinding("class") + get classList(): string[] { + return this.baseClassList + .concat(this.active ? this.activeClassList : []) + .concat(this.disabled ? this.disabledClassList : []); + } + + get baseClassList(): string[] { + return [ + "tw-block", + "tw-relative", + "tw-py-2", + "tw-px-4", + "tw-font-semibold", + "tw-transition", + "tw-rounded-t", + "tw-border-0", + "tw-border-x", + "tw-border-t-4", + "tw-border-transparent", + "tw-border-solid", + "tw-bg-transparent", + "tw-text-main", + "hover:tw-underline", + "hover:tw-text-main", + "focus-visible:tw-z-10", + "focus-visible:tw-outline-none", + "focus-visible:tw-ring-2", + "focus-visible:tw-ring-primary-700", + ]; + } + + get disabledClassList(): string[] { + return [ + "!tw-bg-secondary-100", + "!tw-text-muted", + "hover:!tw-text-muted", + "!tw-no-underline", + "tw-cursor-not-allowed", + ]; + } + + get activeClassList(): string[] { + return [ + "tw--mb-px", + "tw-border-x-secondary-300", + "tw-border-t-primary-500", + "tw-border-b", + "tw-border-b-background", + "tw-bg-background", + "!tw-text-primary-500", + "hover:tw-border-t-primary-700", + "hover:!tw-text-primary-700", + "focus-visible:tw-border-t-primary-700", + "focus-visible:!tw-text-primary-700", + ]; + } +} diff --git a/libs/components/src/tabs/tab-group.component.html b/libs/components/src/tabs/tab-group.component.html deleted file mode 100644 index f4b0ced5c4c..00000000000 --- a/libs/components/src/tabs/tab-group.component.html +++ /dev/null @@ -1,6 +0,0 @@ -
- -
diff --git a/libs/components/src/tabs/tab-group.component.ts b/libs/components/src/tabs/tab-group.component.ts deleted file mode 100644 index 856ab1f1e22..00000000000 --- a/libs/components/src/tabs/tab-group.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "bit-tab-group", - templateUrl: "./tab-group.component.html", -}) -export class TabGroupComponent {} diff --git a/libs/components/src/tabs/tab-group/tab-body.component.html b/libs/components/src/tabs/tab-group/tab-body.component.html new file mode 100644 index 00000000000..8134baf2a02 --- /dev/null +++ b/libs/components/src/tabs/tab-group/tab-body.component.html @@ -0,0 +1 @@ + diff --git a/libs/components/src/tabs/tab-group/tab-body.component.ts b/libs/components/src/tabs/tab-group/tab-body.component.ts new file mode 100644 index 00000000000..18baa49ed06 --- /dev/null +++ b/libs/components/src/tabs/tab-group/tab-body.component.ts @@ -0,0 +1,45 @@ +import { TemplatePortal } from "@angular/cdk/portal"; +import { Component, HostBinding, Input } from "@angular/core"; + +@Component({ + selector: "bit-tab-body", + templateUrl: "tab-body.component.html", +}) +export class TabBodyComponent { + private _firstRender: boolean; + + @Input() content: TemplatePortal; + @Input() preserveContent = false; + + @HostBinding("attr.hidden") get hidden() { + return !this.active || null; + } + + @Input() + get active() { + return this._active; + } + set active(value: boolean) { + this._active = value; + if (this._active) { + this._firstRender = true; + } + } + private _active: boolean; + + /** + * The tab content to render. + * Inactive tabs that have never been rendered/active do not have their + * content rendered by default for performance. If `preserveContent` is `true` + * then the content persists after the first time content is rendered. + */ + get tabContent() { + if (this.active) { + return this.content; + } + if (this.preserveContent && this._firstRender) { + return this.content; + } + return null; + } +} diff --git a/libs/components/src/tabs/tab-group/tab-group.component.html b/libs/components/src/tabs/tab-group/tab-group.component.html new file mode 100644 index 00000000000..dbe71fb4b99 --- /dev/null +++ b/libs/components/src/tabs/tab-group/tab-group.component.html @@ -0,0 +1,44 @@ + +
+ +
+
+
+ + +
diff --git a/libs/components/src/tabs/tab-group/tab-group.component.ts b/libs/components/src/tabs/tab-group/tab-group.component.ts new file mode 100644 index 00000000000..76859976dcc --- /dev/null +++ b/libs/components/src/tabs/tab-group/tab-group.component.ts @@ -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(); + 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