Skip to content

Commit

Permalink
feat(context-menu): add possibility to open via right click (#475)
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabian Maschewski authored and GitHub Enterprise committed Jan 27, 2022
1 parent 709fa7c commit c87c0b6
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
nx-card {
display: block;
}
.card-content {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<nx-context-menu #menu="nxContextMenu">
<button nxContextMenuItem type="button">Settings</button>
<button nxContextMenuItem type="button">Download</button>
<button nxContextMenuItem type="button">Help</button>
</nx-context-menu>

<nx-card [nxContextMenuTriggerFor]="menu" nxContextMenuTriggerMode="cursor">
<div class="card-content">
<p nxCopytext>Right click card to open context menu.</p>
<button
nxIconButton="tertiary small"
[nxContextMenuTriggerFor]="menu"
aria-label="Open menu"
type="button"
>
<nx-icon aria-hidden="true" name="ellipsis-h"></nx-icon>
</button>
</div>
</nx-card>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Component } from '@angular/core';

/**
* @title Cursor Mode Context Menu Example
*/
@Component({
selector: 'context-menu-cursor-mode-example',
templateUrl: './context-menu-cursor-mode-example.html',
styleUrls: ['./context-menu-cursor-mode-example.css'],
})
export class ContextMenuCursorModeExampleComponent {}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { NgModule } from '@angular/core';
import { NxBadgeModule } from '@aposin/ng-aquila/badge';
import { NxCardModule } from '@aposin/ng-aquila/card';
import { NxContextMenuModule } from '@aposin/ng-aquila/context-menu';
import { NxIconModule } from '@aposin/ng-aquila/icon';
import { NxIndicatorModule } from '@aposin/ng-aquila/indicator';
import { NxTableModule } from '@aposin/ng-aquila/table';
import { ExamplesSharedModule } from '../examples-shared.module';
import { ContextMenuBasicExampleComponent } from './context-menu-basic/context-menu-basic-example';
import { ContextMenuCursorModeExampleComponent } from './context-menu-cursor-mode/context-menu-cursor-mode-example';
import { ContextMenuDataExampleComponent } from './context-menu-data/context-menu-data-example';
import { ContextMenuDisabledExampleComponent } from './context-menu-disabled/context-menu-disabled-example';
import { ContextMenuIconsExampleComponent } from './context-menu-icons/context-menu-icons-example';
Expand All @@ -25,6 +27,7 @@ const EXAMPLES = [
ContextMenuProgrammaticExampleComponent,
ContextMenuScrollStrategyExampleComponent,
ContextMenuIndicatorExampleComponent,
ContextMenuCursorModeExampleComponent,
];

@NgModule({
Expand All @@ -34,6 +37,7 @@ const EXAMPLES = [
NxBadgeModule,
NxIndicatorModule,
NxTableModule,
NxCardModule,
ExamplesSharedModule,
],
declarations: [EXAMPLES],
Expand All @@ -53,6 +57,7 @@ export class ContextExamplesModule {
'context-menu-scroll-strategy':
ContextMenuScrollStrategyExampleComponent,
'context-menu-indicator': ContextMenuIndicatorExampleComponent,
'context-menu-cursor-mode': ContextMenuCursorModeExampleComponent,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export const MENU_PANEL_OFFSET_X = 8;

export type NxContextMenuScrollStrategy = 'close' | 'reposition';

export type NxContextMenuMode = 'button' | 'cursor';

interface Point {
x: number;
y: number;
}

/**
* This directive is intended to be used in conjunction with an nx-context-menu tag.
* It is responsible for toggling the display of the provided context menu instance.
Expand All @@ -34,6 +41,7 @@ export type NxContextMenuScrollStrategy = 'close' | 'reposition';
'(mousedown)': '_handleMousedown($event)',
'(keydown)': '_handleKeydown($event)',
'(click)': '_handleClick($event)',
'(contextmenu)': '_handleRightClick($event)',
},
exportAs: 'nxContextMenuTrigger',
})
Expand Down Expand Up @@ -96,6 +104,14 @@ export class NxContextMenuTriggerDirective implements AfterContentInit, OnInit,
/** Data to be passed along to any lazily-rendered content. */
@Input('nxContextMenuTriggerData') contextMenuData!: object;

/**
* Sets the mode of this context menu trigger.
* 'button' (default): Opens by clicking the trigger
* 'cursor': Opens at the cursor position by right clicking anywhere on the trigger.
*/
@Input('nxContextMenuTriggerMode')
mode: NxContextMenuMode = 'button';

/** Event emitted when the associated context menu is opened. */
@Output() readonly contextMenuOpened: EventEmitter<void> = new EventEmitter<void>();

Expand Down Expand Up @@ -159,7 +175,7 @@ export class NxContextMenuTriggerDirective implements AfterContentInit, OnInit,
}

/** Opens the context menu. */
openContextMenu(origin?: FocusOrigin): void {
openContextMenu(origin?: FocusOrigin, position?: Point): void {
if (this.contextMenuOpen) {
return;
}
Expand All @@ -169,7 +185,12 @@ export class NxContextMenuTriggerDirective implements AfterContentInit, OnInit,
const overlayRef = this._createOverlay();
const overlayConfig = overlayRef.getConfig();

this._setPosition(overlayConfig.positionStrategy as FlexibleConnectedPositionStrategy);
if (position) {
this._setPositionToCursor(overlayConfig.positionStrategy as FlexibleConnectedPositionStrategy, position);
} else {
this._setPosition(overlayConfig.positionStrategy as FlexibleConnectedPositionStrategy);
}

overlayRef.attach(this._getPortal());

if (this.contextMenu.lazyContent) {
Expand Down Expand Up @@ -309,6 +330,41 @@ export class NxContextMenuTriggerDirective implements AfterContentInit, OnInit,
});
}

/**
* Sets the position on a position strategy so the overlay is placed at the cursor.
* @param positionStrategy Strategy whose position to update.
* @param position Position of the cursor.
*/
private _setPositionToCursor(positionStrategy: FlexibleConnectedPositionStrategy, cursorPosition: Point) {
positionStrategy.setOrigin(cursorPosition);
positionStrategy.withPositions([
{
overlayX: 'start',
overlayY: 'top',
originX: 'center',
originY: 'center',
},
{
overlayX: 'start',
overlayY: 'bottom',
originX: 'center',
originY: 'center',
},
{
overlayX: 'end',
overlayY: 'top',
originX: 'center',
originY: 'center',
},
{
overlayX: 'end',
overlayY: 'bottom',
originX: 'center',
originY: 'center',
},
]);
}

/**
* Sets the appropriate positions on a position strategy
* so the overlay connects with the trigger correctly.
Expand Down Expand Up @@ -405,8 +461,28 @@ export class NxContextMenuTriggerDirective implements AfterContentInit, OnInit,
}
}

_handleRightClick(event: MouseEvent) {
if (this.mode !== 'cursor') {
return;
}

event.preventDefault();
if (this._contextMenuOpen) {
this.closeContextMenu();
}
const position = {
x: event.clientX,
y: event.clientY,
};
this.openContextMenu('mouse', position);
}

/** Handles key presses on the trigger. */
_handleKeydown(event: KeyboardEvent): void {
if (this.mode !== 'button') {
return;
}

const keyCode = event.keyCode;

if (this.triggersSubmenu() && ((keyCode === RIGHT_ARROW && this.dir === 'ltr') || (keyCode === LEFT_ARROW && this.dir === 'rtl'))) {
Expand All @@ -416,6 +492,11 @@ export class NxContextMenuTriggerDirective implements AfterContentInit, OnInit,

/** Handles click events on the trigger. */
_handleClick(event: MouseEvent): void {
if (this.mode !== 'button') {
return;
}
event.preventDefault();

const origin: FocusOrigin = event.detail ? 'program' : 'keyboard';

if (this.triggersSubmenu()) {
Expand All @@ -431,8 +512,8 @@ export class NxContextMenuTriggerDirective implements AfterContentInit, OnInit,
private _waitForClose() {
return this._documentClickObservable
.pipe(
filter(event => !event.defaultPrevented),
map(event => _getEventTarget(event)),
filter(target => !this._element.nativeElement.contains(target as Node | null)),
takeUntil(this.contextMenu.closed),
)
.subscribe(() => {
Expand Down
6 changes: 6 additions & 0 deletions projects/ng-aquila/src/context-menu/context-menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ContentChild,
ContentChildren,
EventEmitter,
HostListener,
NgZone,
OnDestroy,
Output,
Expand Down Expand Up @@ -77,6 +78,11 @@ export class NxContextMenuComponent implements AfterContentInit, OnDestroy {
/** Event emitted when the menu is closed. */
@Output() readonly closed: EventEmitter<void | 'click' | 'keydown' | 'tab'> = new EventEmitter<void | 'click' | 'keydown' | 'tab'>();

@HostListener('click')
private _onClick(event: Event) {
event.preventDefault();
}

constructor(private _ngZone: NgZone) {}

ngAfterContentInit() {
Expand Down
6 changes: 6 additions & 0 deletions projects/ng-aquila/src/context-menu/context-menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ To use a custom scroll container instead of the body element, [add cdkScrollable

<!-- example(context-menu-scroll-strategy) -->

### Right click

You can set the mode of the context menu to `cursor` to be able to open it via right click. Be aware that this can cause usability issues and it is only recommended to use this in combination with a visible button.

<!-- example(context-menu-cursor-mode) -->

### Keyboard interaction

- DOWN_ARROW: Focuses the next menu item
Expand Down
Loading

0 comments on commit c87c0b6

Please sign in to comment.