diff --git a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.ts b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.ts index 0c16f1be06ce..ab9e1881dccc 100644 --- a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.ts +++ b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.ts @@ -7,6 +7,7 @@ import { Inject, Input, OnChanges, + OnDestroy, OnInit, Output, Renderer2, @@ -36,7 +37,7 @@ import { AnswerPostReactionsBarComponent } from 'app/shared/metis/posting-reacti ]), ], }) -export class AnswerPostComponent extends PostingDirective implements OnInit, OnChanges { +export class AnswerPostComponent extends PostingDirective implements OnInit, OnChanges, OnDestroy { @Input() lastReadDate?: dayjs.Dayjs; @Input() isLastAnswer: boolean; @Output() openPostingCreateEditModal = new EventEmitter(); @@ -119,24 +120,33 @@ export class AnswerPostComponent extends PostingDirective implements } onRightClick(event: MouseEvent) { - event.preventDefault(); - - if (AnswerPostComponent.activeDropdownPost && AnswerPostComponent.activeDropdownPost !== this) { - AnswerPostComponent.activeDropdownPost.showDropdown = false; - AnswerPostComponent.activeDropdownPost.enableBodyScroll(); - AnswerPostComponent.activeDropdownPost.changeDetector.detectChanges(); + const targetElement = event.target as HTMLElement; + let isPointerCursor = false; + try { + isPointerCursor = window.getComputedStyle(targetElement).cursor === 'pointer'; + } catch (error) { + console.error('Failed to compute style:', error); + isPointerCursor = true; } - AnswerPostComponent.activeDropdownPost = this; + if (!isPointerCursor) { + event.preventDefault(); - this.dropdownPosition = { - x: event.clientX, - y: event.clientY, - }; + if (AnswerPostComponent.activeDropdownPost !== this) { + AnswerPostComponent.cleanupActiveDropdown(); + } - this.showDropdown = true; - this.adjustDropdownPosition(); - this.disableBodyScroll(); + AnswerPostComponent.activeDropdownPost = this; + + this.dropdownPosition = { + x: event.clientX, + y: event.clientY, + }; + + this.showDropdown = true; + this.adjustDropdownPosition(); + this.disableBodyScroll(); + } } adjustDropdownPosition() { @@ -148,6 +158,21 @@ export class AnswerPostComponent extends PostingDirective implements } } + private static cleanupActiveDropdown(): void { + if (AnswerPostComponent.activeDropdownPost) { + AnswerPostComponent.activeDropdownPost.showDropdown = false; + AnswerPostComponent.activeDropdownPost.enableBodyScroll(); + AnswerPostComponent.activeDropdownPost.changeDetector.detectChanges(); + AnswerPostComponent.activeDropdownPost = null; + } + } + + ngOnDestroy(): void { + if (AnswerPostComponent.activeDropdownPost === this) { + AnswerPostComponent.cleanupActiveDropdown(); + } + } + private assignPostingToAnswerPost() { // This is needed because otherwise instanceof returns 'object'. if (this.posting && !(this.posting instanceof AnswerPost)) { diff --git a/src/main/webapp/app/shared/metis/post/post.component.ts b/src/main/webapp/app/shared/metis/post/post.component.ts index 5deafdbd287b..feeb30d3ed0d 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.ts +++ b/src/main/webapp/app/shared/metis/post/post.component.ts @@ -128,24 +128,29 @@ export class PostComponent extends PostingDirective implements OnInit, OnC } onRightClick(event: MouseEvent) { - event.preventDefault(); + const targetElement = event.target as HTMLElement; + const isPointerCursor = window.getComputedStyle(targetElement).cursor === 'pointer'; - if (PostComponent.activeDropdownPost && PostComponent.activeDropdownPost !== this) { - PostComponent.activeDropdownPost.showDropdown = false; - PostComponent.activeDropdownPost.enableBodyScroll(); - PostComponent.activeDropdownPost.changeDetector.detectChanges(); - } + if (!isPointerCursor) { + event.preventDefault(); + + if (PostComponent.activeDropdownPost && PostComponent.activeDropdownPost !== this) { + PostComponent.activeDropdownPost.showDropdown = false; + PostComponent.activeDropdownPost.enableBodyScroll(); + PostComponent.activeDropdownPost.changeDetector.detectChanges(); + } - PostComponent.activeDropdownPost = this; + PostComponent.activeDropdownPost = this; - this.dropdownPosition = { - x: event.clientX, - y: event.clientY, - }; + this.dropdownPosition = { + x: event.clientX, + y: event.clientY, + }; - this.showDropdown = true; - this.adjustDropdownPosition(); - this.disableBodyScroll(); + this.showDropdown = true; + this.adjustDropdownPosition(); + this.disableBodyScroll(); + } } adjustDropdownPosition() { diff --git a/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts b/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts index 441b3e4ef382..3a84c4f12e87 100644 --- a/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts @@ -52,6 +52,10 @@ describe('AnswerPostComponent', () => { }); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('should contain an answer post header when isConsecutive is false', () => { runInInjectionContext(fixture.debugElement.injector, () => { component.isConsecutive = input(false); @@ -178,6 +182,42 @@ describe('AnswerPostComponent', () => { expect(component.posting.reactions).toEqual(updatedReactions); }); + it('should handle onRightClick correctly based on cursor style', () => { + const testCases = [ + { + cursor: 'pointer', + preventDefaultCalled: false, + showDropdown: false, + dropdownPosition: { x: 0, y: 0 }, + }, + { + cursor: 'default', + preventDefaultCalled: true, + showDropdown: true, + dropdownPosition: { x: 100, y: 200 }, + }, + ]; + + testCases.forEach(({ cursor, preventDefaultCalled, showDropdown, dropdownPosition }) => { + const event = new MouseEvent('contextmenu', { clientX: 100, clientY: 200 }); + + const targetElement = document.createElement('div'); + Object.defineProperty(event, 'target', { value: targetElement }); + + jest.spyOn(window, 'getComputedStyle').mockReturnValue({ + cursor, + } as CSSStyleDeclaration); + + const preventDefaultSpy = jest.spyOn(event, 'preventDefault'); + + component.onRightClick(event); + + expect(preventDefaultSpy).toHaveBeenCalledTimes(preventDefaultCalled ? 1 : 0); + expect(component.showDropdown).toBe(showDropdown); + expect(component.dropdownPosition).toEqual(dropdownPosition); + }); + }); + it('should cast the post to answer post on change', () => { const mockPost: Posting = { id: 1, diff --git a/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts b/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts index 6d0a1940a7d9..6c0859326aaa 100644 --- a/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts @@ -323,6 +323,44 @@ describe('PostComponent', () => { expect(enableBodyScrollSpy).toHaveBeenCalled(); }); + it('should handle onRightClick correctly based on cursor style', () => { + const testCases = [ + { + cursor: 'pointer', + preventDefaultCalled: false, + showDropdown: false, + dropdownPosition: { x: 0, y: 0 }, + }, + { + cursor: 'default', + preventDefaultCalled: true, + showDropdown: true, + dropdownPosition: { x: 100, y: 200 }, + }, + ]; + + testCases.forEach(({ cursor, preventDefaultCalled, showDropdown, dropdownPosition }) => { + const event = new MouseEvent('contextmenu', { clientX: 100, clientY: 200 }); + + const targetElement = document.createElement('div'); + Object.defineProperty(event, 'target', { value: targetElement }); + + jest.spyOn(window, 'getComputedStyle').mockReturnValue({ + cursor, + } as CSSStyleDeclaration); + + const preventDefaultSpy = jest.spyOn(event, 'preventDefault'); + + component.onRightClick(event); + + expect(preventDefaultSpy).toHaveBeenCalledTimes(preventDefaultCalled ? 1 : 0); + expect(component.showDropdown).toBe(showDropdown); + expect(component.dropdownPosition).toEqual(dropdownPosition); + + jest.restoreAllMocks(); + }); + }); + it('should cast the post to Post on change', () => { const mockPost: Posting = { id: 1,