Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion src/app/components/page/MobileNavDrawer.test.tsx
Original file line number Diff line number Diff line change
@@ -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>()],
Expand Down Expand Up @@ -36,6 +38,11 @@ const renderDrawer = () =>
</MemoryRouter>
);

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,
Expand All @@ -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<HTMLDivElement | null>(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 (
<div ref={containerRef} data-message-swipe data-gestures="ignore">
<div data-gestures="ignore" data-testid="image">
image
</div>
</div>
);
}

render(
<MemoryRouter initialEntries={['/home']}>
<MobileNavDrawer nav={<MessageProbe />}>
<div>content</div>
</MobileNavDrawer>
</MemoryRouter>
);

const image = screen.getByTestId('image');
fireEvent.touchStart(image, touchList(image, 260, 100));
fireEvent.touchMove(image, touchList(image, 200, 100));

expect(move).toHaveBeenCalled();
});
});
7 changes: 6 additions & 1 deletion src/app/components/page/MobileNavDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,12 @@ export function MobileNavDrawer({ nav, rail, bottomNav, children }: MobileNavDra
const ignoredElement = target.closest<HTMLElement>('[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,
Expand Down
Loading