[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,16 +1,17 @@
<button <div [dir]="direction">
<button
style="visibility: hidden" style="visibility: hidden"
[matMenuTriggerFor]="rootMenu" [matMenuTriggerFor]="rootMenu"
#rootTriggerEl #rootTriggerEl
></button> ></button>
<mat-menu <mat-menu
#rootMenu="matMenu" #rootMenu="matMenu"
class="aca-context-menu" class="aca-context-menu"
hasBackdrop="false" hasBackdrop="false"
acaContextMenuOutsideEvent acaContextMenuOutsideEvent
(clickOutside)="onClickOutsideEvent()" (clickOutside)="onClickOutsideEvent()"
> >
<ng-container <ng-container
*ngFor="let entry of actions; trackBy: trackById" *ngFor="let entry of actions; trackBy: trackById"
[ngSwitch]="entry.type" [ngSwitch]="entry.type"
@ -37,7 +38,9 @@
</button> </button>
<mat-menu #childMenu="matMenu"> <mat-menu #childMenu="matMenu">
<ng-container *ngFor="let child of entry.children; trackBy: trackById"> <ng-container
*ngFor="let child of entry.children; trackBy: trackById"
>
<app-context-menu-item [actionRef]="child"></app-context-menu-item> <app-context-menu-item [actionRef]="child"></app-context-menu-item>
</ng-container> </ng-container>
</mat-menu> </mat-menu>
@ -47,4 +50,5 @@
<adf-dynamic-component [id]="entry.component"></adf-dynamic-component> <adf-dynamic-component [id]="entry.component"></adf-dynamic-component>
</ng-container> </ng-container>
</ng-container> </ng-container>
</mat-menu> </mat-menu>
</div>

View File

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

View File

@ -31,36 +31,85 @@ import { of } from 'rxjs';
import { CoreModule } from '@alfresco/adf-core'; import { CoreModule } from '@alfresco/adf-core';
import { ContextMenuService } from './context-menu.service'; import { ContextMenuService } from './context-menu.service';
import { ContextMenuModule } from './context-menu.module'; import { ContextMenuModule } from './context-menu.module';
import { UserPreferencesService } from '@alfresco/adf-core';
describe('ContextMenuService', () => { describe('ContextMenuService', () => {
let contextMenuService; let contextMenuService;
let overlay;
let injector;
let userPreferencesService;
const overlayConfig = { const overlayConfig = {
hasBackdrop: false, hasBackdrop: false,
backdropClass: '', backdropClass: '',
panelClass: 'test-panel' panelClass: 'test-panel',
source: {
x: 1,
y: 1
}
}; };
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CoreModule.forRoot(), ContextMenuModule], imports: [CoreModule.forRoot(), ContextMenuModule],
providers: [Overlay, { provide: Store, useValue: { select: () => of() } }] providers: [
Overlay,
{ provide: Store, useValue: { select: () => of() } },
UserPreferencesService
]
}); });
const injector = TestBed.get(Injector); injector = TestBed.get(Injector);
const overlay = TestBed.get(Overlay); overlay = TestBed.get(Overlay);
userPreferencesService = TestBed.get(UserPreferencesService);
contextMenuService = new ContextMenuService(injector, overlay);
}); });
it('should create a custom overlay', () => { it('should create a custom overlay', () => {
contextMenuService = new ContextMenuService(
injector,
overlay,
userPreferencesService
);
contextMenuService.open(overlayConfig); contextMenuService.open(overlayConfig);
expect(document.querySelector('.test-panel')).not.toBe(null); expect(document.querySelector('.test-panel')).not.toBe(null);
}); });
it('should render component', () => { it('should render component', () => {
contextMenuService = new ContextMenuService(
injector,
overlay,
userPreferencesService
);
contextMenuService.open(overlayConfig); contextMenuService.open(overlayConfig);
expect(document.querySelector('aca-context-menu')).not.toBe(null); 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 { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ContextMenuOverlayRef } from './context-menu-overlay'; import { ContextMenuOverlayRef } from './context-menu-overlay';
import { ContextMenuComponent } from './context-menu.component'; import { ContextMenuComponent } from './context-menu.component';
import { ContextmenuOverlayConfig } from './interfaces'; import { ContextmenuOverlayConfig } from './interfaces';
import { UserPreferencesService } from '@alfresco/adf-core';
import { Directionality } from '@angular/cdk/bidi';
import { CONTEXT_MENU_DIRECTION } from './direction.token';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ContextMenuService { 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) { open(config: ContextmenuOverlayConfig) {
const overlay = this.createOverlay(config); const overlay = this.createOverlay(config);
@ -49,56 +89,33 @@ export class ContextMenuService {
const injectionTokens = new WeakMap(); const injectionTokens = new WeakMap();
injectionTokens.set(ContextMenuOverlayRef, contextmenuOverlayRef); injectionTokens.set(ContextMenuOverlayRef, contextmenuOverlayRef);
injectionTokens.set(CONTEXT_MENU_DIRECTION, this.direction);
return new PortalInjector(this.injector, injectionTokens); return new PortalInjector(this.injector, injectionTokens);
} }
private getOverlayConfig(config: ContextmenuOverlayConfig): OverlayConfig { private getOverlayConfig(config: ContextmenuOverlayConfig): OverlayConfig {
const fakeElement: any = { const { x, y } = config.source;
getBoundingClientRect: (): ClientRect => ({
bottom: config.source.clientY,
height: 0,
left: config.source.clientX,
right: config.source.clientX,
top: config.source.clientY,
width: 0
})
};
const positionStrategy = this.overlay const positionStrategy = this.overlay
.position() .position()
.connectedTo( .flexibleConnectedTo({ x, y })
new ElementRef(fakeElement), .withPositions([
{ originX: 'start', originY: 'bottom' }, {
{ overlayX: 'start', overlayY: 'top' } originX: 'end',
) originY: 'bottom',
.withFallbackPosition( overlayX: 'end',
{ originX: 'start', originY: 'top' }, overlayY: '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' }
);
const overlayConfig = new OverlayConfig({ const overlayConfig = new OverlayConfig({
hasBackdrop: config.hasBackdrop, hasBackdrop: config.hasBackdrop,
backdropClass: config.backdropClass, backdropClass: config.backdropClass,
panelClass: config.panelClass, panelClass: config.panelClass,
scrollStrategy: this.overlay.scrollStrategies.close(), scrollStrategy: this.overlay.scrollStrategies.close(),
positionStrategy positionStrategy,
direction: this.direction
}); });
return overlayConfig; 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'
}
);