-
Notifications
You must be signed in to change notification settings - Fork 125
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(platform): Menu as a standalone component (#1390)
* 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
Showing
12 changed files
with
1,371 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
91 changes: 91 additions & 0 deletions
91
libs/platform/src/lib/components/menu/menu-component/menu-item.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
106 changes: 106 additions & 0 deletions
106
libs/platform/src/lib/components/menu/menu-component/menu-item.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
151 changes: 151 additions & 0 deletions
151
libs/platform/src/lib/components/menu/menu-component/menu-item.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.