diff --git a/packages/design-system/src/composables/useDeviceSupport.test.ts b/packages/design-system/src/composables/useDeviceSupport.test.ts index 767a751601..0260dcacaf 100644 --- a/packages/design-system/src/composables/useDeviceSupport.test.ts +++ b/packages/design-system/src/composables/useDeviceSupport.test.ts @@ -1,5 +1,11 @@ import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport'; +const detectPointerType = (query: string) => { + const isCoarse = query === '(any-pointer: coarse)'; + const isFine = query === '(any-pointer: fine)'; + return { fine: isFine, coarse: isCoarse }; +}; + describe('useDeviceSupport()', () => { beforeEach(() => { global.window = Object.create(window); @@ -7,24 +13,38 @@ describe('useDeviceSupport()', () => { }); describe('isTouchDevice', () => { - it('should be true if ontouchstart is in window', () => { - Object.defineProperty(window, 'ontouchstart', {}); - const { isTouchDevice } = useDeviceSupport(); - expect(isTouchDevice).toEqual(true); - }); - - it('should be true if navigator.maxTouchPoints > 0', () => { - Object.defineProperty(navigator, 'maxTouchPoints', { value: 1 }); - const { isTouchDevice } = useDeviceSupport(); - expect(isTouchDevice).toEqual(true); - }); - - it('should be false if no touch support', () => { - delete window.ontouchstart; - Object.defineProperty(navigator, 'maxTouchPoints', { value: 0 }); + it('should be false if window matches `any-pointer: fine` and `!any-pointer: coarse`', () => { + Object.defineProperty(window, 'matchMedia', { + value: vi.fn().mockImplementation((query: string) => { + const { fine, coarse } = detectPointerType(query); + return { matches: fine && !coarse }; + }), + }); const { isTouchDevice } = useDeviceSupport(); expect(isTouchDevice).toEqual(false); }); + + it('should be false if window matches `any-pointer: fine` and `any-pointer: coarse`', () => { + Object.defineProperty(window, 'matchMedia', { + value: vi.fn().mockImplementation((query: string) => { + const { fine, coarse } = detectPointerType(query); + return { matches: fine && coarse }; + }), + }); + const { isTouchDevice } = useDeviceSupport(); + expect(isTouchDevice).toEqual(false); + }); + + it('should be true if window matches `any-pointer: coarse` and `!any-pointer: fine`', () => { + Object.defineProperty(window, 'matchMedia', { + value: vi.fn().mockImplementation((query: string) => { + const { fine, coarse } = detectPointerType(query); + return { matches: coarse && !fine }; + }), + }); + const { isTouchDevice } = useDeviceSupport(); + expect(isTouchDevice).toEqual(true); + }); }); describe('isMacOs', () => { diff --git a/packages/design-system/src/composables/useDeviceSupport.ts b/packages/design-system/src/composables/useDeviceSupport.ts index 8ead46c140..63d5549dcc 100644 --- a/packages/design-system/src/composables/useDeviceSupport.ts +++ b/packages/design-system/src/composables/useDeviceSupport.ts @@ -1,7 +1,16 @@ import { ref } from 'vue'; export function useDeviceSupport() { - const isTouchDevice = ref(window.hasOwnProperty('ontouchstart') || navigator.maxTouchPoints > 0); + /** + * Check if the device is a touch device but exclude devices that have a fine pointer (mouse or track-pad) + * - `fine` will check for an accurate pointing device. Examples include mice, touch-pads, and drawing styluses + * - `coarse` will check for a pointing device of limited accuracy. Examples include touchscreens and motion-detection sensors + * - `any-pointer` will check for the presence of any pointing device, if there are multiple of them + */ + const isTouchDevice = ref( + window.matchMedia('(any-pointer: coarse)').matches && + !window.matchMedia('(any-pointer: fine)').matches, + ); const userAgent = ref(navigator.userAgent.toLowerCase()); const isMacOs = ref( userAgent.value.includes('macintosh') || diff --git a/packages/editor-ui/src/__tests__/setup.ts b/packages/editor-ui/src/__tests__/setup.ts index 3b9c07462e..91700ea616 100644 --- a/packages/editor-ui/src/__tests__/setup.ts +++ b/packages/editor-ui/src/__tests__/setup.ts @@ -44,3 +44,18 @@ export class IntersectionObserver { window.IntersectionObserver = IntersectionObserver; global.IntersectionObserver = IntersectionObserver; + +// Mocks for useDeviceSupport +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: true, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +});