diff --git a/projects/angular-showcase/src/app/app.module.ts b/projects/angular-showcase/src/app/app.module.ts
index 498b1cbeb4..9c44147307 100644
--- a/projects/angular-showcase/src/app/app.module.ts
+++ b/projects/angular-showcase/src/app/app.module.ts
@@ -1,11 +1,12 @@
+import { MonacoEditorModule } from 'ngx-monaco-editor';
+import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
+
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ICON_COMPONENT_LIST, IconCollectionModule } from '@sbb-esta/angular-icons';
-import { MonacoEditorModule } from 'ngx-monaco-editor';
-import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
diff --git a/projects/angular-showcase/src/app/business/examples/examples.module.ts b/projects/angular-showcase/src/app/business/examples/examples.module.ts
index f45b00523e..2c5b89d60b 100644
--- a/projects/angular-showcase/src/app/business/examples/examples.module.ts
+++ b/projects/angular-showcase/src/app/business/examples/examples.module.ts
@@ -1,8 +1,10 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
+import { HeaderModule } from '@sbb-esta/angular-business';
+
@NgModule({
declarations: [],
- imports: [CommonModule]
+ imports: [CommonModule, HeaderModule]
})
export class ExamplesModule {}
diff --git a/projects/angular-showcase/src/app/public/examples/examples.module.ts b/projects/angular-showcase/src/app/public/examples/examples.module.ts
index 25f548f3fd..e5889b6b7a 100644
--- a/projects/angular-showcase/src/app/public/examples/examples.module.ts
+++ b/projects/angular-showcase/src/app/public/examples/examples.module.ts
@@ -1,12 +1,10 @@
+import { MonacoEditorModule } from 'ngx-monaco-editor';
+
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
-
import { RouterModule } from '@angular/router';
-
-import { MonacoEditorModule } from 'ngx-monaco-editor';
-
import { IconCollectionModule } from '@sbb-esta/angular-icons';
import {
AccordionModule,
diff --git a/projects/sbb-esta/angular-business/src/lib/header/header.md b/projects/sbb-esta/angular-business/src/lib/header/header.md
new file mode 100644
index 0000000000..8659b00bbb
--- /dev/null
+++ b/projects/sbb-esta/angular-business/src/lib/header/header.md
@@ -0,0 +1,32 @@
+# Header Overview
+
+Import header module in your application
+
+```ts
+import { HeaderModule } from '@sbb-esta/angular-business';
+```
+
+The header will appear at the top of the screen in a fixed position, and provide a container for navigation, usermenu, and eventually a logo.
+It supports <a> and <button> tags for navigation. Optionally an <sbb-usermenu> can be provided, as well as any element with a [brand] property, or .brand class, for replacing the standard logo.
+
+```html
+
+ A tag
+
+
+
+ Option 1
+ Option 2
+
+
+
+
+
+
+```
diff --git a/projects/sbb-esta/angular-business/src/lib/header/header.module.ts b/projects/sbb-esta/angular-business/src/lib/header/header.module.ts
new file mode 100644
index 0000000000..b569a58b66
--- /dev/null
+++ b/projects/sbb-esta/angular-business/src/lib/header/header.module.ts
@@ -0,0 +1,24 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import {
+ IconChevronSmallDownModule,
+ IconChevronSmallUpModule,
+ IconHamburgerMenuModule
+} from '@sbb-esta/angular-icons';
+import { DropdownModule } from '@sbb-esta/angular-public';
+
+import { HeaderComponent } from './header/header.component';
+import { NavbuttonComponent } from './navbutton/navbutton.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ IconHamburgerMenuModule,
+ IconChevronSmallDownModule,
+ IconChevronSmallUpModule,
+ DropdownModule
+ ],
+ declarations: [HeaderComponent, NavbuttonComponent],
+ exports: [HeaderComponent, NavbuttonComponent]
+})
+export class HeaderModule {}
diff --git a/projects/sbb-esta/angular-business/src/lib/header/header.ts b/projects/sbb-esta/angular-business/src/lib/header/header.ts
new file mode 100644
index 0000000000..1fd6faa579
--- /dev/null
+++ b/projects/sbb-esta/angular-business/src/lib/header/header.ts
@@ -0,0 +1,3 @@
+export * from './header.module';
+
+export * from './header/header.component';
diff --git a/projects/sbb-esta/angular-business/src/lib/header/header/header.component.html b/projects/sbb-esta/angular-business/src/lib/header/header/header.component.html
new file mode 100644
index 0000000000..d2c537a6b4
--- /dev/null
+++ b/projects/sbb-esta/angular-business/src/lib/header/header/header.component.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/sbb-esta/angular-business/src/lib/header/header/header.component.scss b/projects/sbb-esta/angular-business/src/lib/header/header/header.component.scss
new file mode 100644
index 0000000000..2188cea508
--- /dev/null
+++ b/projects/sbb-esta/angular-business/src/lib/header/header/header.component.scss
@@ -0,0 +1,218 @@
+@import 'common';
+
+$border: solid 2px $sbbColorGranite;
+$buttonMarginTop: calc(54px / 2 - 15px);
+
+@mixin headerBlockComponent {
+ position: absolute;
+ top: 0px;
+ height: 54px;
+}
+
+@mixin smallIconDropdown() {
+ position: relative;
+ top: 3px;
+
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+}
+
+@mixin navButtonDefaultButton() {
+ @include buttonResetFrameless();
+
+ position: relative;
+ white-space: nowrap;
+ font-family: $fontSbbLight;
+ font-size: pxToRem(15);
+ text-decoration: none;
+ text-align: left;
+ background: $sbbColorWhite; //TODO: this is required for overlaps
+
+ &:hover,
+ &:focus {
+ cursor: pointer;
+ outline: none;
+ }
+
+ &::after {
+ content: '';
+ display: block;
+ position: absolute;
+ bottom: 0;
+ width: 0;
+ left: 50%;
+ height: 1px;
+ border-bottom: 1px solid currentColor;
+ transition: width 0.3s, left 0.3s;
+ }
+
+ &.sbb-active::after,
+ &:not(.sbb-active):focus::after,
+ &:not(.sbb-active):hover::after {
+ left: 0;
+ width: 100%;
+ }
+
+ &:not(.sbb-active):focus &:not([aria-expanded='true']),
+ &:not(.sbb-active):hover {
+ color: $sbbColorRed125;
+
+ &::after {
+ border-bottom-color: $sbbColorRed125;
+ }
+ }
+
+ &[disabled] {
+ color: $sbbColorStorm;
+
+ &:hover,
+ &:focus {
+ color: $sbbColorStorm;
+ cursor: default;
+
+ &::after {
+ width: 0;
+ }
+ }
+ }
+
+ &.sbb-active:hover,
+ &.sbb-active:focus {
+ color: currentColor;
+ cursor: default;
+
+ &::after {
+ border-bottom-color: currentColor;
+ }
+ }
+}
+
+.sbb-header {
+ width: 100%;
+
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ z-index: 1000;
+ height: 54px;
+
+ background-color: $sbbColorWhite;
+ border-bottom: solid 1px $sbbColorSilver;
+
+ &-ribbon {
+ width: 80px;
+ position: absolute;
+ top: 12px;
+ left: -20px;
+ text-align: center;
+ line-height: 12px;
+ letter-spacing: 0px;
+ font-size: 10px;
+ color: $sbbColorSilver;
+ transform: rotate(-45deg);
+ }
+
+ &-appchooser {
+ @include headerBlockComponent();
+
+ left: 46px;
+ width: 54px;
+ padding: 17px;
+ }
+
+ &-titlebox {
+ @include headerBlockComponent();
+
+ left: 100px;
+ width: 200px;
+ font-family: $fontSbbLight;
+ &-label {
+ height: 23px;
+ width: 200px;
+ color: $sbbColorBlack;
+ font-size: 15px;
+ line-height: 23px;
+
+ margin-top: 8px;
+ }
+ &-subtitle {
+ height: 16px;
+ width: 200px;
+ color: $sbbColorAnthracite;
+ font-size: 13px;
+ font-weight: 300;
+ line-height: 16px;
+
+ margin-bottom: 7px;
+ }
+ }
+
+ &-mainnavigation {
+ @include headerBlockComponent();
+ left: 350px;
+
+ a {
+ @include navButtonDefaultButton();
+ white-space: nowrap;
+ position: absolute;
+ height: 30px;
+ margin-top: $buttonMarginTop;
+ display: flex;
+ }
+
+ .sbb-navbutton {
+ @include navButtonDefaultButton();
+ white-space: nowrap;
+ position: absolute;
+ height: 30px;
+ // top: -1px;
+ // margin-top: $buttonMarginTop;
+ margin-top: -1px;
+ top: $buttonMarginTop;
+ display: flex;
+
+ .sbb-navbutton-icon-small {
+ @include smallIconDropdown();
+ }
+ .sbb-navbutton-icon-small-expanded {
+ @include smallIconDropdown();
+ position: absolute;
+ right: 5px;
+ }
+ }
+
+ .sbb-navbutton.sbb-navbutton-dropdown-expanded {
+ border: $border;
+ border-bottom: none;
+
+ z-index: 5;
+
+ padding-left: calc(15px - 2px);
+ margin-left: -15px;
+ margin-top: calc($buttonMarginTop - 7px);
+ height: 35px;
+
+ &::after {
+ border-bottom: none;
+ }
+ }
+ }
+
+ &-usermenu {
+ @include headerBlockComponent();
+
+ right: 159px;
+ width: 200px;
+ z-index: 1;
+ }
+
+ &-logo {
+ @include headerBlockComponent();
+
+ top: 17px;
+ right: 35px;
+ width: 80px;
+ }
+}
diff --git a/projects/sbb-esta/angular-business/src/lib/header/header/header.component.spec.ts b/projects/sbb-esta/angular-business/src/lib/header/header/header.component.spec.ts
new file mode 100644
index 0000000000..a411241443
--- /dev/null
+++ b/projects/sbb-esta/angular-business/src/lib/header/header/header.component.spec.ts
@@ -0,0 +1,221 @@
+import { configureTestSuite } from 'ng-bullet';
+
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { IconCollectionModule } from '@sbb-esta/angular-icons';
+import { DropdownModule, UserMenuModule } from '@sbb-esta/angular-public';
+
+import { HeaderComponent } from '../../../public-api';
+import { NavbuttonComponent } from '../navbutton/navbutton.component';
+
+@Component({
+ selector: 'sbb-header-all-test',
+ template: `
+
+ link 1
+
+
+
+
+
+
+
+
+
+
+ `,
+ entryComponents: [NavbuttonComponent]
+})
+class HeaderTemplateAllSetTestComponent {}
+
+describe('HeaderComponent with everything set', () => {
+ let component: HeaderTemplateAllSetTestComponent;
+ let fixture: ComponentFixture;
+
+ configureTestSuite(() => {
+ TestBed.configureTestingModule({
+ imports: [IconCollectionModule, DropdownModule, UserMenuModule],
+ declarations: [HeaderComponent, HeaderTemplateAllSetTestComponent, NavbuttonComponent]
+ });
+ });
+
+ beforeEach(() => {
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;
+ fixture = TestBed.createComponent(HeaderTemplateAllSetTestComponent);
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should have ribbon', () => {
+ const ribbon = fixture.debugElement.queryAll(By.css('.sbb-header-ribbon'))[0];
+ expect(ribbon).toBeTruthy();
+ });
+
+ it('should have ribbon text and color match properties', () => {
+ const ribbon = fixture.debugElement.queryAll(By.css('.sbb-header-ribbon'))[0];
+ expect(ribbon.nativeElement.style.backgroundColor).toBe('red');
+ expect(ribbon.nativeElement.innerHTML.trim()).toBe('dev');
+ });
+
+ it('should have titlebox', () => {
+ const titlebox = fixture.debugElement.queryAll(By.css('.sbb-header-titlebox'))[0];
+ expect(titlebox).toBeTruthy();
+ });
+
+ it('should have titlebox label and subtitle match properties', () => {
+ const label = fixture.debugElement.queryAll(By.css('.sbb-header-titlebox-label'))[0];
+ const subtitle = fixture.debugElement.queryAll(By.css('.sbb-header-titlebox-subtitle'))[0];
+ expect(label).toBeTruthy();
+ expect(label.nativeElement.innerHTML.trim()).toBe('test');
+
+ expect(subtitle).toBeTruthy();
+ expect(subtitle.nativeElement.innerHTML.trim()).toBe('test subtitle');
+ });
+
+ it('should have mainnavigation', () => {
+ const mainnavigation = fixture.debugElement.queryAll(By.css('.sbb-header-mainnavigation'))[0];
+ expect(mainnavigation).toBeTruthy();
+ });
+
+ it('should have 1 navbutton and a link', () => {
+ const navbuttons = fixture.debugElement.queryAll(By.css('.sbb-navbutton'));
+ expect(navbuttons).toBeTruthy();
+ expect(navbuttons.length).toBe(1);
+
+ const links = fixture.debugElement.queryAll(By.css('a'));
+ expect(links).toBeTruthy();
+ expect(links.length).toBe(1);
+ });
+
+ it('should have button with working dropdown', () => {
+ const dropdownButton = fixture.debugElement.queryAll(By.directive(NavbuttonComponent))[0];
+ expect(dropdownButton.componentInstance.isDropdown).toBeTruthy();
+ });
+
+ it('should have usermenu', () => {
+ const usermenu = fixture.debugElement.queryAll(By.css('.sbb-header-usermenu'))[0];
+ expect(usermenu).toBeTruthy();
+ expect(usermenu.children.length).toBe(1);
+ });
+
+ it('should have logo', () => {
+ const logo = fixture.debugElement.queryAll(By.css('.sbb-header-logo'))[0];
+ expect(logo).toBeTruthy();
+ });
+});
+
+@Component({
+ selector: 'sbb-header-minimal-test',
+ template: `
+
+ `,
+ entryComponents: [NavbuttonComponent]
+})
+class HeaderTemplateMinimalTestComponent {}
+
+describe('HeaderComponent minimal', () => {
+ let component: HeaderTemplateMinimalTestComponent;
+ let fixture: ComponentFixture;
+
+ configureTestSuite(() => {
+ TestBed.configureTestingModule({
+ imports: [IconCollectionModule],
+ declarations: [HeaderComponent, HeaderTemplateMinimalTestComponent, NavbuttonComponent]
+ });
+ });
+
+ beforeEach(() => {
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;
+ fixture = TestBed.createComponent(HeaderTemplateMinimalTestComponent);
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should have no ribbon', () => {
+ const ribbon = fixture.debugElement.queryAll(By.css('.sbb-header-ribbon'))[0];
+ expect(ribbon).toBeFalsy();
+ });
+
+ it('should have titlebox', () => {
+ const titlebox = fixture.debugElement.queryAll(By.css('.sbb-header-titlebox'))[0];
+ expect(titlebox).toBeTruthy();
+ });
+
+ it('should have titlebox label and subtitle match properties', () => {
+ const label = fixture.debugElement.queryAll(By.css('.sbb-header-titlebox-label'))[0];
+ const subtitle = fixture.debugElement.queryAll(By.css('.sbb-header-titlebox-subtitle'))[0];
+ expect(label).toBeTruthy();
+ expect(label.nativeElement.innerHTML.trim()).toBe('test');
+
+ expect(subtitle).toBeFalsy();
+ });
+
+ // Mainnavigation is present regardless
+ it('should have mainnavigation', () => {
+ const mainnavigation = fixture.debugElement.queryAll(By.css('.sbb-header-mainnavigation'));
+ expect(mainnavigation).toBeTruthy();
+ expect(mainnavigation.length).toBe(1);
+ });
+
+ it('should have no navbuttons', () => {
+ const navbuttons = fixture.debugElement.queryAll(By.css('.sbb-navbutton'));
+ expect(navbuttons).toBeTruthy();
+ expect(navbuttons.length).toBe(0);
+ });
+
+ it('should have no usermenu', () => {
+ const usermenu = fixture.debugElement.queryAll(By.css('.sbb-header-usermenu'))[0];
+ expect(usermenu).toBeTruthy();
+ expect(usermenu.children.length).toBe(0);
+ });
+
+ it('should have logo', () => {
+ const logo = fixture.debugElement.queryAll(By.css('.sbb-header-logo'))[0];
+ expect(logo).toBeTruthy();
+ });
+});
+
+@Component({
+ selector: 'sbb-header-no-label-test',
+ template: `
+
+ `,
+ entryComponents: [NavbuttonComponent]
+})
+class HeaderTemplateNoLabelTestComponent {}
+
+describe('HeaderComponent without label', () => {
+ let fixture: ComponentFixture;
+
+ configureTestSuite(() => {
+ TestBed.configureTestingModule({
+ imports: [IconCollectionModule],
+ declarations: [HeaderComponent, HeaderTemplateNoLabelTestComponent, NavbuttonComponent]
+ });
+ });
+
+ beforeEach(() => {
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;
+ fixture = TestBed.createComponent(HeaderTemplateNoLabelTestComponent);
+ });
+
+ it('should not create', () => {
+ expect(() => fixture.detectChanges()).toThrow();
+ });
+});
diff --git a/projects/sbb-esta/angular-business/src/lib/header/header/header.component.ts b/projects/sbb-esta/angular-business/src/lib/header/header/header.component.ts
new file mode 100644
index 0000000000..7f98cb1f09
--- /dev/null
+++ b/projects/sbb-esta/angular-business/src/lib/header/header/header.component.ts
@@ -0,0 +1,99 @@
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ Component,
+ ElementRef,
+ HostBinding,
+ Input,
+ OnInit,
+ ViewChild,
+ ViewContainerRef,
+ ViewEncapsulation
+} from '@angular/core';
+
+@Component({
+ selector: 'sbb-header',
+ templateUrl: './header.component.html',
+ styleUrls: ['./header.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class HeaderComponent implements OnInit, AfterViewInit {
+ /** @docs-private */
+ @HostBinding('class.sbb-header') cssClass = true;
+
+ /**
+ * Main title shown in the header.
+ */
+ @Input()
+ label: String;
+
+ /**
+ * Subtitle shown below the main title, if present.
+ */
+ @Input()
+ subtitle?: String;
+
+ /**
+ * String representing the kind of environment the application is running in.
+ * Will be shown in a ribbon, top-left corner of the header.
+ */
+ @Input()
+ environment?: String;
+
+ /**
+ * Background color for the ribbon, if present.
+ */
+ @Input()
+ environmentColor?: String;
+
+ /**
+ * Reference to children elements projected through ng-content
+ */
+ @ViewChild('content', { static: true })
+ ngContent: ElementRef;
+
+ /**
+ * Reference to the template which will hold navbuttons wrapping projected elements
+ */
+ @ViewChild('navigationButtons', { static: true, read: ViewContainerRef })
+ navigationButtons: ViewContainerRef;
+
+ /**
+ * Reference to icon, if given through projection.
+ */
+ @ViewChild('iconContent', { static: true })
+ iconContent: ElementRef;
+
+ /**
+ * Distance between navigation buttons, handled through code.
+ */
+ buttonSpacing = 70;
+
+ /** @docs-private */
+ private _left = 0;
+
+ ngOnInit() {
+ this._checkLabel();
+ }
+
+ ngAfterViewInit() {
+ // Absolute positioning of buttons so that they're all 70px apart and won't
+ // move when one is expanded due to dropdowns, is decided here
+ const element = this.ngContent.nativeElement;
+ for (let k = 0; k < element.children.length; k++) {
+ const child = element.children[k];
+ child.style.left = this._left + 'px';
+ this._left += child.clientWidth + this.buttonSpacing;
+ }
+ }
+
+ /**
+ * Validates required inputs.
+ */
+ private _checkLabel() {
+ if (!this.label) {
+ throw new Error('You must set [label] for sbb-header.');
+ }
+ }
+}
diff --git a/projects/sbb-esta/angular-business/src/lib/header/navbutton/navbutton.component.html b/projects/sbb-esta/angular-business/src/lib/header/navbutton/navbutton.component.html
new file mode 100644
index 0000000000..4b862b3d42
--- /dev/null
+++ b/projects/sbb-esta/angular-business/src/lib/header/navbutton/navbutton.component.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/projects/sbb-esta/angular-business/src/lib/header/navbutton/navbutton.component.spec.ts b/projects/sbb-esta/angular-business/src/lib/header/navbutton/navbutton.component.spec.ts
new file mode 100644
index 0000000000..db852cc6a9
--- /dev/null
+++ b/projects/sbb-esta/angular-business/src/lib/header/navbutton/navbutton.component.spec.ts
@@ -0,0 +1,158 @@
+import { configureTestSuite } from 'ng-bullet';
+
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { IconCollectionModule } from '@sbb-esta/angular-icons';
+import { DropdownModule } from '@sbb-esta/angular-public';
+
+import { NavbuttonComponent } from '../navbutton/navbutton.component';
+
+@Component({
+ selector: 'sbb-navbutton-a-test',
+ template: `
+
+ `,
+ entryComponents: [NavbuttonComponent]
+})
+export class NavbuttonTemplateATagTestComponent {}
+
+describe('Navbutton with child', () => {
+ let component: NavbuttonTemplateATagTestComponent;
+ let fixture: ComponentFixture;
+
+ configureTestSuite(() => {
+ TestBed.configureTestingModule({
+ imports: [IconCollectionModule, DropdownModule],
+ declarations: [NavbuttonTemplateATagTestComponent, NavbuttonComponent]
+ });
+ });
+
+ beforeEach(() => {
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;
+ fixture = TestBed.createComponent(NavbuttonTemplateATagTestComponent);
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should recognize it has no dropdown', () => {
+ const navbutton = fixture.debugElement.queryAll(By.directive(NavbuttonComponent))[0];
+ expect(navbutton.componentInstance.isDropdown).toBeFalsy();
+ expect(navbutton.componentInstance.isDropdownExpanded).toBeFalsy();
+ });
+
+ it('should have no icons', () => {
+ const icons = fixture.debugElement.queryAll(By.css('.sbb-svgsprite-icon'));
+ expect(icons.length).toBe(0);
+ });
+});
+
+@Component({
+ selector: 'sbb-navbutton-button-test',
+ template: `
+
+ `,
+ entryComponents: [NavbuttonComponent]
+})
+class NavbuttonTemplateButtonTagTestComponent {}
+
+describe('Navbutton with child', () => {
+ let component: NavbuttonTemplateButtonTagTestComponent;
+ let fixture: ComponentFixture;
+
+ configureTestSuite(() => {
+ TestBed.configureTestingModule({
+ imports: [IconCollectionModule, DropdownModule],
+ declarations: [NavbuttonTemplateButtonTagTestComponent, NavbuttonComponent]
+ });
+ });
+
+ beforeEach(() => {
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;
+ fixture = TestBed.createComponent(NavbuttonTemplateButtonTagTestComponent);
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should recognize it has no dropdown', () => {
+ const navbutton = fixture.debugElement.queryAll(By.directive(NavbuttonComponent))[0];
+ expect(navbutton.componentInstance.isDropdown).toBeFalsy();
+ expect(navbutton.componentInstance.isDropdownExpanded).toBeFalsy();
+ });
+
+ it('should have no icons', () => {
+ const icons = fixture.debugElement.queryAll(By.css('.sbb-svgsprite-icon'));
+ expect(icons.length).toBe(0);
+ });
+});
+
+@Component({
+ selector: 'sbb-navbutton-dropdown-test',
+ template: `
+
+ only child
+
+
+ something
+
+ `,
+ entryComponents: [NavbuttonComponent]
+})
+class NavbuttonTemplateButtonDropdownTagTestComponent {}
+
+describe('Navbutton with child and dropdown', () => {
+ let component: NavbuttonTemplateButtonDropdownTagTestComponent;
+ let fixture: ComponentFixture;
+
+ configureTestSuite(() => {
+ TestBed.configureTestingModule({
+ imports: [IconCollectionModule, DropdownModule],
+ declarations: [NavbuttonTemplateButtonDropdownTagTestComponent, NavbuttonComponent]
+ });
+ });
+
+ beforeEach(() => {
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;
+ fixture = TestBed.createComponent(NavbuttonTemplateButtonDropdownTagTestComponent);
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should recognize the dropdown', () => {
+ const navbutton = fixture.debugElement.queryAll(By.directive(NavbuttonComponent))[0];
+ expect(navbutton.componentInstance.isDropdown).toBeTruthy();
+ expect(navbutton.componentInstance.isDropdownExpanded).toBeFalsy();
+ });
+
+ it('should recognize the dropdown is expanded after clicking', () => {
+ const navbutton = fixture.debugElement.queryAll(By.directive(NavbuttonComponent))[0];
+ navbutton.nativeElement.click();
+
+ fixture.detectChanges();
+ expect(navbutton.componentInstance.isDropdownExpanded).toBeTruthy();
+ });
+
+ it('should have exactly 1 icon', () => {
+ const icons = fixture.debugElement.queryAll(By.css('.sbb-svgsprite-icon'));
+ expect(icons.length).toBe(1);
+ });
+});
diff --git a/projects/sbb-esta/angular-business/src/lib/header/navbutton/navbutton.component.ts b/projects/sbb-esta/angular-business/src/lib/header/navbutton/navbutton.component.ts
new file mode 100644
index 0000000000..4b3d0bf329
--- /dev/null
+++ b/projects/sbb-esta/angular-business/src/lib/header/navbutton/navbutton.component.ts
@@ -0,0 +1,98 @@
+import { Subscription } from 'rxjs';
+
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ContentChild,
+ ElementRef,
+ HostBinding,
+ OnDestroy,
+ Optional,
+ SkipSelf,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
+import { DropdownTriggerDirective } from '@sbb-esta/angular-public';
+
+@Component({
+ // tslint:disable-next-line:component-selector
+ selector: 'button[sbbNavbutton]',
+ templateUrl: './navbutton.component.html',
+ styleUrls: ['../header/header.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class NavbuttonComponent implements AfterViewInit, OnDestroy {
+ /** @docs-private */
+ @HostBinding('class.sbb-navbutton') cssClass = true;
+
+ /**
+ * Button (or equivalent) component that navbutton wraps around.
+ */
+ @ViewChild('button', { static: true }) childButton: ElementRef;
+
+ /**
+ * Returns whether childButton's dropdown is expanded.
+ */
+ private _isDropdownExpanded = false;
+ @HostBinding('class.sbb-navbutton-dropdown-expanded')
+ get isDropdownExpanded() {
+ return this._isDropdownExpanded;
+ }
+
+ /**
+ * Css width to apply when the dropdown is opened.
+ */
+ dropdownWidth = '200px';
+
+ /** @docs-private */
+ private _subscriptions: Subscription[] = [];
+
+ constructor(
+ @SkipSelf() private _changeDetectorRef: ChangeDetectorRef,
+ private _el: ElementRef,
+ @Optional() private _dropdownTrigger: DropdownTriggerDirective
+ ) {}
+
+ ngAfterViewInit() {
+ if (this._dropdownTrigger) {
+ this._subscriptions = [
+ this._dropdownTrigger.dropdown.opened.subscribe(() => this._toggleDropdown(true)),
+ this._dropdownTrigger.dropdown.closed.subscribe(() => this._toggleDropdown(false))
+ ];
+ }
+ }
+
+ ngOnDestroy(): void {
+ while (this._subscriptions.length) {
+ this._subscriptions.shift().unsubscribe();
+ }
+ }
+
+ /**
+ * Returns whether childButton has a dropdown attached.
+ */
+ get isDropdown() {
+ return this._el.nativeElement && this._el.nativeElement.getAttribute('role') === 'combobox';
+ }
+
+ /**
+ * Will expand the dropdown panel and the button itself when the dropdown is opened.
+ * @param expanded Whether the dropdown has been expanded or not
+ */
+ private _toggleDropdown(expanded: boolean) {
+ if (this._dropdownTrigger) {
+ this._isDropdownExpanded = expanded;
+ this._el.nativeElement.style.width = expanded ? this.dropdownWidth : null;
+ this._dropdownTrigger.dropdown.panelWidth = expanded ? this.dropdownWidth : null;
+
+ if (expanded) {
+ // This will update the panel with the new width
+ this._dropdownTrigger.openPanel();
+ }
+ this._changeDetectorRef.detectChanges();
+ }
+ }
+}
diff --git a/projects/sbb-esta/angular-business/src/public-api.ts b/projects/sbb-esta/angular-business/src/public-api.ts
index a94991db91..04d8f1cd53 100644
--- a/projects/sbb-esta/angular-business/src/public-api.ts
+++ b/projects/sbb-esta/angular-business/src/public-api.ts
@@ -7,6 +7,7 @@ export * from './lib/button/button';
export * from './lib/checkbox/checkbox';
export * from './lib/datepicker/datepicker';
export * from './lib/field/field';
+export * from './lib/header/header';
export * from './lib/radio-button/radio-button';
export * from './lib/select/select';
export * from './lib/textarea/textarea';