diff --git a/.changeset/little-suits-leave.md b/.changeset/little-suits-leave.md new file mode 100644 index 0000000000..29f44bb5ff --- /dev/null +++ b/.changeset/little-suits-leave.md @@ -0,0 +1,6 @@ +--- +'rrweb': minor +'@rrweb/types': minor +--- + +click events (as well as mousedown/mouseup/touchstart/touchend events) now include a `.pointerType` attribute which distinguishes between ['pen', 'mouse' and 'touch' events](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pointerType) diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index d2852b970a..2e4ab35976 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -9,7 +9,7 @@ import { getWindowHeight, getWindowWidth, isBlocked, - isTouchEvent, + legacy_isTouchEvent, patch, StyleSheetMirror, } from '../utils'; @@ -20,6 +20,7 @@ import { mousePosition, mouseInteractionCallBack, MouseInteractions, + PointerTypes, listenerHandler, scrollCallback, styleSheetRuleCallback, @@ -170,7 +171,8 @@ function initMoveObserver({ throttle( callbackWrapper((evt) => { const target = getEventTarget(evt); - const { clientX, clientY } = isTouchEvent(evt) + // 'legacy' here as we could switch to https://developer.mozilla.org/en-US/docs/Web/API/Element/pointermove_event + const { clientX, clientY } = legacy_isTouchEvent(evt) ? evt.changedTouches[0] : evt; if (!timeBaseline) { @@ -228,13 +230,47 @@ function initMouseInteractionObserver({ : sampling.mouseInteraction; const handlers: listenerHandler[] = []; + let currentPointerType = null; const getHandler = (eventKey: keyof typeof MouseInteractions) => { - return (event: MouseEvent | TouchEvent) => { + return (event: MouseEvent | TouchEvent | PointerEvent) => { const target = getEventTarget(event) as Node; if (isBlocked(target, blockClass, blockSelector, true)) { return; } - const e = isTouchEvent(event) ? event.changedTouches[0] : event; + let pointerType: PointerTypes | null = null; + let e = event; + if ('pointerType' in e) { + Object.keys(PointerTypes).forEach( + (pointerKey: keyof typeof PointerKeys) => { + if ((e as PointerEvent).pointerType === pointerKey.toLowerCase()) { + pointerType = PointerTypes[pointerKey]; + return; + } + }, + ); + if (pointerType === PointerTypes.Touch) { + if (MouseInteractions[eventKey] === MouseInteractions.MouseDown) { + // we are actually listening on 'pointerdown' + eventKey = 'TouchStart'; + } else if ( + MouseInteractions[eventKey] === MouseInteractions.MouseUp + ) { + // we are actually listening on 'pointerup' + eventKey = 'TouchEnd'; + } + } else if (pointerType == PointerTypes.Pen) { + // TODO: these will get incorrectly emitted as MouseDown/MouseUp + } + } else if (legacy_isTouchEvent(event)) { + e = event.changedTouches[0]; + pointerType = PointerTypes.Touch; + } + if (pointerType !== null) { + currentPointerType = pointerType; + } else if (MouseInteractions[eventKey] === MouseInteractions.Click) { + pointerType = currentPointerType; + currentPointerType = null; // cleanup as we've used it + } if (!e) { return; } @@ -245,6 +281,7 @@ function initMouseInteractionObserver({ id, x: clientX, y: clientY, + ...(pointerType !== null && { pointerType }), }); }; }; @@ -256,8 +293,20 @@ function initMouseInteractionObserver({ disableMap[key] !== false, ) .forEach((eventKey: keyof typeof MouseInteractions) => { - const eventName = eventKey.toLowerCase(); + let eventName = eventKey.toLowerCase(); const handler = getHandler(eventKey); + if (window.PointerEvent) { + switch (MouseInteractions[eventKey]) { + case MouseInteractions.MouseDown: + case MouseInteractions.MouseUp: + eventName = eventName.replace('mouse', 'pointer'); + break; + case MouseInteractions.TouchStart: + case MouseInteractions.TouchEnd: + // these are handled by pointerdown/pointerup + return; + } + } handlers.push(on(eventName, handler, doc)); }); return callbackWrapper(() => { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 1626e3734c..78ca08448d 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -277,8 +277,8 @@ export function isAncestorRemoved(target: Node, mirror: Mirror): boolean { return isAncestorRemoved(target.parentNode, mirror); } -export function isTouchEvent( - event: MouseEvent | TouchEvent, +export function legacy_isTouchEvent( + event: MouseEvent | TouchEvent | PointerEvent, ): event is TouchEvent { return Boolean((event as TouchEvent).changedTouches); } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 1188ef2fb7..6601457291 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -362,6 +362,12 @@ export enum MouseInteractions { TouchCancel, } +export enum PointerTypes { + Mouse, + Pen, + Touch, +} + export enum CanvasContext { '2D', WebGL, @@ -404,6 +410,7 @@ type mouseInteractionParam = { id: number; x: number; y: number; + pointerType?: PointerTypes; }; export type mouseInteractionCallBack = (d: mouseInteractionParam) => void;