diff --git a/extension.schema.json b/extension.schema.json index cc3e86642..55c6627e1 100644 --- a/extension.schema.json +++ b/extension.schema.json @@ -173,7 +173,7 @@ }, "navBarLinkRef": { "type": "object", - "required": ["id", "icon", "title", "route"], + "required": ["id", "icon", "title"], "properties": { "id": { "description": "Unique identifier", @@ -199,6 +199,12 @@ "description": "Element order", "type": "number" }, + "children": { + "description": "Navigation children items", + "type": "array", + "items": { "$ref": "#/definitions/navBarLinkRef" }, + "minItems": 1 + }, "rules": { "description": "Element rules", "type": "object", @@ -209,7 +215,21 @@ } } } - } + }, + "oneOf": [ + { + "required": ["route"], + "not": { + "required": ["children"] + } + }, + { + "required": ["children"], + "not": { + "required": ["route"] + } + } + ] }, "navBarGroupRef": { "type": "object", diff --git a/src/app/components/sidenav/sidenav.component.html b/src/app/components/sidenav/sidenav.component.html index 101ad1bda..f3fbe8865 100644 --- a/src/app/components/sidenav/sidenav.component.html +++ b/src/app/components/sidenav/sidenav.component.html @@ -1,40 +1,148 @@ <div class="sidenav"> <div class="sidenav__section sidenav__section sidenav_action-menu"> - <app-create-menu [expanded]="showLabel"></app-create-menu> + <app-create-menu [expanded]="showLabel"></app-create-menu> </div> <div *ngFor="let group of groups; trackBy: trackById" - class="sidenav__section sidenav__section--menu" > - <ul class="sidenav-menu"> - <li *ngFor="let item of group.items; trackBy: trackById" - class="sidenav-menu__item" - routerLinkActive - #rla="routerLinkActive" - [attr.title]="item.description | translate"> + class="sidenav__section sidenav__section--menu"> - <button - [id]="item.id" - mat-icon-button - mat-ripple - [routerLink]="item.url" - [color]="rla.isActive ? 'accent': 'primary'" - [attr.aria-label]="item.title | translate" - matRippleColor="primary" - [matRippleTrigger]="rippleTrigger" - [matRippleCentered]="true"> + <div class="sidenav-menu"> + <div *ngFor="let item of group.items; trackBy: trackById" + routerLinkActive + #routerLink="routerLinkActive"> - <mat-icon>{{ item.icon }}</mat-icon> - </button> + <ng-container *ngIf="showLabel"> + <ng-container *ngIf="!item.children"> + <div class="sidenav-menu__item"> + <button [id]="item.id" + mat-icon-button mat-ripple + [routerLink]="item.url" + [color]="routerLink.isActive ? 'accent': 'primary'" + [attr.aria-label]="item.title | translate" + matRippleColor="primary" + [matRippleTrigger]="rippleTrigger" + [matRippleCentered]="true" + [matRippleRadius]="20"> - <span #rippleTrigger - [routerLink]="item.url" - class="menu__item--label" - [hidden]="!showLabel" - [ngClass]="{ - 'menu__item--active': rla.isActive, - 'menu__item--default': !rla.isActive - }">{{ item.title | translate }}</span> - </li> - </ul> + <mat-icon>{{ item.icon }}</mat-icon> + </button> + + <span #rippleTrigger + class="menu__item--label" + [routerLink]="item.url" + [attr.aria-label]="item.title | translate" + [ngClass]="{ + 'menu__item--active': routerLink.isActive, + 'menu__item--default': !routerLink.isActive + }"> + {{ item.title | translate }}</span> + </div> + </ng-container> + + <ng-container *ngIf="item.children && item.children.length"> + <mat-expansion-panel [expanded]="routerLink.isActive" [@.disabled]="true"> + <mat-expansion-panel-header expandedHeight="48px" collapsedHeight="48px" [id]="item.id"> + <mat-panel-title> + <mat-icon [color]="routerLink.isActive? 'accent': 'primary'">{{ item.icon }}</mat-icon> + <span class="menu__item--label" + [ngClass]="{ + 'menu__item--active': routerLink.isActive, + 'menu__item--default': !routerLink.isActive + }"> + {{ item.title | translate }}</span> + </mat-panel-title> + </mat-expansion-panel-header> + + <div *ngFor="let child of item.children; trackBy: trackById" + routerLinkActive #childRouteActive="routerLinkActive" + [attr.title]="child.description | translate"> + + <button [id]="child.id" + mat-icon-button mat-ripple + [routerLink]="child.url" + [color]="childRouteActive.isActive ? 'accent': 'primary'" + [attr.aria-label]="child.title | translate" + matRippleColor="primary" + [matRippleTrigger]="rippleTrigger" + [matRippleCentered]="true" + [matRippleRadius]="20"> + + <mat-icon>{{ child.icon }}</mat-icon> + </button> + + <span #rippleTrigger + [routerLink]="child.url" + class="menu__item--label" + [ngClass]="{ + 'menu__item--active': childRouteActive.isActive, + 'menu__item--default': !childRouteActive.isActive + }"> + {{ child.title | translate }}</span> + </div> + </mat-expansion-panel> + </ng-container> + </ng-container> + + <ng-container *ngIf="!showLabel"> + <ng-container *ngIf="!item.children"> + <div class="sidenav-menu__item"> + <button [id]="item.id" + mat-icon-button [routerLink]="item.url" + [color]="routerLink.isActive ? 'accent': 'primary'" + [attr.aria-label]="item.title | translate"> + + <mat-icon>{{ item.icon }}</mat-icon> + </button> + </div> + </ng-container> + + <ng-container *ngIf="item.children && item.children.length"> + <div class="sidenav-menu__item"> + <button [id]="item.id" + color="accent" + mat-mini-fab + mat-icon-button + disableRipple="true" + matRipple + [matRippleCentered]="true" + matRippleColor="accent" + [matRippleRadius]="24" + #childMenu="matMenuTrigger" + [matMenuTriggerFor]="menu"> + + <mat-icon + [color]="routerLink.isActive|| childMenu.menuOpen? 'accent': 'primary'"> + {{ item.icon }} + </mat-icon> + + <mat-icon + [color]="routerLink.isActive|| childMenu.menuOpen? 'accent': 'primary'"> + more_vert + </mat-icon> + </button> + </div> + + <mat-menu #menu="matMenu"[overlapTrigger]="false"> + <button mat-menu-item + *ngFor="let child of item.children; trackBy: trackById" + routerLinkActive + #menuRouterLink="routerLinkActive" + [routerLink]="child.url" + [id]="child.id"> + + <mat-icon [color]="menuRouterLink.isActive ? 'primary': null" + [attr.color]="menuRouterLink.isActive ? 'primary': null"> + {{ child.icon }} + </mat-icon> + + <span class="mat-button" [ngClass]="{ 'mat-primary': menuRouterLink.isActive }"> + {{ child.title | translate }} + </span> + </button> + </mat-menu> + </ng-container> + </ng-container> + </div> + </div> </div> -</div> + </div> \ No newline at end of file diff --git a/src/app/components/sidenav/sidenav.component.scss b/src/app/components/sidenav/sidenav.component.scss index 7a0b9e0d9..4882b7bfa 100644 --- a/src/app/components/sidenav/sidenav.component.scss +++ b/src/app/components/sidenav/sidenav.component.scss @@ -18,15 +18,13 @@ &__section { padding: 8px 14px; - position: relative; } &-menu { - display: inline-flex; + display: flex; flex-direction: column; padding: 0; margin: 0; - list-style-type: none; } &-menu__item { @@ -48,4 +46,18 @@ .menu__item--label:focus { outline: none; } + + .mat-expansion-panel-header { + padding: 0 8px !important; + } + + .mat-expansion-panel-header-title span { + margin-left: 18px; + } + + .mat-expansion-panel-header-title { + display: flex; + flex-direction: row; + align-items: center; + } } diff --git a/src/app/components/sidenav/sidenav.component.theme.scss b/src/app/components/sidenav/sidenav.component.theme.scss index 7c0541ac7..d3149d383 100644 --- a/src/app/components/sidenav/sidenav.component.theme.scss +++ b/src/app/components/sidenav/sidenav.component.theme.scss @@ -11,6 +11,25 @@ background-color: mat-color($background, background); + .mat-expansion-panel { + background-color: unset; + color: mat-color($primary, 0.87) !important; + } + + .mat-expansion-panel { + box-shadow: none !important; + } + + .mat-expansion-panel:not(.mat-expanded) + .mat-expansion-panel-header:not([aria-disabled='true']):hover { + background: none !important; + } + + .mat-mini-fab { + box-shadow: unset !important; + background-color: unset !important; + } + .adf-sidebar-action-menu-button { background-color: mat-color($accent); } diff --git a/src/app/extensions/extension.service.spec.ts b/src/app/extensions/extension.service.spec.ts index 4734aaa53..fe2330ad4 100644 --- a/src/app/extensions/extension.service.spec.ts +++ b/src/app/extensions/extension.service.spec.ts @@ -618,4 +618,25 @@ describe('AppExtensionService', () => { expect(result[0].id).toBe('1'); expect(result[1].id).toBe('3'); }); + + describe('getApplicationNavigation', () => { + it('should create navigation data', () => { + const navigation = service.getApplicationNavigation([ + { items: [{ route: 'route1' }, { route: 'route2' }] }, + { items: [{ children: [{ route: 'route3' }] }] } + ]); + + expect(navigation).toEqual([ + { + items: [ + { route: 'route1', url: '/route1' }, + { route: 'route2', url: '/route2' } + ] + }, + { + items: [{ children: [{ route: 'route3', url: '/route3' }] }] + } + ]); + }); + }); }); diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 825116a21..ddf5b0f1f 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -185,11 +185,31 @@ export class AppExtensionService implements RuleContext { return { ...group, items: (group.items || []) - .filter(item => { - return this.filterByRules(item); - }) + .filter(item => this.filterByRules(item)) .sort(sortByOrder) .map(item => { + if (item.children && item.children.length > 0) { + item.children = item.children + .filter(child => this.filterByRules(child)) + .sort(sortByOrder) + .map(child => { + const childRouteRef = this.extensions.getRouteById( + child.route + ); + const childUrl = `/${ + childRouteRef ? childRouteRef.path : child.route + }`; + return { + ...child, + url: childUrl + }; + }); + + return { + ...item + }; + } + const routeRef = this.extensions.getRouteById(item.route); const url = `/${routeRef ? routeRef.path : item.route}`; return { @@ -197,6 +217,7 @@ export class AppExtensionService implements RuleContext { url }; }) + .reduce(reduceEmptyMenus, []) }; }); }