From 1f87a559fb98249d0df88533c91554cf907a7b8c Mon Sep 17 00:00:00 2001 From: Jack Crymble Date: Thu, 12 Mar 2026 15:32:22 +0000 Subject: [PATCH] fix: additional check for touch event before calling preventDefault --- .../src/lib/resizable.directive.ts | 3 +- .../src/lib/resize-handle.directive.ts | 12 +++---- .../src/lib/util/get-listen-options.ts | 14 -------- .../src/lib/util/is-touch-event.ts | 10 ++++++ .../src/test/resizable.spec.ts | 36 ++++++++++++++----- 5 files changed, 44 insertions(+), 31 deletions(-) delete mode 100644 projects/angular-resizable-element/src/lib/util/get-listen-options.ts create mode 100644 projects/angular-resizable-element/src/lib/util/is-touch-event.ts diff --git a/projects/angular-resizable-element/src/lib/resizable.directive.ts b/projects/angular-resizable-element/src/lib/resizable.directive.ts index 1d61c67..91a1f6a 100644 --- a/projects/angular-resizable-element/src/lib/resizable.directive.ts +++ b/projects/angular-resizable-element/src/lib/resizable.directive.ts @@ -29,6 +29,7 @@ import { BoundingRectangle } from './interfaces/bounding-rectangle.interface'; import { ResizeEvent } from './interfaces/resize-event.interface'; import { IS_TOUCH_DEVICE } from './util/is-touch-device'; import { deepCloneNode } from './util/clone-node'; +import { isTouchEvent } from './util/is-touch-event'; interface PointerEventCoordinate { clientX: number; @@ -333,7 +334,7 @@ export class ResizableDirective implements OnInit, OnDestroy { this.mousemove, ).pipe( tap(({ event }) => { - if (currentResize && event.cancelable) { + if (currentResize && event.cancelable && !isTouchEvent(event)) { event.preventDefault(); } }), diff --git a/projects/angular-resizable-element/src/lib/resize-handle.directive.ts b/projects/angular-resizable-element/src/lib/resize-handle.directive.ts index 20451a4..9b40bac 100644 --- a/projects/angular-resizable-element/src/lib/resize-handle.directive.ts +++ b/projects/angular-resizable-element/src/lib/resize-handle.directive.ts @@ -12,8 +12,8 @@ import { fromEvent, merge, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { ResizableDirective } from './resizable.directive'; import { Edges } from './interfaces/edges.interface'; -import { getListenOptions } from './util/get-listen-options'; import { IS_TOUCH_DEVICE } from './util/is-touch-device'; +import { isTouchEvent } from './util/is-touch-event'; /** * An element placed inside a `mwlResizable` directive to be used as a drag and resize handle @@ -100,7 +100,7 @@ export class ResizeHandleDirective implements OnInit, OnDestroy { clientX: number, clientY: number, ): void { - if (event.cancelable) { + if (event.cancelable && !isTouchEvent(event)) { event.preventDefault(); } if (!this.eventListeners.touchmove) { @@ -174,10 +174,8 @@ export class ResizeHandleDirective implements OnInit, OnDestroy { } private listenOnTheHost(eventName: string) { - return fromEvent( - this.element.nativeElement, - eventName, - getListenOptions(eventName), - ).pipe(takeUntil(this.destroy$)); + return fromEvent(this.element.nativeElement, eventName).pipe( + takeUntil(this.destroy$), + ); } } diff --git a/projects/angular-resizable-element/src/lib/util/get-listen-options.ts b/projects/angular-resizable-element/src/lib/util/get-listen-options.ts deleted file mode 100644 index 1f34a3c..0000000 --- a/projects/angular-resizable-element/src/lib/util/get-listen-options.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Options for fromEvent when listening on the host. - * Touch events need passive: false so preventDefault can be called. - * @hidden - */ -export function getListenOptions( - eventName: string, -): { passive: false } | Record { - const isTouchEvent = - eventName === 'touchstart' || - eventName === 'touchend' || - eventName === 'touchcancel'; - return isTouchEvent ? { passive: false } : {}; -} diff --git a/projects/angular-resizable-element/src/lib/util/is-touch-event.ts b/projects/angular-resizable-element/src/lib/util/is-touch-event.ts new file mode 100644 index 0000000..d003848 --- /dev/null +++ b/projects/angular-resizable-element/src/lib/util/is-touch-event.ts @@ -0,0 +1,10 @@ +/** + * Check if the event is a touch event + * @param event - The event to check + * @returns True if the event is a touch event, false otherwise + * @hidden + */ + +export function isTouchEvent(event: Event): boolean { + return event.type.startsWith('touch'); +} diff --git a/projects/angular-resizable-element/src/test/resizable.spec.ts b/projects/angular-resizable-element/src/test/resizable.spec.ts index 6231ff1..13bda33 100644 --- a/projects/angular-resizable-element/src/test/resizable.spec.ts +++ b/projects/angular-resizable-element/src/test/resizable.spec.ts @@ -5,7 +5,6 @@ import { ResizeEvent, ResizeHandleDirective, } from 'angular-resizable-element'; -import { getListenOptions } from '../lib/util/get-listen-options'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { expect } from 'chai'; import * as sinon from 'sinon'; @@ -104,6 +103,7 @@ import { NgStyle } from '@angular/common'; }) class TestComponent { @ViewChild(ResizableDirective) resizable: ResizableDirective; + @ViewChild(ResizeHandleDirective) handleDirective: ResizeHandleDirective; @ViewChild('handle') handle: ElementRef; style: object = {}; resizeStart: sinon.SinonSpy = sinon.spy(); @@ -508,16 +508,34 @@ describe('resizable directive', () => { }); describe('touch event listeners', () => { - ['touchstart', 'touchend', 'touchcancel'].forEach((eventName) => { - it(`when eventName is ${eventName}, getListenOptions returns passive: false so fromEvent is called with passive: false`, () => { - expect(getListenOptions(eventName)).to.deep.equal({ passive: false }); - }); + it('should not call preventDefault for touch events when cancelable', () => { + const fixture: ComponentFixture = createComponent(); + const touchEvent = { + type: 'touchstart', + cancelable: true, + preventDefault: sinon.spy(), + } as unknown as TouchEvent; + fixture.componentInstance.handleDirective.onMousedown( + touchEvent, + 100, + 200, + ); + expect(touchEvent.preventDefault).not.to.have.been.called; }); - ['mousedown', 'mouseup'].forEach((eventName) => { - it(`when eventName is ${eventName}, getListenOptions returns empty options`, () => { - expect(getListenOptions(eventName)).to.deep.equal({}); - }); + it('should call preventDefault for mouse events when cancelable', () => { + const fixture: ComponentFixture = createComponent(); + const mouseEvent = { + type: 'mousedown', + cancelable: true, + preventDefault: sinon.spy(), + } as unknown as MouseEvent; + fixture.componentInstance.handleDirective.onMousedown( + mouseEvent, + 100, + 200, + ); + expect(mouseEvent.preventDefault).to.have.been.calledOnce; }); });