ACS-11160: improve context menu overlay service implementation (#11661)

* refactor: improve context menu overlay service implementation

- Introduced a new interface for overlay references with backdrop handling.
- Updated event listener for backdrop clicks to use the new interface, enhancing type safety.
- Refactored the injector creation method to utilize the Injector.create method for better clarity and maintainability.
- Improved the fake element's type definition in the getOverlayConfig method for better type inference.

These changes enhance the overall robustness and readability of the context menu overlay service.

* refactor: enhance type safety in context menu components

- Introduced the ContextMenuItem interface to improve type definitions for context menu items.
- Updated the ContextMenuListComponent to use ContextMenuItem for links and menu item parameters, enhancing clarity and maintainability.
- Modified CONTEXT_MENU_DATA injection token to specify ContextMenuItem[] for better type safety.
- Adjusted the ContextMenuOverlayConfig interface to accept an array of ContextMenuItem, ensuring consistent data handling.

These changes improve the overall robustness and readability of the context menu implementation.
[ci:force]

* refactor: remove unnecessary eslint-disable comments in category management and search components

- Eliminated eslint-disable comments related to no-underscore-dangle in the CategoriesManagementComponent and SearchControlComponent tests, improving code clarity and adherence to linting rules.
- This change enhances the overall readability of the test files by ensuring consistent coding standards.

* refactor: simplify backdrop click handling in context menu overlay service

- Removed the unnecessary backdrop click handling logic, replacing it with a direct call to close the overlay reference. This change enhances code clarity and reduces complexity in the event listener implementation.

* refactor: streamline backdrop handling in context menu overlay service

- Removed the custom interface for overlay references with backdrop handling, simplifying the code.
- Updated the event listener for backdrop clicks to directly use the overlay's backdropElement, enhancing clarity and maintainability.
- This change improves the overall robustness of the context menu overlay service.
This commit is contained in:
Denys Vuika
2026-02-17 13:18:41 +00:00
committed by GitHub
parent 1f49af4c8f
commit fafa5d6c41
5 changed files with 45 additions and 26 deletions
@@ -328,7 +328,6 @@ describe('CategoriesManagementComponent', () => {
const categoriesChangeSpy = spyOn(component.categoriesChange, 'emit').and.callThrough(); const categoriesChangeSpy = spyOn(component.categoriesChange, 'emit').and.callThrough();
typeCategory('test'); typeCategory('test');
const options = getExistingCategoriesList(); const options = getExistingCategoriesList();
// eslint-disable-next-line no-underscore-dangle
options[0].click(); options[0].click();
expect(component.categories.length).toBe(3); expect(component.categories.length).toBe(3);
@@ -342,7 +341,6 @@ describe('CategoriesManagementComponent', () => {
it('should remove selected category from categories list and add it back to existing categories', fakeAsync(() => { it('should remove selected category from categories list and add it back to existing categories', fakeAsync(() => {
typeCategory('test'); typeCategory('test');
const options = getExistingCategoriesList(); const options = getExistingCategoriesList();
// eslint-disable-next-line no-underscore-dangle
options[0].click(); options[0].click();
fixture.detectChanges(); fixture.detectChanges();
@@ -26,6 +26,7 @@ import { NgForOf, NgIf } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core'; import { TranslatePipe } from '@ngx-translate/core';
import { DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes'; import { DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
import { IconModule } from '../icon/icon.module'; import { IconModule } from '../icon/icon.module';
import { ContextMenuItem } from './interfaces';
@Component({ @Component({
selector: 'adf-context-menu', selector: 'adf-context-menu',
@@ -42,10 +43,10 @@ import { IconModule } from '../icon/icon.module';
export class ContextMenuListComponent implements AfterViewInit { export class ContextMenuListComponent implements AfterViewInit {
private keyManager: FocusKeyManager<MatMenuItem>; private keyManager: FocusKeyManager<MatMenuItem>;
@ViewChildren(MatMenuItem) items: QueryList<MatMenuItem>; @ViewChildren(MatMenuItem) items: QueryList<MatMenuItem>;
links: any[]; links: ContextMenuItem[];
@HostListener('document:keydown.Escape', ['$event']) @HostListener('document:keydown.Escape', ['$event'])
handleKeydownEscape(event: KeyboardEvent) { handleKeydownEscape(event: Event) {
if (event) { if (event) {
this.contextMenuOverlayRef.close(); this.contextMenuOverlayRef.close();
} }
@@ -63,12 +64,12 @@ export class ContextMenuListComponent implements AfterViewInit {
constructor( constructor(
@Inject(ContextMenuOverlayRef) private contextMenuOverlayRef: ContextMenuOverlayRef, @Inject(ContextMenuOverlayRef) private contextMenuOverlayRef: ContextMenuOverlayRef,
@Optional() @Inject(CONTEXT_MENU_DATA) private data: any @Optional() @Inject(CONTEXT_MENU_DATA) private data: ContextMenuItem[]
) { ) {
this.links = this.data; this.links = this.data;
} }
onMenuItemClick(event: Event, menuItem: any) { onMenuItemClick(event: Event, menuItem: ContextMenuItem) {
if (menuItem?.model?.disabled) { if (menuItem?.model?.disabled) {
event.preventDefault(); event.preventDefault();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
@@ -17,7 +17,7 @@
import { Injectable, Injector, ElementRef, ComponentRef } from '@angular/core'; import { Injectable, Injector, ElementRef, ComponentRef } from '@angular/core';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { PortalInjector, ComponentPortal } from '@angular/cdk/portal'; import { ComponentPortal } from '@angular/cdk/portal';
import { ContextMenuOverlayRef } from './context-menu-overlay'; import { ContextMenuOverlayRef } from './context-menu-overlay';
import { ContextMenuOverlayConfig } from './interfaces'; import { ContextMenuOverlayConfig } from './interfaces';
import { CONTEXT_MENU_DATA } from './context-menu.tokens'; import { CONTEXT_MENU_DATA } from './context-menu.tokens';
@@ -33,7 +33,10 @@ const DEFAULT_CONFIG: ContextMenuOverlayConfig = {
providedIn: 'root' providedIn: 'root'
}) })
export class ContextMenuOverlayService { export class ContextMenuOverlayService {
constructor(private injector: Injector, private overlay: Overlay) {} constructor(
private injector: Injector,
private overlay: Overlay
) {}
open(config: ContextMenuOverlayConfig): ContextMenuOverlayRef { open(config: ContextMenuOverlayConfig): ContextMenuOverlayRef {
const overlayConfig = { ...DEFAULT_CONFIG, ...config }; const overlayConfig = { ...DEFAULT_CONFIG, ...config };
@@ -47,14 +50,12 @@ export class ContextMenuOverlayService {
overlay.backdropClick().subscribe(() => overlayRef.close()); overlay.backdropClick().subscribe(() => overlayRef.close());
// prevent native contextmenu on overlay element if config.hasBackdrop is true // prevent native contextmenu on overlay element if config.hasBackdrop is true
if (overlayConfig.hasBackdrop) { if (overlayConfig.hasBackdrop && overlay.backdropElement) {
// eslint-disable-next-line no-underscore-dangle overlay.backdropElement.addEventListener(
(overlay as any).backdropElement.addEventListener(
'contextmenu', 'contextmenu',
(event) => { (event) => {
event.preventDefault(); event.preventDefault();
// eslint-disable-next-line no-underscore-dangle overlayRef.close();
(overlay as any)._backdropClick.next(null);
}, },
true true
); );
@@ -77,28 +78,32 @@ export class ContextMenuOverlayService {
return containerRef.instance; return containerRef.instance;
} }
private createInjector(config: ContextMenuOverlayConfig, contextMenuOverlayRef: ContextMenuOverlayRef): PortalInjector { private createInjector(config: ContextMenuOverlayConfig, contextMenuOverlayRef: ContextMenuOverlayRef): Injector {
const injectionTokens = new WeakMap(); return Injector.create({
parent: this.injector,
injectionTokens.set(ContextMenuOverlayRef, contextMenuOverlayRef); providers: [
injectionTokens.set(CONTEXT_MENU_DATA, config.data); { provide: ContextMenuOverlayRef, useValue: contextMenuOverlayRef },
{ provide: CONTEXT_MENU_DATA, useValue: config.data }
return new PortalInjector(this.injector, injectionTokens); ]
});
} }
private getOverlayConfig(config: ContextMenuOverlayConfig): OverlayConfig { private getOverlayConfig(config: ContextMenuOverlayConfig): OverlayConfig {
const { clientY, clientX } = config.source; const { clientY, clientX } = config.source;
const fakeElement: any = { const fakeElement: Pick<HTMLElement, 'getBoundingClientRect'> = {
getBoundingClientRect: (): ClientRect => getBoundingClientRect: (): DOMRect =>
({ ({
bottom: clientY, bottom: clientY,
height: 0, height: 0,
left: clientX, left: clientX,
right: clientX, right: clientX,
top: clientY, top: clientY,
width: 0 width: 0,
} as any) x: clientX,
y: clientY,
toJSON: () => ({})
}) as DOMRect
}; };
const positionStrategy = this.overlay const positionStrategy = this.overlay
@@ -16,5 +16,6 @@
*/ */
import { InjectionToken } from '@angular/core'; import { InjectionToken } from '@angular/core';
import { ContextMenuItem } from './interfaces';
export const CONTEXT_MENU_DATA = new InjectionToken<any>('CONTEXT_MENU_DATA'); export const CONTEXT_MENU_DATA = new InjectionToken<ContextMenuItem[]>('CONTEXT_MENU_DATA');
+15 -1
View File
@@ -20,5 +20,19 @@ export interface ContextMenuOverlayConfig {
hasBackdrop?: boolean; hasBackdrop?: boolean;
backdropClass?: string; backdropClass?: string;
source?: MouseEvent; source?: MouseEvent;
data?: any; data?: ContextMenuItem[];
}
export interface ContextMenuItem {
title?: string;
model?: {
title?: string;
visible?: boolean;
disabled?: boolean;
icon?: string;
tooltip?: string;
};
subject: {
next: (value: ContextMenuItem) => void;
};
} }