diff --git a/README.md b/README.md index 676073e..6698398 100644 --- a/README.md +++ b/README.md @@ -327,14 +327,14 @@ export class AppModule {} You can use the keyboard to manipulate the context menu. Note: Keyboard navigation should be used in conjunction with `autoFocus`, since key events are only captured when the context menu is focused. -| Key | Action | -|:--------------:|------------------------------------------------| -| ArrowDown | Move to next menu item (wrapping) | -| ArrowUp | Move to previous menu item (wrapping) | -| ArrowRight | Open submenu of current menu item if present | -| ArrowLeft | Close current menu unless already at root menu | -| Enter \| Space | Open submenu or execute current menu item | -| Esc | Close current menu | +| Key | Action | +|:-------------------------------:|------------------------------------------------| +| Arrow Down | Move to next menu item (wrapping) | +| Arrow Up | Move to previous menu item (wrapping) | +| Arrow Right (LTR) \/ Left (RTL) | Open submenu of current menu item if present | +| Arrow Left (LTR) \/ Right (RTL) | Close current menu unless already at root menu | +| Enter \/ Space | Open submenu or execute current menu item | +| Escape | Close current menu | ## Disable Context Menu diff --git a/projects/ngx-contextmenu/src/lib/contextMenu.component.ts b/projects/ngx-contextmenu/src/lib/contextMenu.component.ts index ed623dc..4946b08 100644 --- a/projects/ngx-contextmenu/src/lib/contextMenu.component.ts +++ b/projects/ngx-contextmenu/src/lib/contextMenu.component.ts @@ -1,18 +1,19 @@ +import { Directionality } from '@angular/cdk/bidi'; import { - ChangeDetectorRef, - Component, - ContentChildren, - ElementRef, - EventEmitter, - HostListener, - Inject, - Input, - OnDestroy, - Optional, - Output, - QueryList, - ViewChild, - ViewEncapsulation, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + EventEmitter, + HostListener, + Inject, + Input, + OnDestroy, + Optional, + Output, + QueryList, + ViewChild, + ViewEncapsulation, } from '@angular/core'; import { Subscription } from 'rxjs'; import { first } from 'rxjs/operators'; @@ -75,6 +76,7 @@ export class ContextMenuComponent implements OnDestroy { private _contextMenuService: ContextMenuService, private changeDetector: ChangeDetectorRef, private elementRef: ElementRef, + public directionality: Directionality, @Optional() @Inject(CONTEXT_MENU_OPTIONS) private options: IContextMenuOptions, ) { @@ -102,7 +104,12 @@ export class ContextMenuComponent implements OnDestroy { this.event = event; this.item = item; this.setVisibleMenuItems(); - this._contextMenuService.openContextMenu({ ...menuEvent, menuItems: this.visibleMenuItems, menuClass: this.menuClass }); + this._contextMenuService.openContextMenu({ + ...menuEvent, + menuItems: this.visibleMenuItems, + menuClass: this.menuClass, + directionality: this.directionality, + }); this._contextMenuService.close.asObservable().pipe(first()).subscribe(closeEvent => this.close.emit(closeEvent)); this.open.next(menuEvent); } diff --git a/projects/ngx-contextmenu/src/lib/contextMenu.service.ts b/projects/ngx-contextmenu/src/lib/contextMenu.service.ts index 123accf..c6eddf8 100644 --- a/projects/ngx-contextmenu/src/lib/contextMenu.service.ts +++ b/projects/ngx-contextmenu/src/lib/contextMenu.service.ts @@ -1,3 +1,4 @@ +import { Directionality } from '@angular/cdk/bidi'; import { Overlay, OverlayRef, ScrollStrategyOptions } from '@angular/cdk/overlay'; import { ComponentPortal } from '@angular/cdk/portal'; import { ComponentRef, Injectable, ElementRef } from '@angular/core'; @@ -18,6 +19,7 @@ export interface IContextMenuClickEvent { export interface IContextMenuContext extends IContextMenuClickEvent { menuItems: ContextMenuItemDirective[]; menuClass: string; + directionality: Directionality; } export interface CloseLeafMenuEvent { exceptRootMenu?: boolean; @@ -66,7 +68,7 @@ export class ContextMenuService { ) { } public openContextMenu(context: IContextMenuContext) { - const { anchorElement, event, parentContextMenu } = context; + const { anchorElement, event, parentContextMenu, directionality } = context; if (!parentContextMenu) { const mouseEvent = event as MouseEvent; @@ -103,6 +105,7 @@ export class ContextMenuService { positionStrategy, panelClass: 'ngx-contextmenu', scrollStrategy: this.scrollStrategy.close(), + direction: directionality, })]; this.attachContextMenu(this.overlays[0], context); } else { @@ -124,6 +127,7 @@ export class ContextMenuService { positionStrategy, panelClass: 'ngx-contextmenu', scrollStrategy: this.scrollStrategy.close(), + direction: directionality, }); this.destroySubMenus(parentContextMenu); this.overlays = this.overlays.concat(newOverlay); diff --git a/projects/ngx-contextmenu/src/lib/contextMenuContent.component.ts b/projects/ngx-contextmenu/src/lib/contextMenuContent.component.ts index 6fc62a9..8336881 100644 --- a/projects/ngx-contextmenu/src/lib/contextMenuContent.component.ts +++ b/projects/ngx-contextmenu/src/lib/contextMenuContent.component.ts @@ -1,23 +1,31 @@ -import { CloseLeafMenuEvent, IContextMenuClickEvent } from './contextMenu.service'; +import { ActiveDescendantKeyManager } from '@angular/cdk/a11y'; +import { Directionality } from '@angular/cdk/bidi'; +import { LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, SPACE, ENTER, ESCAPE } from '@angular/cdk/keycodes'; import { OverlayRef } from '@angular/cdk/overlay'; import { - AfterViewInit, - ChangeDetectorRef, - Component, - ElementRef, - Inject, - Input, - Optional, - ViewChild, - ViewChildren, + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Inject, + Input, + OnDestroy, + OnInit, + Optional, + Output, + QueryList, + Renderer, + ViewChild, + ViewChildren, } from '@angular/core'; -import { EventEmitter, OnDestroy, OnInit, Output, QueryList, HostListener } from '@angular/core'; import { Subscription } from 'rxjs'; import { ContextMenuItemDirective } from './contextMenu.item.directive'; import { IContextMenuOptions } from './contextMenu.options'; +import { CloseLeafMenuEvent, IContextMenuClickEvent } from './contextMenu.service'; import { CONTEXT_MENU_OPTIONS } from './contextMenu.tokens'; -import { ActiveDescendantKeyManager } from '@angular/cdk/a11y'; export interface ILinkConfig { click: (item: any, $event?: MouseEvent) => void; @@ -25,8 +33,6 @@ export interface ILinkConfig { html: (item: any) => string; } -const ARROW_LEFT_KEYCODE = 37; - @Component({ selector: 'context-menu-content', styles: [ @@ -38,13 +44,23 @@ const ARROW_LEFT_KEYCODE = 37; line-height: @line-height-base; white-space: nowrap; } - .hasSubMenu:before { + :dir(ltr) .hasSubMenu:before { content: "\u25B6"; float: right; + } + :dir(rtl) .dropdown { + direction: rtl; + } + :dir(rtl) .dropdown-menu { + text-align: right; + } + :dir(rtl) .hasSubMenu:before { + content: "\u25C0"; + float: left; }`, ], template: - ``, }) export class ContextMenuContentComponent implements OnInit, OnDestroy, AfterViewInit { @Input() public menuItems: ContextMenuItemDirective[] = []; @@ -87,11 +102,14 @@ export class ContextMenuContentComponent implements OnInit, OnDestroy, AfterView public useBootstrap4 = false; private _keyManager: ActiveDescendantKeyManager; private subscription: Subscription = new Subscription(); + constructor( private changeDetector: ChangeDetectorRef, private elementRef: ElementRef, @Optional() @Inject(CONTEXT_MENU_OPTIONS) private options: IContextMenuOptions, + public renderer: Renderer, + public directionality: Directionality, ) { if (options) { this.autoFocus = options.autoFocus; @@ -149,48 +167,31 @@ export class ContextMenuContentComponent implements OnInit, OnDestroy, AfterView return link.enabled && !link.enabled(this.item); } - @HostListener('window:keydown.ArrowDown', ['$event']) - @HostListener('window:keydown.ArrowUp', ['$event']) - public onKeyEvent(event: KeyboardEvent): void { - if (!this.isLeaf) { - return; - } - this._keyManager.onKeydown(event); - } - - @HostListener('window:keydown.ArrowRight', ['$event']) - public keyboardOpenSubMenu(event?: KeyboardEvent): void { - if (!this.isLeaf) { - return; - } - this.cancelEvent(event); - const menuItem = this.menuItems[this._keyManager.activeItemIndex]; - if (menuItem) { - this.onOpenSubMenu(menuItem); - } - } - - @HostListener('window:keydown.Enter', ['$event']) - @HostListener('window:keydown.Space', ['$event']) - public keyboardMenuItemSelect(event?: KeyboardEvent): void { - if (!this.isLeaf) { - return; - } - this.cancelEvent(event); - const menuItem = this.menuItems[this._keyManager.activeItemIndex]; - if (menuItem) { - this.onMenuItemSelect(menuItem, event); - } - } - - @HostListener('window:keydown.Escape', ['$event']) - @HostListener('window:keydown.ArrowLeft', ['$event']) - public onCloseLeafMenu(event: KeyboardEvent): void { - if (!this.isLeaf) { - return; + @HostListener('window:keydown', ['$event']) + public onKeydownEvent(event: KeyboardEvent): void { + const openSubMenuArrowKeyCode = this.directionality.value === 'ltr' ? RIGHT_ARROW : LEFT_ARROW; + const closeLeafMenuArrowKeyCode = this.directionality.value === 'ltr' ? LEFT_ARROW : RIGHT_ARROW; + switch (event.keyCode) { + case UP_ARROW: + case DOWN_ARROW: { + this.onKeyEvent(event); + break; + } + case ENTER: + case SPACE: { + this.keyboardMenuItemSelect(event); + break; + } + case openSubMenuArrowKeyCode: { + this.keyboardOpenSubMenu(event); + break; + } + case ESCAPE: + case closeLeafMenuArrowKeyCode: { + this.onCloseLeafMenu(event); + break; + } } - this.cancelEvent(event); - this.closeLeafMenu.emit({ exceptRootMenu: event.keyCode === ARROW_LEFT_KEYCODE, event }); } @HostListener('document:click', ['$event']) @@ -223,6 +224,43 @@ export class ContextMenuContentComponent implements OnInit, OnDestroy, AfterView } } + private onKeyEvent(event: KeyboardEvent): void { + if (!this.isLeaf) { + return; + } + this._keyManager.onKeydown(event); + } + + private keyboardOpenSubMenu(event?: KeyboardEvent): void { + if (!this.isLeaf) { + return; + } + this.cancelEvent(event); + const menuItem = this.menuItems[this._keyManager.activeItemIndex]; + if (menuItem) { + this.onOpenSubMenu(menuItem); + } + } + + private keyboardMenuItemSelect(event?: KeyboardEvent): void { + if (!this.isLeaf) { + return; + } + this.cancelEvent(event); + const menuItem = this.menuItems[this._keyManager.activeItemIndex]; + if (menuItem) { + this.onMenuItemSelect(menuItem, event); + } + } + + private onCloseLeafMenu(event: KeyboardEvent): void { + if (!this.isLeaf) { + return; + } + this.cancelEvent(event); + this.closeLeafMenu.emit({ exceptRootMenu: event.keyCode === LEFT_ARROW, event }); + } + private cancelEvent(event): void { if (!event) { return; diff --git a/projects/ngx-contextmenu/src/lib/ngx-contextmenu.ts b/projects/ngx-contextmenu/src/lib/ngx-contextmenu.ts index 09075e3..ae118e7 100644 --- a/projects/ngx-contextmenu/src/lib/ngx-contextmenu.ts +++ b/projects/ngx-contextmenu/src/lib/ngx-contextmenu.ts @@ -1,4 +1,5 @@ -import { OverlayModule, FullscreenOverlayContainer, OverlayContainer, } from '@angular/cdk/overlay'; +import { BidiModule } from '@angular/cdk/bidi'; +import { FullscreenOverlayContainer, OverlayContainer, OverlayModule } from '@angular/cdk/overlay'; import { CommonModule } from '@angular/common'; import { ModuleWithProviders, NgModule } from '@angular/core'; @@ -28,6 +29,7 @@ import { ContextMenuContentComponent } from './contextMenuContent.component'; imports: [ CommonModule, OverlayModule, + BidiModule, ], }) export class ContextMenuModule { diff --git a/src/demo/app.component.html b/src/demo/app.component.html index 22ee362..8572263 100644 --- a/src/demo/app.component.html +++ b/src/demo/app.component.html @@ -90,6 +90,59 @@

Custom styling

+
+

RTL

+