[ACA-2375] Contextmenu - RTL support (#1099)

* direction element wrapper

* use CONTEXT_MENU_DIRECTION injection

* direction injection token

* refactor positionStrategy

* set CONTEXT_MENU_DIRECTION  value

* test
This commit is contained in:
Cilibiu Bogdan 2019-05-08 18:22:03 +03:00 committed by Denys Vuika
parent 7e4b635154
commit 996975fdb5
5 changed files with 197 additions and 90 deletions

View File

@ -1,50 +1,54 @@
<button
style="visibility: hidden"
[matMenuTriggerFor]="rootMenu"
#rootTriggerEl
></button>
<div [dir]="direction">
<button
style="visibility: hidden"
[matMenuTriggerFor]="rootMenu"
#rootTriggerEl
></button>
<mat-menu
#rootMenu="matMenu"
class="aca-context-menu"
hasBackdrop="false"
acaContextMenuOutsideEvent
(clickOutside)="onClickOutsideEvent()"
>
<ng-container
*ngFor="let entry of actions; trackBy: trackById"
[ngSwitch]="entry.type"
<mat-menu
#rootMenu="matMenu"
class="aca-context-menu"
hasBackdrop="false"
acaContextMenuOutsideEvent
(clickOutside)="onClickOutsideEvent()"
>
<ng-container *ngSwitchDefault>
<button
mat-menu-item
[id]="entry.id"
(click)="runAction(entry.actions.click)"
>
<adf-icon [value]="entry.icon"></adf-icon>
<span>{{ entry.title | translate }}</span>
</button>
</ng-container>
<ng-container
*ngFor="let entry of actions; trackBy: trackById"
[ngSwitch]="entry.type"
>
<ng-container *ngSwitchDefault>
<button
mat-menu-item
[id]="entry.id"
(click)="runAction(entry.actions.click)"
>
<adf-icon [value]="entry.icon"></adf-icon>
<span>{{ entry.title | translate }}</span>
</button>
</ng-container>
<ng-container *ngSwitchCase="'separator'">
<mat-divider></mat-divider>
</ng-container>
<ng-container *ngSwitchCase="'separator'">
<mat-divider></mat-divider>
</ng-container>
<ng-container *ngSwitchCase="'menu'">
<button mat-menu-item [id]="entry.id" [matMenuTriggerFor]="childMenu">
<adf-icon [value]="entry.icon"></adf-icon>
<span>{{ entry.title | translate }}</span>
</button>
<ng-container *ngSwitchCase="'menu'">
<button mat-menu-item [id]="entry.id" [matMenuTriggerFor]="childMenu">
<adf-icon [value]="entry.icon"></adf-icon>
<span>{{ entry.title | translate }}</span>
</button>
<mat-menu #childMenu="matMenu">
<ng-container *ngFor="let child of entry.children; trackBy: trackById">
<app-context-menu-item [actionRef]="child"></app-context-menu-item>
</ng-container>
</mat-menu>
</ng-container>
<mat-menu #childMenu="matMenu">
<ng-container
*ngFor="let child of entry.children; trackBy: trackById"
>
<app-context-menu-item [actionRef]="child"></app-context-menu-item>
</ng-container>
</mat-menu>
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<adf-dynamic-component [id]="entry.component"></adf-dynamic-component>
<ng-container *ngSwitchCase="'custom'">
<adf-dynamic-component [id]="entry.component"></adf-dynamic-component>
</ng-container>
</ng-container>
</ng-container>
</mat-menu>
</mat-menu>
</div>

View File

@ -30,10 +30,10 @@ import {
OnDestroy,
HostListener,
ViewChild,
AfterViewInit
AfterViewInit,
Inject
} from '@angular/core';
import { MatMenuTrigger } from '@angular/material/menu';
import { AppExtensionService } from '../../extensions/extension.service';
import { AppStore, getAppSelection } from '@alfresco/aca-shared/store';
import { Store } from '@ngrx/store';
@ -41,6 +41,8 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ContentActionRef } from '@alfresco/adf-extensions';
import { ContextMenuOverlayRef } from './context-menu-overlay';
import { CONTEXT_MENU_DIRECTION } from './direction.token';
import { Directionality } from '@angular/cdk/bidi';
@Component({
selector: 'aca-context-menu',
@ -70,7 +72,8 @@ export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit {
constructor(
private contextMenuOverlayRef: ContextMenuOverlayRef,
private extensions: AppExtensionService,
private store: Store<AppStore>
private store: Store<AppStore>,
@Inject(CONTEXT_MENU_DIRECTION) public direction: Directionality
) {}
onClickOutsideEvent() {

View File

@ -31,36 +31,85 @@ import { of } from 'rxjs';
import { CoreModule } from '@alfresco/adf-core';
import { ContextMenuService } from './context-menu.service';
import { ContextMenuModule } from './context-menu.module';
import { UserPreferencesService } from '@alfresco/adf-core';
describe('ContextMenuService', () => {
let contextMenuService;
let overlay;
let injector;
let userPreferencesService;
const overlayConfig = {
hasBackdrop: false,
backdropClass: '',
panelClass: 'test-panel'
panelClass: 'test-panel',
source: {
x: 1,
y: 1
}
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreModule.forRoot(), ContextMenuModule],
providers: [Overlay, { provide: Store, useValue: { select: () => of() } }]
providers: [
Overlay,
{ provide: Store, useValue: { select: () => of() } },
UserPreferencesService
]
});
const injector = TestBed.get(Injector);
const overlay = TestBed.get(Overlay);
contextMenuService = new ContextMenuService(injector, overlay);
injector = TestBed.get(Injector);
overlay = TestBed.get(Overlay);
userPreferencesService = TestBed.get(UserPreferencesService);
});
it('should create a custom overlay', () => {
contextMenuService = new ContextMenuService(
injector,
overlay,
userPreferencesService
);
contextMenuService.open(overlayConfig);
expect(document.querySelector('.test-panel')).not.toBe(null);
});
it('should render component', () => {
contextMenuService = new ContextMenuService(
injector,
overlay,
userPreferencesService
);
contextMenuService.open(overlayConfig);
expect(document.querySelector('aca-context-menu')).not.toBe(null);
});
it('should have default LTR direction value', () => {
contextMenuService = new ContextMenuService(
injector,
overlay,
userPreferencesService
);
contextMenuService.open(overlayConfig);
expect(document.body.querySelector('div[dir="ltr"]')).not.toBe(null);
});
it('should change direction on textOrientation event', () => {
spyOn(userPreferencesService, 'select').and.returnValue(of('rtl'));
contextMenuService = new ContextMenuService(
injector,
overlay,
userPreferencesService
);
contextMenuService.open(overlayConfig);
expect(document.body.querySelector('div[dir="rtl"]')).not.toBe(null);
});
});

View File

@ -1,15 +1,55 @@
import { Injectable, Injector, ComponentRef, ElementRef } from '@angular/core';
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Injectable, Injector, ComponentRef } from '@angular/core';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ContextMenuOverlayRef } from './context-menu-overlay';
import { ContextMenuComponent } from './context-menu.component';
import { ContextmenuOverlayConfig } from './interfaces';
import { UserPreferencesService } from '@alfresco/adf-core';
import { Directionality } from '@angular/cdk/bidi';
import { CONTEXT_MENU_DIRECTION } from './direction.token';
@Injectable({
providedIn: 'root'
})
export class ContextMenuService {
constructor(private injector: Injector, private overlay: Overlay) {}
private direction: Directionality;
constructor(
private injector: Injector,
private overlay: Overlay,
private userPreferenceService: UserPreferencesService
) {
this.userPreferenceService
.select('textOrientation')
.subscribe(textOrientation => {
this.direction = textOrientation;
});
}
open(config: ContextmenuOverlayConfig) {
const overlay = this.createOverlay(config);
@ -49,56 +89,33 @@ export class ContextMenuService {
const injectionTokens = new WeakMap();
injectionTokens.set(ContextMenuOverlayRef, contextmenuOverlayRef);
injectionTokens.set(CONTEXT_MENU_DIRECTION, this.direction);
return new PortalInjector(this.injector, injectionTokens);
}
private getOverlayConfig(config: ContextmenuOverlayConfig): OverlayConfig {
const fakeElement: any = {
getBoundingClientRect: (): ClientRect => ({
bottom: config.source.clientY,
height: 0,
left: config.source.clientX,
right: config.source.clientX,
top: config.source.clientY,
width: 0
})
};
const { x, y } = config.source;
const positionStrategy = this.overlay
.position()
.connectedTo(
new ElementRef(fakeElement),
{ originX: 'start', originY: 'bottom' },
{ overlayX: 'start', overlayY: 'top' }
)
.withFallbackPosition(
{ originX: 'start', originY: 'top' },
{ overlayX: 'start', overlayY: 'bottom' }
)
.withFallbackPosition(
{ originX: 'end', originY: 'top' },
{ overlayX: 'start', overlayY: 'top' }
)
.withFallbackPosition(
{ originX: 'start', originY: 'top' },
{ overlayX: 'end', overlayY: 'top' }
)
.withFallbackPosition(
{ originX: 'end', originY: 'center' },
{ overlayX: 'start', overlayY: 'center' }
)
.withFallbackPosition(
{ originX: 'start', originY: 'center' },
{ overlayX: 'end', overlayY: 'center' }
);
.flexibleConnectedTo({ x, y })
.withPositions([
{
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
}
]);
const overlayConfig = new OverlayConfig({
hasBackdrop: config.hasBackdrop,
backdropClass: config.backdropClass,
panelClass: config.panelClass,
scrollStrategy: this.overlay.scrollStrategies.close(),
positionStrategy
positionStrategy,
direction: this.direction
});
return overlayConfig;

View File

@ -0,0 +1,34 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2019 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { InjectionToken } from '@angular/core';
export const CONTEXT_MENU_DIRECTION = new InjectionToken(
'CONTEXT_MENU_DIRECTION',
{
providedIn: 'root',
factory: () => 'ltr'
}
);