Skip to content

Commit

Permalink
feat(platform): Menu as a standalone component (#1390)
Browse files Browse the repository at this point in the history
* Integrating Kevin's af-menu from shellbar to ngx

Removed custom elements,
added some css aligning with spec,
made a MenuModule for using with demo app

* Integrating Kevin's af-menu from shellbar to ngx

Removed custom elements,
added some css aligning with spec,
made a MenuModule for using with demo app

* Fixing some build errors

* Fixing some build errors

* feat: Issue #1264 Development of menu component

- added scrolling and scrollingLimit attributes
- added some CSS to align with spec
- modified existing unit tests
- added optional separator line

* feat: Issue #1264 Development of menu component

- added scrolling and scrollingLimit attributes
- added some CSS to align with spec
- modified existing unit tests
- added optional separator line

* feat(platform): #1264 added support for attributes

such as:
disabled,
tooltip customization,
keyboard accessibility,
var colors to scss,

fixed compiler error issue

* feat(platform): #1264 added support for attributes

such as:
disabled,
tooltip customization,
keyboard accessibility,
var colors to scss,

fixed compiler error issue

* feat(platform): #1264 fixed optional separator css issue

* feat(platform): #1264 fixed optional separator css issue

* feat(platform): #1264 css changes, code reorganized into proper folder

also,
rtl fixes for icon and text
secondary icon css fix
truncation with inline-flex issue resolved

* feat(platform): #1264 css changes, code reorganized into proper folder

also,
rtl fixes for icon and text
secondary icon css fix
truncation with inline-flex issue resolved

* feat(platform): using KeyboardService from core for accessibility

Additionally,
1. scrollbar styling aligned with specs
2. state styling issues fixed
3. added id and childItems to MenuItem interface
4. added/modified unit tests
5. handled secondaryIcon

* feat(platform): using KeyboardService from core for accessibility

Additionally,
1. scrollbar styling aligned with specs
2. state styling issues fixed
3. added id and childItems to MenuItem interface
4. added/modified unit tests
5. handled secondaryIcon

* style(platform): cleaning up code

Removing Highlightable and active classes, renaming customLabel to tooltipLabel

* style(platform): cleaned up code

* fix(platform): addressed review comment changes

-fixed usage of method call in template as suggested
-added hardcoded color values to support IE11
-fixed failing testcases due to ang8 update not detecting @ViewChild properly

* style(platform): removed an incorrect color value from fundamental styles

added MenuModule back into platform.module.ts after it got removed while merging conflicts

* fix(platform):changed name from MenuModule to FdpMenuModule

* fix(platform): reused core directives

also provided fix for handling width in em or px and invalid width values.
  • Loading branch information
kavya-b committed Oct 31, 2019
1 parent 6d35940 commit 99ea344
Show file tree
Hide file tree
Showing 12 changed files with 1,371 additions and 10 deletions.
1 change: 1 addition & 0 deletions libs/platform/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './lib/platform.module';
export * from './lib/components/action-bar/public_api';
export * from './lib/components/menu/public_api';
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<ng-container
*ngTemplateOutlet="
selectable ? selectableItemTemplate : itemTemplate;
context: {
label: label,
i: index,
selected: selected,
item: item,
group: group,
separated: separated,
secondaryIcon: secondaryIcon,
disabled: disabled,
tooltipLabel: tooltipLabel,
itemWidth: itemWidth,
childItems: childItems
}
"
>
</ng-container>
<ng-template #itemTemplate let-label="label" let-i="i">
<li
data-tag="menu-item"
[attr.data-index]="i"
[attr.disabled]="disabled"
[ngClass]="['normal-item-state', disabled ? 'is-disabled' : '']"
>
<div fd-menu-item-addon *ngIf="icon; else noIcon">
<span [ngClass]="[icon]" data-tag="menu-item__icon-before" [attr.title]="icon"> </span>
</div>
<div class="fd-menu__label-truncation">
<span
fd-menu-item
[ngStyle]="{ width: finalItemWidth }"
(click)="onItemClick()"
role="button"
data-tag="menu-item__button"
[attr.title]="tooltipLabel ? tooltipLabel : label"
>
{{ label }}
</span>
</div>
<div class="fd-menu__addon-after" *ngIf="secondaryIcon; else noIcon">
<span
[ngClass]="[secondaryIcon ? secondaryIcon : '']"
data-tag="menu-item__icon-after"
[attr.title]="secondaryIcon"
>
</span>
</div>
</li>
</ng-template>
<ng-template #noIcon><span [ngClass]="{ 'fd-menu__list--separated': separated }"></span></ng-template>
<ng-template #selectableItemTemplate let-label="label" let-i="i" let-selected="selected">
<li
data-tag="menu-item"
[attr.data-index]="i"
[attr.disabled]="disabled"
[ngClass]="[selected ? 'selected-item-state' : 'normal-item-state', disabled ? 'is-disabled' : '']"
[attr.aria-selected]="selected ? 'true' : 'false'"
>
<div fd-menu-item-addon>
<span
*ngIf="selected; else noIcon"
[ngClass]="[icon ? icon : 'sap-icon--accept']"
data-tag="menu-item__icon-before"
[attr.title]="icon"
>
</span>
</div>
<div class="fd-menu__label-truncation">
<span
fd-menu-item
(click)="onItemClick()"
[ngStyle]="{ width: finalItemWidth }"
role="button"
data-tag="menu-item__button"
[attr.title]="tooltipLabel ? tooltipLabel : label"
>
{{ label }}
</span>
</div>
<div class="fd-menu__addon-after" *ngIf="secondaryIcon; else noIcon">
<span
[ngClass]="[secondaryIcon ? secondaryIcon : '']"
data-tag="menu-item__icon-after"
[attr.title]="secondaryIcon"
>
</span>
</div>
</li>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
.fd-menu__item {
&:hover {
//don't apply hover properties on only label item, we need it over entire item
background-color: transparent;
}
text-overflow: ellipsis;
height: 40px; //line=24 px. calc is 40-8-8(padding top/bottom)=24px
overflow: hidden;
background-color: transparent;
padding: 12px 16px;
}

.fd-menu__item[aria-selected='true'] {
background-color: transparent;
}

/*
my calculation is as follows:
1. icon element height = 28px
item height = 40px
therefore, space between element and item = 40-28 = 12px, or 6px top and 6px bottom.
2. icon padding top-bottom = 8+8=16px
therefore, between icon and item, space should be (icon padding top-bottom + space between element and item top-bottom) or (8+6 = 14px).
*/
.fd-menu__addon-before {
//fd-menu__addon-before represents the add-on icon element at the start of the item
width: 28px;
height: 28px;
padding: 8px; //center-aligned icon, therefore 8px
align-items: center;
margin: 6px 4px 6px 8px;
color: #0a6ed1;
color: var(--fd-color-action);
}

.fd-menu--addon-before > .fd-menu__item {
//fd-menu--addon-before represents the container of the menu item
padding-left: 0px;
}

.fd-menu__addon-after {
//fd-menu__addon-after represents the add-on icon element at the end of the item
width: 28px;
height: 28px;
padding: 8px; //center-aligned icon, therefore 8px
margin: 6px 4px 6px 8px;
color: #0a6ed1;
color: var(--fd-color-action);
display: flex;
float: right;
clear: right;
justify-content: center;
align-items: center;
}

[dir='rtl'] .fd-menu__addon-after {
float: left;
clear: left;
}
.fd-menu--addon-after > .fd-menu__item {
padding-right: 36px;
}

.is-disabled,
.is-disabled:hover,
.is-disabled:focus,
.is-disabled:active {
cursor: not-allowed;
text-decoration: none;
pointer-events: none;
.fd-menu__item,
.fd-menu__addon-before,
.fd-menu__addon-after {
color: rgba(81, 85, 89, 0.6);
}
}

//inline-flex cannot be used in conjunction with overflow:hidden. This is a workaround to have both.
.fd-menu__label-truncation {
display: inline-flex;
}

//have the right state for selected item
.selected-item-state {
background-color: #0a6ed112;
background-color: var(--fd-color-background-selected);
&:hover {
.fd-menu__item {
font-weight: bold;
}
background-color: #0a6ed11a;
background-color: var(--fd-color-background-selected-hover);
}
}

//have the right state for normal/unselected items
.normal-item-state {
&:hover {
background-color: #fafafa;
background-color: var(--fd-color-background-hover);
.fd-menu__item {
font-weight: bold;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Component, Input, ViewChild } from '@angular/core';
import { ComponentFixture, async, TestBed } from '@angular/core/testing';
import { MenuItemComponent } from './menu-item.component';
import { By } from '@angular/platform-browser';
import { MenuKeyboardService } from '@fundamental-ngx/core';

@Component({
selector: 'fdp-test-component',
template: `
<div>
<fdp-menu-item
[label]="label"
[index]="index"
[selectable]="selectable"
[selected]="selected"
[icon]="icon"
[itemWidth]="itemWidth"
(itemClick)="onItemClick($event)"
></fdp-menu-item>
</div>
`
})
class TestComponent {
@Input()
public label: string;
@Input()
public index: string;
@Input()
public selectable = false;
@Input()
public selected = false;
@Input()
public icon: string;

@Input()
public itemWidth: string;

@ViewChild(MenuItemComponent, { static: false })
menuItem: MenuItemComponent;

public itemClicked = false;

constructor() {}

onItemClick() {
this.itemClicked = true;
}
}

describe('MenuItemComponent', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TestComponent, MenuItemComponent],
providers: [MenuKeyboardService]
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should display the label', () => {
component.label = 'First Item';
fixture.detectChanges();

let button = fixture.debugElement.query(By.css('[data-tag="menu-item__button"]'));
expect(button.nativeElement.textContent).toBe(' First Item ');

component.label = 'New Item';
fixture.detectChanges();

button = fixture.debugElement.query(By.css('[data-tag="menu-item__button"]'));
expect(button.nativeElement.textContent).toBe(' New Item ');
});

it('should accept an "index" attribute', () => {
component.label = 'New Item';
component.index = '1';
fixture.detectChanges();

let menuItem = fixture.debugElement.query(By.css('[data-tag="menu-item"]'));
expect(menuItem.nativeElement.attributes['data-index'].value).toBe('1');

component.index = '8';
fixture.detectChanges();

menuItem = fixture.debugElement.query(By.css('[data-tag="menu-item"]'));
expect(menuItem.nativeElement.attributes['data-index'].value).toBe('8');
});

it('should invoke callback on click', () => {
component.label = 'New Item';
fixture.detectChanges();

const button = fixture.debugElement.query(By.css('[data-tag="menu-item__button"]'));
button.nativeElement.click();

expect(component.itemClicked).toBeTruthy();
});

it('should use "selectable" template if item is selectable', () => {
component.label = 'Selectable Item';
component.index = '1';
component.selectable = true;
fixture.detectChanges();

const button = fixture.debugElement.query(By.css('[data-tag="menu-item"]'));
expect(button.nativeElement.attributes['aria-selected'].value).toBe('false');
});

it('should allow for icons to be added', () => {
component.icon = 'sap-icon--lightbulb';
component.label = 'Icon Item';
component.index = '1';
fixture.detectChanges();

const icon = fixture.debugElement.query(By.css('[data-tag="menu-item__icon-before"]'));
expect(icon).not.toBeNull();
expect(icon.nativeElement.classList.contains('sap-icon--lightbulb')).toBeTruthy();
});

it('should allow for alternative select icons to be added', () => {
component.icon = 'sap-icon--lightbulb';
component.label = 'Icon Item';
component.index = '1';
component.selectable = true;
component.selected = true;
fixture.detectChanges();

const icon = fixture.debugElement.query(By.css('[data-tag="menu-item__icon-before"]'));
expect(icon).not.toBeNull();
expect(icon.nativeElement.classList.contains('sap-icon--lightbulb')).toBeTruthy();
});

it('should call handleKeyboardEvent on keypress', () => {
component.label = 'New Item';
component.index = '1';
fixture.detectChanges();

spyOn(component.menuItem, 'handleKeyboardEvent');
const event: any = { code: 'ArrowDown', preventDefault: () => {} };

component.menuItem.handleKeyboardEvent(event);
expect(component.menuItem.handleKeyboardEvent).toHaveBeenCalled();
});
});

0 comments on commit 99ea344

Please sign in to comment.