ADF-2974] New Buttons Menu component version (#3429)

* [ADF-2974] Buttons injected from parent html component

* [ADF-2974] New version of buttons menu component

* [ADF-2974] Updated unit tests

* [ADF-2974] Updated documentation

* [ADF-2974] Removed unused variable

* [ADF-2974] Fixed failing test at analytics report parameters

* [ADF-2974] Removed fdescribe

* [ADF-2974] Moved mock inside testing file for buttons menu component
This commit is contained in:
davidcanonieto
2018-06-06 23:07:54 +01:00
committed by Eugenio Romano
parent 1838818295
commit 3759a7967c
11 changed files with 226 additions and 236 deletions

View File

@@ -6,76 +6,47 @@ Last reviewed: 2018-04-24
# Buttons Menu Component
Displays buttons on a responsive menu.
![adf-buttons-menu-desktop](../docassets/images/adf-buttons-menu-desktop.png)
Displays buttons on a responsive menu. This way the html doesn't need to
## Basic Usage
In order to use this component, you will have to place the buttons that you want to have in your menu inside this component's html tags.
They must use the following structure:
```html
<adf-buttons-action-menu
[buttons]="buttons">
<adf-buttons-action-menu>
<button mat-menu-item (click)="showSettings()">
<mat-icon>settings</mat-icon><span>Settings</span>
</button>
<button mat-menu-item (click)="delete()">
<mat-icon>delete</mat-icon><span>Delete</span>
</button>
</adf-buttons-action-menu>
```
Notice that they need an icon and a label for the button inside a span tag. They also make use of the Angular material directive `mat-menu-item`.
```html
<button mat-menu-item (click)="event()">
<mat-icon> icon </mat-icon>
<span> label </span>
</button>
```
## Class members
### Properties
| Name | Type | Default value | Description |
| -- | -- | -- | -- |
| buttons | [`MenuButton`](../../lib/core/buttons-menu/menu-button.model.ts)`[]` | | Array of buttons that defines the menu. |
| Name | Type | Description |
| -- | -- | -- |
| isMenuEmpty | boolean | If the menu has no buttons it won't be displayed. |
## Details
This component shows buttons on a responsive menu. The display of the menu changes to fit
the screen size of the device:
Desktop view of the menu
This component is fully responsive and it will display two different layouts regarding the screen size.
#### Desktop View
![adf-buttons-menu-desktop](../docassets/images/adf-buttons-menu-desktop.png)
Mobile view of the menu
#### Mobile View
![adf-buttons-menu-mobile](../docassets/images/adf-buttons-menu-mobile.png)
The `buttons` property contains an array of [`MenuButton`](../../lib/core/buttons-menu/menu-button.model.ts) instances that define
the label and appearance of each button along with a handler function to
implement its action:
```ts
buttons: MenuButton[] = [];
setButtons() {
this.buttons = [
new MenuButton({
label: 'Settings',
icon: 'settings',
handler: this.settings.bind(this)
}),
new MenuButton({
label: 'Delete',
icon: 'delete',
handler: this.deleteItem.bind(this, this.reportId),
id: 'delete-button'
}),
new MenuButton({
label: 'Export',
icon: 'file_download',
handler: this.exportItem.bind(this),
id: 'export-button',
isVisible: this.isItemValid.bind(this)
}),
new MenuButton({
label: 'Save',
icon: 'save',
handler: this.saveItem.bind(this),
id: 'save-button',
isVisible: this.isItemValid.bind(this)
})
];
```
## See also
- [Menu Button Model](./menu-button.model.md)

View File

@@ -1,27 +1,20 @@
<div fxShow fxHide.xs="true" *ngIf="hasButtons()" id="adf-buttons-menu">
<ng-container *ngFor="let button of buttons">
<ng-container *ngTemplateOutlet="desktopMenu; context: button"></ng-container>
</ng-container>
</div>
<div fxHide fxShow.xs="true" *ngIf="hasButtons()" id="adf-buttons-menu">
<div id="adf-buttons-menu" class="adf-buttons-menu" *ngIf="!isMenuEmpty">
<div *ngIf="isMobile()">
<button mat-icon-button [matMenuTriggerFor]="editReportMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #editReportMenu="matMenu">
<ng-container *ngFor="let button of buttons">
<ng-container *ngTemplateOutlet="mobileMenu; context:button"></ng-container>
</ng-container>
<mat-menu #editReportMenu="matMenu" class="adf-buttons-menu-mobile">
<ng-content *ngTemplateOutlet="desktop">
</ng-content>
</mat-menu>
</div>
<div *ngIf="!isMobile()" class="adf-buttons-menu-desktop">
<ng-content *ngTemplateOutlet="desktop">
</ng-content>
</div>
</div>
<ng-template #desktopMenu let-handler="handler" let-icon="icon" let-label="label" let-styles="styles" let-id="id" let-isVisible="isVisible">
<button mat-button (click)="handler()" id="{{id}}" *ngIf="isVisible()" class="styles">
<mat-icon>{{icon}}</mat-icon>
</button>
</ng-template>
<ng-template #mobileMenu let-handler="handler" let-icon="icon" let-label="label" let-styles="styles" let-id="id" let-isVisible="isVisible">
<button mat-menu-item (click)="handler()" id="{{id}}" *ngIf="isVisible()" class="styles">
<mat-icon>{{icon}}</mat-icon><span>{{label | translate}}</span>
</button>
<ng-template #desktop>
<ng-content></ng-content>
</ng-template>

View File

@@ -0,0 +1,32 @@
@mixin adf-buttons-menu-theme($theme) {
.adf-buttons-menu {
margin-right: 10px;
& div {
display: flex;
}
&-mobile {
margin-right: 10px;
}
&-desktop {
display: flex;
button {
color: black;
padding: 0;
}
button > span {
display: none;
}
button > mat-icon.mat-icon.material-icons {
color: black;
margin: 0 10px;
}
}
}
}

View File

@@ -16,29 +16,65 @@
*/
import { TestBed, async } from '@angular/core/testing';
import { ButtonsMenuComponent } from './buttons-menu.component';
import { MenuButton } from './menu-button.model';
import { MaterialModule } from '../material.module';
import { CoreTestingModule } from '../testing/core.testing.module';
import { setupTestBed } from '../testing/setupTestBed';
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';
@Component({
selector: 'adf-custom-container',
template: `
<adf-buttons-action-menu>
<button mat-menu-item (click)="assignValue()">
<mat-icon>settings</mat-icon><span> Button </span>
</button>
</adf-buttons-action-menu>
`
})
export class CustomContainerComponent {
value: number;
assignValue() {
this.value = 1;
}
}
@Component({
selector: 'adf-custom-empty-container',
template: `
<adf-buttons-action-menu>
</adf-buttons-action-menu>
`
})
export class CustomEmptyContainerComponent {
}
describe('ButtonsMenuComponent', () => {
describe('When Buttons are injected', () => {
let fixture;
let buttonsMenuComponent: ButtonsMenuComponent;
let component: CustomContainerComponent;
let element: HTMLElement;
setupTestBed({
imports: [
CoreTestingModule,
MaterialModule
],
declarations: [
CustomContainerComponent
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
});
beforeEach(() => {
fixture = TestBed.createComponent(ButtonsMenuComponent);
element = fixture.nativeElement;
buttonsMenuComponent = <ButtonsMenuComponent> fixture.debugElement.componentInstance;
fixture = TestBed.createComponent(CustomContainerComponent);
element = fixture.debugElement.nativeElement;
component = fixture.componentInstance;
});
afterEach(() => {
@@ -46,46 +82,59 @@ describe('ButtonsMenuComponent', () => {
TestBed.resetTestingModule();
});
it('should hide buttons menu div if buttons input is empty', async(() => {
buttonsMenuComponent.buttons = [];
it('should render buttons menu when at least one button is declared', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const buttonsMenuElement = element.querySelector('#adf-buttons-menu');
expect(buttonsMenuElement).toBeDefined();
});
}));
it('should trigger event when a specific button is clicked', async(() => {
expect(component.value).toBeUndefined();
let button = element.querySelector('button');
button.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(component.value).toBe(1);
});
}));
});
describe('When no buttons are injected', () => {
let fixture;
let element: HTMLElement;
setupTestBed({
imports: [
CoreTestingModule,
MaterialModule
],
declarations: [
CustomEmptyContainerComponent
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
});
beforeEach(() => {
fixture = TestBed.createComponent(CustomEmptyContainerComponent);
element = fixture.nativeElement;
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
it('should hide buttons menu if buttons input is empty', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const buttonsMenuElement = element.querySelector('#adf-buttons-menu');
expect(buttonsMenuElement).toBeNull();
});
}));
});
it('should render buttons menu when there is at least one button declared in the buttons array', async(() => {
const button = new MenuButton({
label: 'button',
icon: 'button',
id: 'clickMe'
});
buttonsMenuComponent.buttons = [button];
fixture.detectChanges();
fixture.whenStable().then(() => {
const buttonsMenuElement = element.querySelector('#adf-buttons-menu');
expect(buttonsMenuElement).not.toBeNull();
expect(buttonsMenuElement).toBeDefined();
});
}));
it('should call the handler function when button is clicked', async(() => {
const button = new MenuButton({
label: 'button',
icon: 'button',
id: 'clickMe'
});
button.handler = jasmine.createSpy('handler');
buttonsMenuComponent.buttons = [button];
fixture.detectChanges();
fixture.whenStable().then(() => {
const buttonsMenuElement: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#clickMe');
expect(buttonsMenuElement).not.toBeNull();
expect(buttonsMenuElement).toBeDefined();
buttonsMenuElement.click();
fixture.detectChanges();
expect(button.handler).toHaveBeenCalled();
});
}));
});

View File

@@ -15,25 +15,30 @@
* limitations under the License.
*/
/* tslint:disable:component-selector no-access-missing-member no-input-rename */
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { MenuButton } from './menu-button.model';
import { Component, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
import { MatMenuItem } from '@angular/material';
@Component({
selector: 'adf-buttons-action-menu',
templateUrl: './buttons-menu.component.html'
templateUrl: './buttons-menu.component.html',
styleUrls: ['./buttons-menu.component.scss']
})
export class ButtonsMenuComponent implements OnChanges {
/** Array of buttons that defines the menu. */
@Input() buttons: MenuButton[];
export class ButtonsMenuComponent implements AfterContentInit {
ngOnChanges(changes: SimpleChanges) {
this.buttons = changes['buttons'].currentValue;
@ContentChildren(MatMenuItem) buttons: QueryList<MatMenuItem>;
isMenuEmpty: boolean;
ngAfterContentInit() {
if (this.buttons.length > 0) {
this.isMenuEmpty = false;
} else {
this.isMenuEmpty = true;
}
}
hasButtons() {
return this.buttons.length > 0 ? true : false;
isMobile(): boolean {
return !!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
}

View File

@@ -1,37 +0,0 @@
/*!
* @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 type VisibiltyFunction = (obj?: any) => boolean;
const defaultValidation = () => true;
export class MenuButton {
label: string;
icon: string;
handler: any;
styles: string;
id: string;
isVisible: VisibiltyFunction;
constructor(obj?: any) {
this.label = obj.label;
this.icon = obj.icon;
this.handler = obj.handler;
this.styles = obj.styles || null;
this.id = obj.id || null;
this.isVisible = obj.isVisible ? obj.isVisible : defaultValidation;
}
}

View File

@@ -16,5 +16,4 @@
*/
export * from './buttons-menu.component';
export * from './menu-button.model';
export * from './buttons-menu.module';

View File

@@ -28,6 +28,7 @@
@import '../sidenav-layout/components/layout-container/layout-container.component';
@import "../templates/empty-content/empty-content.component";
@import "../templates/error-content/error-content.component";
@import "../buttons-menu/buttons-menu.component";
@mixin adf-core-theme($theme) {
@include adf-colors-theme($theme);
@@ -58,6 +59,7 @@
@include adf-layout-container-theme($theme);
@include adf-empty-content-theme($theme);
@include adf-error-content-theme($theme);
@include adf-buttons-menu-theme($theme);
}

View File

@@ -25,8 +25,21 @@
<h4>{{reportParameters.name}}</h4>
</div>
</adf-toolbar-title>
<adf-buttons-action-menu *ngIf="!isEditable"
[buttons]="buttons">
<adf-buttons-action-menu *ngIf="!isEditable">
<button mat-menu-item (click)="toggleParameters()" id="">
<mat-icon>settings</mat-icon><span>{{ 'ANALYTICS.MESSAGES.ICON-SETTING' | translate }}</span>
</button>
<button mat-menu-item (click)="deleteReport(reportId)" id="delete-button">
<mat-icon>delete</mat-icon><span>{{ 'ANALYTICS.MESSAGES.ICON-DELETE' | translate }}</span>
</button>
<div *ngIf="isFormValid()">
<button mat-menu-item (click)="showDialog('Export')" id="export-button">
<mat-icon>file_download</mat-icon><span>{{ 'ANALYTICS.MESSAGES.ICON-EXPORT-CSV' | translate }}</span>
</button>
<button mat-menu-item (click)="showDialog('Save')" id="save-button">
<mat-icon>save</mat-icon><span>{{ 'ANALYTICS.MESSAGES.ICON-SAVE' | translate }}</span>
</button>
</div>
</adf-buttons-action-menu>
</adf-toolbar>
<div *ngFor="let field of reportParameters.definition.parameters"

View File

@@ -561,7 +561,7 @@ describe('AnalyticsReportParametersComponent', () => {
it('Should raise an event for report deleted', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
spyOn(component, 'deleteReport');
let deleteButton = fixture.debugElement.nativeElement.querySelector('#delete-button');
expect(deleteButton).toBeDefined();
expect(deleteButton).not.toBeNull();
@@ -569,11 +569,7 @@ describe('AnalyticsReportParametersComponent', () => {
expect(reportId).not.toBeNull();
});
deleteButton.click();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json'
});
});
expect(component.deleteReport).toHaveBeenCalled();
}));
it('Should hide export button if the form is not valid', async(() => {

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { ContentService, LogService, MenuButton } from '@alfresco/adf-core';
import { ContentService, LogService } from '@alfresco/adf-core';
import {
AfterContentChecked,
Component,
@@ -95,8 +95,6 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
reportName: string;
buttons: MenuButton[] = [];
private dropDownSub;
private reportParamsSub;
private paramOpts;
@@ -138,7 +136,6 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
if (reportId && reportId.currentValue) {
this.reportId = reportId.currentValue;
this.getReportParams(reportId.currentValue);
this.setButtons();
}
let appId = changes['appId'];
@@ -391,34 +388,4 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
isFormValid() {
return this.reportForm && this.reportForm.dirty && this.reportForm.valid;
}
setButtons() {
this.buttons = [
new MenuButton({
label: 'ANALYTICS.MESSAGES.ICON-SETTING',
icon: 'settings',
handler: this.toggleParameters.bind(this)
}),
new MenuButton({
label: 'ANALYTICS.MESSAGES.ICON-DELETE',
icon: 'delete',
handler: this.deleteReport.bind(this, this.reportId),
id: 'delete-button'
}),
new MenuButton({
label: 'ANALYTICS.MESSAGES.ICON-EXPORT-CSV',
icon: 'file_download',
handler: this.showDialog.bind(this, 'Export'),
id: 'export-button',
isVisible: this.isFormValid.bind(this)
}),
new MenuButton({
label: 'ANALYTICS.MESSAGES.ICON-SAVE',
icon: 'save',
handler: this.showDialog.bind(this, 'Save'),
id: 'save-button',
isVisible: this.isFormValid.bind(this)
})
];
}
}