From debaef29415a0c2fc999acc1181625fb5ed0ce22 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 26 Sep 2022 14:41:51 -0700 Subject: [PATCH] [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 --- libs/components/src/tabs/index.ts | 6 +- .../src/tabs/shared/tab-header.component.ts | 14 ++ .../shared/tab-list-container.directive.ts | 12 ++ .../tabs/shared/tab-list-item.directive.ts | 85 ++++++++ .../src/tabs/tab-group.component.html | 6 - .../src/tabs/tab-group.component.ts | 7 - .../tabs/tab-group/tab-body.component.html | 1 + .../src/tabs/tab-group/tab-body.component.ts | 45 +++++ .../tabs/tab-group/tab-group.component.html | 44 +++++ .../src/tabs/tab-group/tab-group.component.ts | 187 ++++++++++++++++++ .../src/tabs/tab-group/tab-label.directive.ts | 22 +++ .../src/tabs/tab-group/tab.component.html | 1 + .../src/tabs/tab-group/tab.component.ts | 42 ++++ .../src/tabs/tab-item.component.html | 26 --- .../components/src/tabs/tab-item.component.ts | 54 ----- .../tabs/tab-nav-bar/tab-link.component.html | 13 ++ .../tabs/tab-nav-bar/tab-link.component.ts | 51 +++++ .../tab-nav-bar/tab-nav-bar.component.html | 5 + .../tabs/tab-nav-bar/tab-nav-bar.component.ts | 43 ++++ libs/components/src/tabs/tabs.module.ts | 34 +++- libs/components/src/tabs/tabs.stories.ts | 67 +++++-- 21 files changed, 653 insertions(+), 112 deletions(-) create mode 100644 libs/components/src/tabs/shared/tab-header.component.ts create mode 100644 libs/components/src/tabs/shared/tab-list-container.directive.ts create mode 100644 libs/components/src/tabs/shared/tab-list-item.directive.ts delete mode 100644 libs/components/src/tabs/tab-group.component.html delete mode 100644 libs/components/src/tabs/tab-group.component.ts create mode 100644 libs/components/src/tabs/tab-group/tab-body.component.html create mode 100644 libs/components/src/tabs/tab-group/tab-body.component.ts create mode 100644 libs/components/src/tabs/tab-group/tab-group.component.html create mode 100644 libs/components/src/tabs/tab-group/tab-group.component.ts create mode 100644 libs/components/src/tabs/tab-group/tab-label.directive.ts create mode 100644 libs/components/src/tabs/tab-group/tab.component.html create mode 100644 libs/components/src/tabs/tab-group/tab.component.ts delete mode 100644 libs/components/src/tabs/tab-item.component.html delete mode 100644 libs/components/src/tabs/tab-item.component.ts create mode 100644 libs/components/src/tabs/tab-nav-bar/tab-link.component.html create mode 100644 libs/components/src/tabs/tab-nav-bar/tab-link.component.ts create mode 100644 libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.html create mode 100644 libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts diff --git a/libs/components/src/tabs/index.ts b/libs/components/src/tabs/index.ts index 9b45ff1d43..e89b429445 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 0000000000..4712df0549 --- /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 0000000000..1cf8a762d5 --- /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 0000000000..d96b7adbc8 --- /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 f4b0ced5c4..0000000000 --- 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 856ab1f1e2..0000000000 --- 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 0000000000..8134baf2a0 --- /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 0000000000..18baa49ed0 --- /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 0000000000..dbe71fb4b9 --- /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 0000000000..76859976dc --- /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