diff --git a/src/app/components/page/MobileNavDrawer.test.tsx b/src/app/components/page/MobileNavDrawer.test.tsx index 1f4c45167a..4de18989d1 100644 --- a/src/app/components/page/MobileNavDrawer.test.tsx +++ b/src/app/components/page/MobileNavDrawer.test.tsx @@ -1,7 +1,9 @@ -import { render, screen } from '@testing-library/react'; +import { useLayoutEffect, useRef } from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { beforeAll, describe, expect, it, vi } from 'vitest'; import { MemoryRouter } from 'react-router-dom'; import { MobileNavDrawer } from './MobileNavDrawer'; +import { useMobileNavDrawer } from './MobileNavDrawerContext'; vi.mock('$state/hooks/settings', () => ({ useSetting: () => [true, vi.fn<() => void>()], @@ -36,6 +38,11 @@ const renderDrawer = () => ); +const touchList = (target: HTMLElement, clientX: number, clientY: number) => { + const point = { identifier: 0, target, clientX, clientY, pageX: clientX, pageY: clientY }; + return { touches: [point], targetTouches: [point], changedTouches: [point] }; +}; + describe('MobileNavDrawer', () => { // The panels sit side by side in a track twice the viewport wide, moved by transform. // `hidden` leaves a scrollport that focus or scrollIntoView scrolls a full panel width, @@ -48,4 +55,43 @@ describe('MobileNavDrawer', () => { expect(viewport.style.overflow).toBe('clip'); expect(viewport.style.overflow).not.toBe('hidden'); }); + + it('still drives message swipe when the gesture starts on a nested ignored element (e.g. an image)', () => { + const move = vi.fn<(distanceX: number) => void>(); + + function MessageProbe() { + const drawer = useMobileNavDrawer(); + const containerRef = useRef(null); + useLayoutEffect(() => { + const el = containerRef.current; + if (!el || !drawer) return undefined; + return drawer.registerMessageSwipe(el, { + move, + end: vi.fn<(gesture: { distanceX: number; velocityX: number }) => void>(), + cancel: vi.fn<() => void>(), + }); + }, [drawer]); + return ( +
+
+ image +
+
+ ); + } + + render( + + }> +
content
+
+
+ ); + + const image = screen.getByTestId('image'); + fireEvent.touchStart(image, touchList(image, 260, 100)); + fireEvent.touchMove(image, touchList(image, 200, 100)); + + expect(move).toHaveBeenCalled(); + }); }); diff --git a/src/app/components/page/MobileNavDrawer.tsx b/src/app/components/page/MobileNavDrawer.tsx index 9d4d2c1177..440fb4316d 100644 --- a/src/app/components/page/MobileNavDrawer.tsx +++ b/src/app/components/page/MobileNavDrawer.tsx @@ -353,7 +353,12 @@ export function MobileNavDrawer({ nav, rail, bottomNav, children }: MobileNavDra const ignoredElement = target.closest('[data-gestures="ignore"]'); const message = messageElement ? messageTargetsRef.current.get(messageElement) : undefined; const chat = chatElement ? chatTargetsRef.current.get(chatElement) : undefined; - const blocked = ignoredElement !== null && ignoredElement !== messageElement; + // Nested ignore markers (e.g. inline media) don't block swipe if they're part of + // this message/chat's own surface, only if they sit outside it. + const blocked = + ignoredElement !== null && + !(messageElement && messageElement.contains(ignoredElement)) && + !(chatElement && chatElement.contains(ignoredElement)); gestureRef.current = { startX: touch.clientX,