diff --git a/src/packages/core/index.ts b/src/packages/core/index.ts index 181b831f..92575284 100644 --- a/src/packages/core/index.ts +++ b/src/packages/core/index.ts @@ -14,6 +14,7 @@ export * from './src/radio/index'; export * from './src/select/index'; export * from './src/snackbar/index'; export * from './src/spinner/index'; +export * from './src/tabs/index'; export * from './src/tag/index'; export * from './src/textarea/index'; export * from './src/tooltip/index'; diff --git a/src/packages/core/src/tabs/__tests__/tab-title.spec.ts b/src/packages/core/src/tabs/__tests__/tab-title.spec.ts new file mode 100644 index 00000000..c6db8831 --- /dev/null +++ b/src/packages/core/src/tabs/__tests__/tab-title.spec.ts @@ -0,0 +1,42 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NavigationExtras, Router, Routes } from '@angular/router'; + +import { ETabTitle } from '../tab-title'; +import { EHome } from '../demo/home/home'; + +describe('ETabTitle', (): void => { + let component: ETabTitle; + let fixture: ComponentFixture; + let router: Router; + const routes: Routes = [ + { + path: 'home', + component: EHome + } + ]; + + beforeEach(async((): void => { + TestBed.configureTestingModule({ + imports: [ RouterTestingModule.withRoutes(routes) ], + declarations: [ ETabTitle ] + }).compileComponents(); + })); + + beforeEach((): void => { + fixture = TestBed.createComponent(ETabTitle); + component = fixture.componentInstance; + router = TestBed.inject(Router); + }); + + it('should create', (): void => { + expect(component).toBeTruthy(); + }); + + it('should navigate', (): void => { + const navigateSpy: jasmine.Spy<(commands: any[], extras?: NavigationExtras) => Promise> = spyOn(router, 'navigate'); + + router.navigate(['home']); + expect(navigateSpy).toHaveBeenCalledWith(['home']); + }); +}); diff --git a/src/packages/core/src/tabs/__tests__/tab.spec.ts b/src/packages/core/src/tabs/__tests__/tab.spec.ts new file mode 100644 index 00000000..0f790677 --- /dev/null +++ b/src/packages/core/src/tabs/__tests__/tab.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ETab } from '../tab'; + +describe('ETab', (): void => { + let component: ETab; + let fixture: ComponentFixture; + + beforeEach(async((): void => { + TestBed.configureTestingModule({ + declarations: [ ETab ] + }) + .compileComponents(); + })); + + beforeEach((): void => { + fixture = TestBed.createComponent(ETab); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', (): void => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/packages/core/src/tabs/__tests__/tabs.spec.ts b/src/packages/core/src/tabs/__tests__/tabs.spec.ts new file mode 100644 index 00000000..101b3031 --- /dev/null +++ b/src/packages/core/src/tabs/__tests__/tabs.spec.ts @@ -0,0 +1,27 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {ETabs} from '../tabs'; +import {ETab} from '../tab'; + +describe('ETabs', (): void => { + let component: ETabs; + let fixture: ComponentFixture; + + beforeEach(async((): void => { + TestBed.configureTestingModule({ + declarations: [ ETabs, ETab ] + }).compileComponents(); + })); + + beforeEach((): void => { + fixture = TestBed.createComponent(ETabs); + component = fixture.componentInstance; + }); + + it('should create', (): void => { + expect(component).toBeTruthy(); + }); + + it('should have as className "undefined"', (): void => { + expect(component.className).toEqual(undefined); + }); +}); diff --git a/src/packages/core/src/tabs/demo/about/about.ts b/src/packages/core/src/tabs/demo/about/about.ts new file mode 100644 index 00000000..6d4b57ec --- /dev/null +++ b/src/packages/core/src/tabs/demo/about/about.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'e-about', + template: ` +

About Component

+ ` +}) +export class EAbout { } diff --git a/src/packages/core/src/tabs/demo/demo-routing.module.ts b/src/packages/core/src/tabs/demo/demo-routing.module.ts new file mode 100644 index 00000000..e390e6d9 --- /dev/null +++ b/src/packages/core/src/tabs/demo/demo-routing.module.ts @@ -0,0 +1,26 @@ +import {NgModule} from '@angular/core'; +import {Routes, RouterModule} from '@angular/router'; +import {EHome} from './home/home'; +import {EAbout} from './about/about'; + +const routes: Routes = [ + { + path: 'home', + component: EHome + }, + { + path: 'about', + component: EAbout + }, + { + path: '**', + redirectTo: 'home', + pathMatch: 'full' + } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class EDemoRoutingModule { } diff --git a/src/packages/core/src/tabs/demo/demo.module.ts b/src/packages/core/src/tabs/demo/demo.module.ts new file mode 100644 index 00000000..34eb7fe9 --- /dev/null +++ b/src/packages/core/src/tabs/demo/demo.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { EHome } from './home/home'; +import { EAbout } from './about/about'; +import { EDemo } from './demo'; +import { EDemoRoutingModule } from './demo-routing.module'; +import { ETabsModule } from '../tabs.module'; + +@NgModule({ + declarations: [EHome, EAbout, EDemo], + imports: [ + CommonModule, + RouterModule, + EDemoRoutingModule, + ETabsModule + ], + exports: [EDemo, EHome, EAbout] +}) +export class EDemoModule { } diff --git a/src/packages/core/src/tabs/demo/demo.ts b/src/packages/core/src/tabs/demo/demo.ts new file mode 100644 index 00000000..62aa639d --- /dev/null +++ b/src/packages/core/src/tabs/demo/demo.ts @@ -0,0 +1,29 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'e-demo', + template: ` + + + + + + ` +}) +export class EDemo { + + public tabs: any[] = [ + { + title: 'Home', + link: 'home' + }, + { + title: 'About', + link: 'about' + } + ]; + +} diff --git a/src/packages/core/src/tabs/demo/home/home.ts b/src/packages/core/src/tabs/demo/home/home.ts new file mode 100644 index 00000000..35f4f51c --- /dev/null +++ b/src/packages/core/src/tabs/demo/home/home.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'e-home', + template: ` +

Home Component

+ ` +}) +export class EHome {} diff --git a/src/packages/core/src/tabs/index.ts b/src/packages/core/src/tabs/index.ts new file mode 100644 index 00000000..d39f64d5 --- /dev/null +++ b/src/packages/core/src/tabs/index.ts @@ -0,0 +1,4 @@ +export * from './tabs'; +export * from './tab'; +export * from './tab-title'; +export * from './tabs.module'; diff --git a/src/packages/core/src/tabs/tab-title.html b/src/packages/core/src/tabs/tab-title.html new file mode 100644 index 00000000..bf220cf6 --- /dev/null +++ b/src/packages/core/src/tabs/tab-title.html @@ -0,0 +1,22 @@ +
+ {{ tab.titleStr }} + +
+
+ {{ tab.titleStr }} + +
diff --git a/src/packages/core/src/tabs/tab-title.ts b/src/packages/core/src/tabs/tab-title.ts new file mode 100644 index 00000000..4db55269 --- /dev/null +++ b/src/packages/core/src/tabs/tab-title.ts @@ -0,0 +1,13 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { ETab } from './tab'; + +@Component({ + selector: 'div[eTabTitle]', + templateUrl: './tab-title.html', + encapsulation: ViewEncapsulation.None +}) +export class ETabTitle { + + @Input() public tab: ETab; + +} diff --git a/src/packages/core/src/tabs/tab.html b/src/packages/core/src/tabs/tab.html new file mode 100644 index 00000000..e2cb802d --- /dev/null +++ b/src/packages/core/src/tabs/tab.html @@ -0,0 +1,6 @@ +
+ +
diff --git a/src/packages/core/src/tabs/tab.ts b/src/packages/core/src/tabs/tab.ts new file mode 100644 index 00000000..855e0dff --- /dev/null +++ b/src/packages/core/src/tabs/tab.ts @@ -0,0 +1,47 @@ +import {Component, Input, TemplateRef, ViewEncapsulation} from '@angular/core'; + +@Component({ + selector: 'e-tab', + templateUrl: './tab.html', + encapsulation: ViewEncapsulation.None +}) +export class ETab { + + @Input() public tabIndex: number; + /** + * Tab selected state. + */ + @Input() public selected: boolean = false; + + /** + * Router link. + */ + @Input() public routerLink: string | string[]; + + /** + * Router query params (model: {[k: string]: string}). + */ + @Input() public queryParams: { [k: string]: string }; + + /** + * Plain text title for the tab. + */ + @Input() public set title(value: string | TemplateRef) { + if (value instanceof TemplateRef) { + this.titleTpl = value; + } else { + this.titleStr = value; + } + } + /** + * Text title for the tab. + * @internal + */ + public titleStr: string; + /** + * Template title for the tab. + * @internal + */ + public titleTpl: TemplateRef; + +} diff --git a/src/packages/core/src/tabs/tabs.html b/src/packages/core/src/tabs/tabs.html new file mode 100644 index 00000000..21da7a6e --- /dev/null +++ b/src/packages/core/src/tabs/tabs.html @@ -0,0 +1,10 @@ + + diff --git a/src/packages/core/src/tabs/tabs.module.ts b/src/packages/core/src/tabs/tabs.module.ts new file mode 100644 index 00000000..2bcb3ae1 --- /dev/null +++ b/src/packages/core/src/tabs/tabs.module.ts @@ -0,0 +1,16 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {RouterModule} from '@angular/router'; +import {ETabs} from './tabs'; +import {ETab} from './tab'; +import {ETabTitle} from './tab-title'; + +@NgModule({ + declarations: [ETabs, ETab, ETabTitle], + imports: [ + CommonModule, + RouterModule + ], + exports: [ETabs, ETab, ETabTitle] +}) +export class ETabsModule { } diff --git a/src/packages/core/src/tabs/tabs.stories.mdx b/src/packages/core/src/tabs/tabs.stories.mdx new file mode 100644 index 00000000..bdd05e5c --- /dev/null +++ b/src/packages/core/src/tabs/tabs.stories.mdx @@ -0,0 +1,58 @@ +import { Meta, Props, Preview, Story } from '@storybook/addon-docs/blocks'; +import { moduleMetadata } from '@storybook/angular'; +import { RouterModule } from '@angular/router'; +import { APP_BASE_HREF } from '@angular/common'; + +import { ETab, ETabs, ETabsModule } from './'; +import { EBadgeModule } from '../badge' +import { EDemoModule } from './demo/demo.module'; + +# Tabs + + + + + + + + + {{ + template: ` + + + Tab 1 content + + + + Tab 2 + 2 + + Tab 2 content + + + `, + props: { + tab1: 'Tab 1', + } + }} + + + + + + {{ + template: ` + + `, + }} + + + diff --git a/src/packages/core/src/tabs/tabs.ts b/src/packages/core/src/tabs/tabs.ts new file mode 100644 index 00000000..fbc85cb4 --- /dev/null +++ b/src/packages/core/src/tabs/tabs.ts @@ -0,0 +1,36 @@ +import {AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, Input, QueryList, ViewEncapsulation} from '@angular/core'; +import {ETab} from './tab'; + +@Component({ + selector: 'e-tabs', + templateUrl: './tabs.html', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ETabs implements AfterContentInit { + + /** + * Additional CSS class. + */ + @Input() public className: string; + /** + * @internal + */ + @ContentChildren(ETab) public tabs: QueryList; + + public ngAfterContentInit(): void { + const activeTabs: ETab[] = this.tabs.filter((tab: ETab): boolean => tab.selected); + if (!activeTabs.length) { + this.onSelectTab(this.tabs.first); + } + } + + /** + * @internal + */ + public onSelectTab(tab: ETab): void { + this.tabs.toArray().forEach((t: ETab): boolean => t.selected = false); + tab.selected = true; + } + +}