[ADF-2559] Sidenav layout (#3119)

* First step of extraction

* Add sidenav-layout to demo-shell

* Fix some sidenav-layout styles

* Fix tests

* Add documentation

* Fix typescript transpiler's stomache

* Modify component's directive's interface

* Enrich documentation

* Another documentation enrichment

* Fix tests and mobile expanded issue
This commit is contained in:
Popovics András
2018-03-28 13:49:52 +01:00
committed by Eugenio Romano
parent 9e2969b955
commit 0aba5bb1b5
23 changed files with 942 additions and 42 deletions

View File

@@ -1,47 +1,55 @@
<mat-sidenav-container class="adf-nav-container">
<mat-sidenav #sidenav class="adf-sidenav" position="end" mode="push">
<mat-nav-list>
<a mat-list-item *ngFor="let link of links" [routerLink]="link.href" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }" (click)="sidenav.close()" class="adf-sidenav-link">
<mat-icon matListIcon>{{link.icon}}</mat-icon>
<span>{{link.title | translate }}</span>
</a>
<a mat-list-item adf-logout (click)="sidenav.close()">
<mat-icon matListIcon>exit_to_app</mat-icon>
<span>Logout</span>
</a>
</mat-nav-list>
</mat-sidenav>
<mat-toolbar color="primary" class="adf-app-layout-toolbar mat-elevation-z6"> <adf-sidenav-layout [sidenavMin]="70" [sidenavMax]="220" [stepOver]="600" [hideSidenav]="false" [expandedSidenav]="false">
<adf-userinfo
class="adf-app-layout-user-profile"
[menuPositionX]="'before'"
[menuPositionY]="'above'">
</adf-userinfo>
<span fxFlex="1 1 auto" fxShow fxHide.lt-sm="true">{{'APP_LAYOUT.APP_NAME' | translate }}</span> <adf-sidenav-layout-header>
<ng-template let-toggleMenu="toggleMenu">
<mat-toolbar color="primary" class="adf-app-layout-toolbar mat-elevation-z6">
<button mat-icon-button (click)="toggleMenu()">
<mat-icon>menu</mat-icon>
</button>
<div class="adf-app-layout-menu-spacer"></div> <span fxFlex="1 1 auto" fxShow fxHide.lt-sm="true">{{'APP_LAYOUT.APP_NAME' | translate }}</span>
<app-search-bar fxFlex="0 1 auto"></app-search-bar> <div class="adf-app-layout-menu-spacer"></div>
<a fxFlex="0 0 auto" class="adf-toolbar-link" fxShow fxHide.lt-md="true" mat-button data-automation-id="home" href="" routerLink="/home" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">{{'APP_LAYOUT.HOME' | translate }}</a> <app-search-bar fxFlex="0 1 auto"></app-search-bar>
<a fxFlex="0 0 auto" class="adf-toolbar-link" fxShow fxHide.lt-md="true" mat-button data-automation-id="files" href="" routerLink="/files" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">{{'APP_LAYOUT.CONTENT_SERVICES' | translate }}</a>
<a fxFlex="0 0 auto" class="adf-toolbar-link" fxShow fxHide.lt-md="true" mat-button data-automation-id="activiti" href="" routerLink="/activiti" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">{{'APP_LAYOUT.PROCESS_SERVICES' | translate }}</a>
<a fxFlex="0 0 auto" class="adf-toolbar-link" fxShow fxHide.lt-md="true" mat-button data-automation-id="login" href="" routerLink="/login">Login</a>
<app-theme-picker></app-theme-picker> <adf-userinfo
<button mat-icon-button [matMenuTriggerFor]="langMenu"> class="adf-app-layout-user-profile"
<mat-icon>language</mat-icon> [menuPositionX]="'before'"
</button> [menuPositionY]="'above'">
<mat-menu #langMenu="matMenu"> </adf-userinfo>
<adf-language-menu></adf-language-menu>
</mat-menu>
<button mat-icon-button (click)="sidenav.open()"> <app-theme-picker></app-theme-picker>
<mat-icon>menu</mat-icon> <button mat-icon-button [matMenuTriggerFor]="langMenu">
</button> <mat-icon>language</mat-icon>
</mat-toolbar> </button>
<mat-menu #langMenu="matMenu">
<adf-language-menu></adf-language-menu>
</mat-menu>
</mat-toolbar>
</ng-template>
</adf-sidenav-layout-header>
<router-outlet></router-outlet> <adf-sidenav-layout-navigation>
</mat-sidenav-container> <ng-template let-isMenuMinimized="isMenuMinimized">
<mat-nav-list class="adf-sidenav-linklist">
<a mat-list-item *ngFor="let link of links" [routerLink]="link.href" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }" class="adf-sidenav-link">
<mat-icon matListIcon class="sidenav-menu-icon">{{link.icon}}</mat-icon>
<div class="sidenav-menu-label" *ngIf="!isMenuMinimized()">{{link.title | translate }}</div>
</a>
<a mat-list-item adf-logout class="adf-sidenav-link">
<mat-icon matListIcon class="sidenav-menu-icon">exit_to_app</mat-icon>
<div class="sidenav-menu-label" *ngIf="!isMenuMinimized()">Logout</div>
</a>
</mat-nav-list>
</ng-template>
</adf-sidenav-layout-navigation>
<adf-sidenav-layout-content>
<ng-template>
<router-outlet></router-outlet>
</ng-template>
</adf-sidenav-layout-content>
</adf-sidenav-layout>

View File

@@ -1,5 +1,7 @@
@mixin adf-app-layout-theme($theme) { @mixin adf-app-layout-theme($theme) {
$primary: map-get($theme, primary); $primary: map-get($theme, primary);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$minimumAppWidth: 320px; $minimumAppWidth: 320px;
$toolbarHeight: 64px; $toolbarHeight: 64px;
@@ -22,7 +24,8 @@
} }
.adf-app-layout { .adf-app-layout {
display: block; display: flex;
flex: 1;
min-width: $minimumAppWidth; min-width: $minimumAppWidth;
height: 100%; height: 100%;
@@ -32,10 +35,32 @@
height: 100%; height: 100%;
} }
.adf-sidenav-linklist {
height: 100%;
overflow: auto;
padding-bottom: 8px;
box-sizing: border-box;
}
.adf-sidenav-link { .adf-sidenav-link {
&.active { &.active {
color: mat-color($primary); color: mat-color($primary);
} }
.sidenav-menu-icon {
margin-right: 20px;
font-size: 14px;
}
.sidenav-menu-label {
font-size: 14px;
white-space: nowrap;
}
}
.mat-nav-list .mat-list-item.adf-sidenav-link {
height: 40px;
} }
&-user-profile { &-user-profile {

View File

@@ -30,9 +30,9 @@ export class AppLayoutComponent {
links: Array<any> = [ links: Array<any> = [
{ href: '/home', icon: 'home', title: 'APP_LAYOUT.HOME' }, { href: '/home', icon: 'home', title: 'APP_LAYOUT.HOME' },
{ href: '/files', icon: 'folder_open', title: 'APP_LAYOUT.CONTENT_SERVICES' }, { href: '/files', icon: 'folder_open', title: 'APP_LAYOUT.CONTENT_SERVICES' },
{ href: '/trashcan', icon: 'delete', title: 'APP_LAYOUT.TRASHCAN' },
{ href: '/activiti', icon: 'device_hub', title: 'APP_LAYOUT.PROCESS_SERVICES' }, { href: '/activiti', icon: 'device_hub', title: 'APP_LAYOUT.PROCESS_SERVICES' },
{ href: '/login', icon: 'vpn_key', title: 'APP_LAYOUT.LOGIN' }, { href: '/login', icon: 'vpn_key', title: 'APP_LAYOUT.LOGIN' },
{ href: '/trashcan', icon: 'delete', title: 'APP_LAYOUT.TRASHCAN' },
{ href: '/dl-custom-sources', icon: 'extension', title: 'APP_LAYOUT.CUSTOM_SOURCES' }, { href: '/dl-custom-sources', icon: 'extension', title: 'APP_LAYOUT.CUSTOM_SOURCES' },
{ href: '/datatable', icon: 'view_module', title: 'APP_LAYOUT.DATATABLE' }, { href: '/datatable', icon: 'view_module', title: 'APP_LAYOUT.DATATABLE' },
{ href: '/datatable-lazy', icon: 'view_module', title: 'APP_LAYOUT.DATATABLE_LAZY' }, { href: '/datatable-lazy', icon: 'view_module', title: 'APP_LAYOUT.DATATABLE_LAZY' },

View File

@@ -1,4 +1,4 @@
<header class="adf-home-background adf-primary-background-color"> <header class="adf-home-background">
<div class="adf-home-section ad"> <div class="adf-home-section ad">
<div class="adf-home-headline"> <div class="adf-home-headline">
<h1 class="mat-h1">ADF</h1> <h1 class="mat-h1">ADF</h1>

View File

@@ -1,3 +1,9 @@
:host {
display: flex;
justify-content: center;
align-items: center;
}
.adf-home-header-background { .adf-home-header-background {
overflow: hidden; overflow: hidden;
} }
@@ -8,6 +14,7 @@
} }
.adf-home-headline { .adf-home-headline {
h1 { h1 {
font-size: 56px; font-size: 56px;
font-weight: 300; font-weight: 300;

View File

@@ -0,0 +1,78 @@
---
Added: v2.3.0
Status: Active
---
# Sidenav layout component
A generalised component to help displayig the "ADF style" application frame. The layout consists of 3 regions:
- header
- navigation
- content
The navigation (depending on the screensize) either uses the Angular Material Sidenav (on small breakpoint), or the ADF style Sidenav (on bigger breakpoint).
- For Angular Material Sidenav see examples on the Angular Material project's site.
- The ADF style Sidenav has 2 states: **expanded** and **compact**. Regardless of the state, it is always displayed on the screen, either in small width (compact) or in bigger width (expanded).
The contents of the 3 regions can be injected through Angular's template transclusion. For more details see the usage example of the components.
On desktop (above stepOver):
<img src="../docassets/images/sidenav-layout.png" width="800">
On mobile (below stepOver):
<img src="../docassets/images/sidenav-layout-mobile.png" width="800">
## Basic Usage
```html
<adf-sidenav-layout
[sidenavMin]="70"
[sidenavMax]="220"
[stepOver]="600"
[hideSidenav]="false"
[expandedSidenav]="true">
<adf-sidenav-layout-header>
<ng-template let-toggleMenu="toggleMenu">
<div class="app-header">
<button (click)="toggleMenu()">toggle menu</button>
</div>
</ng-template>
</adf-sidenav-layout-header>
<adf-sidenav-layout-navigation>
<ng-template let-isMenuMinimized="isMenuMinimized">
<div *ngIf="isMenuMinimized()" class="app-compact-navigation"></div>
<div *ngIf="!isMenuMinimized()" class="app-expanded-navigation"></div>
</ng-template>
</adf-sidenav-layout-navigation>
<adf-sidenav-layout-content>
<ng-template>
<router-outlet></router-outlet>
</ng-template>
</adf-sidenav-layout-content>
</adf-sidenav-layout>
```
### Properties
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| sidenavMin | number | - | (**required**) the compact width in pixels |
| sidenavMax | number | - | (**required**) the expanded width in pixels |
| stepOver | number | - | (**required**) The breakpoint in pixels, where to step over to mobile/desktop |
| hideSidenav | boolean | false | Hide the navigation or not |
| expandedSidenav | boolean | true | The initial (expanded/compact) state of the navigation |
## Template context
Each template is given the context containing the following methods:
### toggleMenu(): void
Trigger menu toggling
### isMenuMinimized(): boolean
The expanded/compact (minimized) state of the navigation. This one only makes sense in case of desktop size, when the screen size is above the value of stepOver.

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -37,6 +37,7 @@ import { ToolbarModule } from './toolbar/toolbar.module';
import { UserInfoModule } from './userinfo/userinfo.module'; import { UserInfoModule } from './userinfo/userinfo.module';
import { ViewerModule } from './viewer/viewer.module'; import { ViewerModule } from './viewer/viewer.module';
import { FormModule } from './form/form.module'; import { FormModule } from './form/form.module';
import { SidenavLayoutModule } from './sidenav-layout/sidenav-layout.module';
import { SideBarActionModule } from './sidebar/sidebar-action.module'; import { SideBarActionModule } from './sidebar/sidebar-action.module';
import { DirectiveModule } from './directives/directive.module'; import { DirectiveModule } from './directives/directive.module';
@@ -122,6 +123,7 @@ export function providers() {
@NgModule({ @NgModule({
imports: [ imports: [
ViewerModule, ViewerModule,
SidenavLayoutModule,
SideBarActionModule, SideBarActionModule,
PipeModule, PipeModule,
CommonModule, CommonModule,
@@ -155,6 +157,7 @@ export function providers() {
exports: [ exports: [
ViewerModule, ViewerModule,
SideBarActionModule, SideBarActionModule,
SidenavLayoutModule,
PipeModule, PipeModule,
CommonModule, CommonModule,
DirectiveModule, DirectiveModule,
@@ -186,6 +189,7 @@ export class CoreModuleLazy {
imports: [ imports: [
ViewerModule, ViewerModule,
SideBarActionModule, SideBarActionModule,
SidenavLayoutModule,
PipeModule, PipeModule,
CommonModule, CommonModule,
DirectiveModule, DirectiveModule,
@@ -218,6 +222,7 @@ export class CoreModuleLazy {
exports: [ exports: [
ViewerModule, ViewerModule,
SideBarActionModule, SideBarActionModule,
SidenavLayoutModule,
PipeModule, PipeModule,
CommonModule, CommonModule,
DirectiveModule, DirectiveModule,

View File

@@ -0,0 +1,13 @@
<mat-sidenav-container>
<mat-sidenav
[ngClass]="{ 'sidenav--hidden': hideSidenav }"
[@sidenavAnimation]="sidenavAnimationState"
[opened]="!isMobileScreenSize"
[mode]="isMobileScreenSize ? 'over' : 'side'">
<ng-content sidenav select="[app-layout-navigation]"></ng-content>
</mat-sidenav>
<mat-sidenav-content [@contentAnimation]="contentAnimationState">
<ng-content select="[app-layout-content]"></ng-content>
</mat-sidenav-content>
</mat-sidenav-container>

View File

@@ -0,0 +1,39 @@
:host {
display: block;
width: 100%;
height: 100%;
overflow: hidden;
}
ng-content {
display: block;
width: 100%;
height: 100%;
overflow: hidden;
}
.sidenav--hidden {
visibility: hidden !important;
width: 0 !important;
transform: unset !important;
opacity: 0 !important;
}
.mat-sidenav-container {
display: block;
width: 100%;
height: 100%;
overflow: hidden;
}
.mat-sidenav {
overflow: hidden;
}
.mat-sidenav-content,
.mat-drawer-transition .mat-drawer-content {
transform: unset !important;
transition-property: unset !important;
transition-duration: unset !important;
transition-timing-function: unset !important;
}

View File

@@ -0,0 +1,109 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { MatSidenav } from '@angular/material';
import { sidenavAnimation, contentAnimation } from '../../helpers/animations';
@Component({
selector: 'adf-layout-container',
templateUrl: './layout-container.component.html',
styleUrls: ['./layout-container.component.scss'],
animations: [ sidenavAnimation, contentAnimation ]
})
export class LayoutContainerComponent implements OnInit, OnDestroy {
@Input() sidenavMin: number;
@Input() sidenavMax: number;
@Input() mediaQueryList: MediaQueryList;
@Input() hideSidenav = false;
@Input() expandedSidenav = true;
@ViewChild(MatSidenav) sidenav: MatSidenav;
sidenavAnimationState: any;
contentAnimationState: any;
SIDENAV_STATES = { MOBILE: {}, EXPANDED: {}, COMPACT: {} };
CONTENT_STATES = { MOBILE: {}, EXPANDED: {}, COMPACT: {} };
constructor() {
this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
}
ngOnInit() {
this.SIDENAV_STATES.MOBILE = { value: 'expanded', params: { width: this.sidenavMax } };
this.SIDENAV_STATES.EXPANDED = { value: 'expanded', params: { width: this.sidenavMax } };
this.SIDENAV_STATES.COMPACT = { value: 'compact', params: {width: this.sidenavMin } };
this.CONTENT_STATES.MOBILE = { value: 'expanded', params: { marginLeft: 0 } };
this.CONTENT_STATES.EXPANDED = { value: 'expanded', params: { marginLeft: this.sidenavMin } };
this.CONTENT_STATES.COMPACT = { value: 'compact', params: { marginLeft: this.sidenavMax } };
this.mediaQueryList.addListener(this.onMediaQueryChange);
if (this.isMobileScreenSize) {
this.sidenavAnimationState = this.SIDENAV_STATES.MOBILE;
this.contentAnimationState = this.CONTENT_STATES.MOBILE;
} else if (this.expandedSidenav) {
this.sidenavAnimationState = this.SIDENAV_STATES.EXPANDED;
this.contentAnimationState = this.CONTENT_STATES.COMPACT;
} else {
this.sidenavAnimationState = this.SIDENAV_STATES.COMPACT;
this.contentAnimationState = this.CONTENT_STATES.EXPANDED;
}
}
ngOnDestroy(): void {
this.mediaQueryList.removeListener(this.onMediaQueryChange);
}
toggleMenu(): void {
if (this.isMobileScreenSize) {
this.sidenav.toggle();
} else {
this.sidenavAnimationState = this.toggledSidenavAnimation;
this.contentAnimationState = this.toggledContentAnimation;
}
}
private get toggledSidenavAnimation() {
return this.sidenavAnimationState === this.SIDENAV_STATES.EXPANDED
? this.SIDENAV_STATES.COMPACT
: this.SIDENAV_STATES.EXPANDED;
}
private get toggledContentAnimation() {
if (this.isMobileScreenSize) {
return this.CONTENT_STATES.MOBILE;
}
if (this.sidenavAnimationState === this.SIDENAV_STATES.EXPANDED) {
return this.CONTENT_STATES.COMPACT;
} else {
return this.CONTENT_STATES.EXPANDED;
}
}
private get isMobileScreenSize(): boolean {
return this.mediaQueryList.matches;
}
private onMediaQueryChange() {
this.sidenavAnimationState = this.SIDENAV_STATES.EXPANDED;
this.contentAnimationState = this.toggledContentAnimation;
}
}

View File

@@ -0,0 +1,26 @@
<div class="sidenav-layout">
<ng-container *ngIf="!isHeaderInside">
<ng-container class="adf-sidenav-layout-outer-header" *ngTemplateOutlet="headerTemplate; context:templateContext"></ng-container>
</ng-container>
<adf-layout-container #container
[sidenavMin]="sidenavMin"
[sidenavMax]="sidenavMax"
[mediaQueryList]="mediaQueryList"
[hideSidenav]="hideSidenav"
[expandedSidenav]="expandedSidenav"
data-automation-id="adf-layout-container"
class="layout__content">
<ng-container app-layout-navigation *ngTemplateOutlet="navigationTemplate; context:templateContext"></ng-container>
<ng-container app-layout-content>
<ng-container *ngIf="isHeaderInside">
<ng-container *ngTemplateOutlet="headerTemplate; context:templateContext"></ng-container>
</ng-container>
<ng-container *ngTemplateOutlet="contentTemplate; context:templateContext"></ng-container>
</ng-container>
</adf-layout-container>
</div>
<ng-template #emptyTemplate></ng-template>

View File

@@ -0,0 +1,18 @@
:host {
display: flex;
flex: 1;
.sidenav-layout {
width: 100%;
display: flex;
flex-direction: column;
.layout__content {
flex: 1 1 auto;
}
}
router-outlet {
flex: 0 0;
}
}

View File

@@ -0,0 +1,280 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*tslint:disable: ban*/
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { SidenavLayoutComponent } from './sidenav-layout.component';
import { Component, Input } from '@angular/core';
import { LayoutModule, MediaMatcher } from '@angular/cdk/layout';
import { PlatformModule } from '@angular/cdk/platform';
import { MaterialModule } from '../../../material.module';
import { SidenavLayoutContentDirective } from '../../directives/sidenav-layout-content.directive';
import { SidenavLayoutHeaderDirective } from '../../directives/sidenav-layout-header.directive';
import { SidenavLayoutNavigationDirective } from '../../directives/sidenav-layout-navigation.directive';
import { CommonModule } from '@angular/common';
@Component({
selector: 'adf-layout-container',
template: `
<ng-content select="[app-layout-navigation]"></ng-content>
<ng-content select="[app-layout-content]"></ng-content>`
})
export class DummyLayoutContainerComponent {
@Input() sidenavMin: number;
@Input() sidenavMax: number;
@Input() mediaQueryList: MediaQueryList;
@Input() hideSidenav: boolean;
@Input() expandedSidenav: boolean;
toggleMenu () {}
}
describe('SidenavLayoutComponent', () => {
let fixture: ComponentFixture<any>,
mediaMatcher: MediaMatcher,
mediaQueryList: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
PlatformModule,
LayoutModule,
MaterialModule
],
declarations: [
DummyLayoutContainerComponent,
SidenavLayoutComponent,
SidenavLayoutContentDirective,
SidenavLayoutHeaderDirective,
SidenavLayoutNavigationDirective
],
providers: [
MediaMatcher
]
});
}));
beforeEach(() => {
mediaQueryList = {
matches: false,
addListener: () => {},
removeListener: () => {}
};
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('Template transclusion', () => {
@Component({
selector: 'adf-test-component-for-sidenav',
template: `
<adf-sidenav-layout [sidenavMin]="70" [sidenavMax]="320" [stepOver]="600" [hideSidenav]="false">
<adf-sidenav-layout-header>
<ng-template let-toggleMenu="toggleMenu">
<div id="header-test" (click)="toggleMenu()"></div>
</ng-template>
</adf-sidenav-layout-header>
<adf-sidenav-layout-navigation>
<ng-template let-isMenuMinimized="isMenuMinimized">
<div id="nav-test">{{ isMenuMinimized !== undefined ? 'variable-is-injected' : 'variable-is-not-injected' }}</div>
</ng-template>
</adf-sidenav-layout-navigation>
<adf-sidenav-layout-content>
<ng-template>
<div id="content-test"></div>
</ng-template>
</adf-sidenav-layout-content>
</adf-sidenav-layout>`
})
class SidenavLayoutTesterComponent {}
beforeEach(async(() => {
TestBed.configureTestingModule({ declarations: [ SidenavLayoutTesterComponent ] }).compileComponents();
}));
beforeEach(() => {
mediaMatcher = TestBed.get(MediaMatcher);
spyOn(mediaMatcher, 'matchMedia').and.returnValue(mediaQueryList);
fixture = TestBed.createComponent(SidenavLayoutTesterComponent);
fixture.detectChanges();
});
describe('adf-sidenav-layout-navigation', () => {
const injectedElementSelector = By.css('[data-automation-id="adf-layout-container"] #nav-test');
it('should contain the transcluded side navigation template', () => {
const injectedElement = fixture.debugElement.query(injectedElementSelector);
expect(injectedElement === null).toBe(false);
});
it('should let the isMenuMinimized property of component to be accessed by the transcluded template', () => {
const injectedElement = fixture.debugElement.query(injectedElementSelector);
expect(injectedElement.nativeElement.innerText.trim()).toBe('variable-is-injected');
});
});
describe('adf-sidenav-layout-header', () => {
const outerHeaderSelector = By.css('.sidenav-layout > #header-test'),
innerHeaderSelector = By.css('.sidenav-layout [data-automation-id="adf-layout-container"] #header-test');
it('should contain the transcluded header template outside of the layout-container', () => {
mediaQueryList.matches = false;
fixture.detectChanges();
const outerHeaderElement = fixture.debugElement.query(outerHeaderSelector);
const innerHeaderElement = fixture.debugElement.query(innerHeaderSelector);
expect(outerHeaderElement === null).toBe(false, 'Outer header should be shown');
expect(innerHeaderElement === null).toBe(true, 'Inner header should not be shown');
});
it('should contain the transcluded header template inside of the layout-container', () => {
mediaQueryList.matches = true;
fixture.detectChanges();
const outerHeaderElement = fixture.debugElement.query(outerHeaderSelector);
const innerHeaderElement = fixture.debugElement.query(innerHeaderSelector);
expect(outerHeaderElement === null).toBe(true, 'Outer header should not be shown');
expect(innerHeaderElement === null).toBe(false, 'Inner header should be shown');
});
it('should call through the layout container\'s toggleMenu method', () => {
mediaQueryList.matches = false;
fixture.detectChanges();
const layoutContainerComponent = fixture.debugElement.query(By.directive(DummyLayoutContainerComponent)).componentInstance;
spyOn(layoutContainerComponent, 'toggleMenu');
const outerHeaderElement = fixture.debugElement.query(outerHeaderSelector);
outerHeaderElement.triggerEventHandler('click', {});
expect(layoutContainerComponent.toggleMenu).toHaveBeenCalled();
});
});
describe('adf-sidenav-layout-content', () => {
const injectedElementSelector = By.css('[data-automation-id="adf-layout-container"] #content-test');
it('should contain the transcluded content template', () => {
const injectedElement = fixture.debugElement.query(injectedElementSelector);
expect(injectedElement === null).toBe(false);
});
});
});
describe('General behaviour', () => {
let component: SidenavLayoutComponent;
beforeEach(async(() => {
TestBed.compileComponents();
}));
beforeEach(() => {
mediaMatcher = TestBed.get(MediaMatcher);
spyOn(mediaMatcher, 'matchMedia').and.callFake((mediaQuery) => {
mediaQueryList.originalMediaQueryPassed = mediaQuery;
spyOn(mediaQueryList, 'addListener').and.stub();
spyOn(mediaQueryList, 'removeListener').and.stub();
return mediaQueryList;
});
fixture = TestBed.createComponent(SidenavLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should pass through input paramters', () => {
component.sidenavMin = 1;
component.sidenavMax = 2;
component.hideSidenav = true;
component.expandedSidenav = false;
fixture.detectChanges();
const layoutContainerComponent = fixture.debugElement.query(By.directive(DummyLayoutContainerComponent)).componentInstance;
expect(layoutContainerComponent.sidenavMin).toBe(component.sidenavMin);
expect(layoutContainerComponent.sidenavMax).toBe(component.sidenavMax);
expect(layoutContainerComponent.hideSidenav).toBe(component.hideSidenav);
expect(layoutContainerComponent.expandedSidenav).toBe(component.expandedSidenav);
expect(layoutContainerComponent.mediaQueryList.originalMediaQueryPassed).toBe(`(max-width: 600px)`);
});
it('addListener of mediaQueryList should have been called', () => {
expect(mediaQueryList.addListener).toHaveBeenCalledTimes(1);
expect(mediaQueryList.addListener).toHaveBeenCalledWith(component.onMediaQueryChange);
});
it('addListener of mediaQueryList should have been called', () => {
fixture.destroy();
expect(mediaQueryList.removeListener).toHaveBeenCalledTimes(1);
expect(mediaQueryList.removeListener).toHaveBeenCalledWith(component.onMediaQueryChange);
});
});
describe('toggleMenu', () => {
let component;
beforeEach(async(() => {
TestBed.compileComponents();
}));
beforeEach(() => {
mediaMatcher = TestBed.get(MediaMatcher);
spyOn(mediaMatcher, 'matchMedia').and.returnValue(mediaQueryList);
fixture = TestBed.createComponent(SidenavLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should toggle the isMenuMinimized if the mediaQueryList.matches is false (we are on desktop)', () => {
mediaQueryList.matches = false;
component.isMenuMinimized = false;
component.toggleMenu();
expect(component.isMenuMinimized).toBe(true);
});
it('should set the isMenuMinimized to false if the mediaQueryList.matches is true (we are on mobile)', () => {
mediaQueryList.matches = true;
component.isMenuMinimized = true;
component.toggleMenu();
expect(component.isMenuMinimized).toBe(false);
});
});
});

View File

@@ -0,0 +1,101 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, ContentChild, Input, OnInit, AfterViewInit, ViewChild, OnDestroy, TemplateRef } from '@angular/core';
import { MediaMatcher } from '@angular/cdk/layout';
import { SidenavLayoutContentDirective } from '../../directives/sidenav-layout-content.directive';
import { SidenavLayoutHeaderDirective } from '../../directives/sidenav-layout-header.directive';
import { SidenavLayoutNavigationDirective } from '../../directives/sidenav-layout-navigation.directive';
@Component({
selector: 'adf-sidenav-layout',
templateUrl: './sidenav-layout.component.html',
styleUrls: ['./sidenav-layout.component.scss']
})
export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy {
static STEP_OVER = 600;
@Input() sidenavMin: number;
@Input() sidenavMax: number;
@Input() stepOver: number;
@Input() hideSidenav = false;
@Input() expandedSidenav = true;
@ContentChild(SidenavLayoutHeaderDirective) headerDirective: SidenavLayoutHeaderDirective;
@ContentChild(SidenavLayoutNavigationDirective) navigationDirective: SidenavLayoutNavigationDirective;
@ContentChild(SidenavLayoutContentDirective) contentDirective: SidenavLayoutContentDirective;
@ViewChild('container') container: any;
@ViewChild('emptyTemplate') emptyTemplate: any;
mediaQueryList: MediaQueryList;
isMenuMinimized;
templateContext = {
toggleMenu: () => {},
isMenuMinimized: () => this.isMenuMinimized
};
constructor(private mediaMatcher: MediaMatcher) {
this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
}
ngOnInit() {
const stepOver = this.stepOver || SidenavLayoutComponent.STEP_OVER;
this.isMenuMinimized = !this.expandedSidenav;
this.mediaQueryList = this.mediaMatcher.matchMedia(`(max-width: ${stepOver}px)`);
this.mediaQueryList.addListener(this.onMediaQueryChange);
}
ngAfterViewInit() {
this.templateContext.toggleMenu = this.toggleMenu.bind(this);
}
ngOnDestroy(): void {
this.mediaQueryList.removeListener(this.onMediaQueryChange);
}
toggleMenu() {
if (!this.mediaQueryList.matches) {
this.isMenuMinimized = !this.isMenuMinimized;
} else {
this.isMenuMinimized = false;
}
this.container.toggleMenu();
}
get isHeaderInside() {
return this.mediaQueryList.matches;
}
get headerTemplate(): TemplateRef<any> {
return this.headerDirective && this.headerDirective.template || this.emptyTemplate;
}
get navigationTemplate(): TemplateRef<any> {
return this.navigationDirective && this.navigationDirective.template || this.emptyTemplate;
}
get contentTemplate(): TemplateRef<any> {
return this.contentDirective && this.contentDirective.template || this.emptyTemplate;
}
onMediaQueryChange() {
this.isMenuMinimized = false;
}
}

View File

@@ -0,0 +1,26 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ContentChild, Directive, TemplateRef } from '@angular/core';
@Directive({
selector: 'adf-sidenav-layout-content'
})
export class SidenavLayoutContentDirective {
@ContentChild(TemplateRef)
public template: TemplateRef<any>;
}

View File

@@ -0,0 +1,26 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ContentChild, Directive, TemplateRef } from '@angular/core';
@Directive({
selector: 'adf-sidenav-layout-header'
})
export class SidenavLayoutHeaderDirective {
@ContentChild(TemplateRef)
public template: TemplateRef<any>;
}

View File

@@ -0,0 +1,26 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ContentChild, Directive, TemplateRef } from '@angular/core';
@Directive({
selector: 'adf-sidenav-layout-navigation'
})
export class SidenavLayoutNavigationDirective {
@ContentChild(TemplateRef)
public template: TemplateRef<any>;
}

View File

@@ -0,0 +1,30 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { trigger, transition, animate, style, state, AnimationTriggerMetadata } from '@angular/animations';
export const sidenavAnimation: AnimationTriggerMetadata = trigger('sidenavAnimation', [
state('expanded', style({ width: '{{ width }}px' }), { params : { width: 0 } }),
state('compact', style({ width: '{{ width }}px' }), { params : { width: 0 } }),
transition('compact <=> expanded', animate('0.4s cubic-bezier(0.25, 0.8, 0.25, 1)'))
]);
export const contentAnimation: AnimationTriggerMetadata = trigger('contentAnimation', [
state('expanded', style({ 'margin-left': '{{ marginLeft }}px' }), { params : { marginLeft: 0 } }),
state('compact', style({'margin-left': '{{ marginLeft }}px' }), { params : { marginLeft: 0 } }),
transition('expanded <=> compact', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)'))
]);

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './public-api';

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './components/sidenav-layout/sidenav-layout.component';

View File

@@ -0,0 +1,47 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MaterialModule } from '../material.module';
import { SidenavLayoutContentDirective } from './directives/sidenav-layout-content.directive';
import { SidenavLayoutHeaderDirective } from './directives/sidenav-layout-header.directive';
import { SidenavLayoutNavigationDirective } from './directives/sidenav-layout-navigation.directive';
import { SidenavLayoutComponent } from '.';
import { LayoutContainerComponent } from './components/layout-container/layout-container.component';
@NgModule({
imports: [
CommonModule,
MaterialModule
],
exports: [
SidenavLayoutHeaderDirective,
SidenavLayoutContentDirective,
SidenavLayoutNavigationDirective,
SidenavLayoutComponent,
LayoutContainerComponent
],
declarations: [
SidenavLayoutHeaderDirective,
SidenavLayoutContentDirective,
SidenavLayoutNavigationDirective,
SidenavLayoutComponent,
LayoutContainerComponent
]
})
export class SidenavLayoutModule {}