From 37c834d97584419c0ec800dd5b714b623811021a Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Mon, 16 Mar 2026 14:42:30 +0800 Subject: [PATCH] fix(reply-modal): keep drag interactions sharp (#1086) --- .../__tests__/reply-modal.test.tsx | 28 +++++++++++++++++-- .../reply-modal/reply-modal.module.css | 3 -- src/components/reply-modal/reply-modal.tsx | 23 ++++++++------- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/components/reply-modal/__tests__/reply-modal.test.tsx b/src/components/reply-modal/__tests__/reply-modal.test.tsx index 0300a25b..7162b7ae 100644 --- a/src/components/reply-modal/__tests__/reply-modal.test.tsx +++ b/src/components/reply-modal/__tests__/reply-modal.test.tsx @@ -166,15 +166,26 @@ vi.mock('lodash/debounce', () => ({ vi.mock('@react-spring/web', async () => { const React = await vi.importActual('react'); + const normalizeStyle = (style: Record | undefined) => + style + ? Object.fromEntries( + Object.entries(style).map(([key, value]) => [ + key, + typeof value === 'object' && value !== null && 'get' in value && typeof (value as { get: unknown }).get === 'function' + ? (value as { get: () => unknown }).get() + : value, + ]), + ) + : undefined; return { animated: { - div: React.forwardRef(({ style, ...props }: any, ref) => React.createElement('div', { ...props, ref, style: { touchAction: style?.touchAction } })), + div: React.forwardRef(({ style, ...props }: any, ref) => React.createElement('div', { ...props, ref, style: normalizeStyle(style) })), }, useSpring: () => [ { - x: { get: () => 120 }, - y: { get: () => 80 }, + left: { get: () => 120 }, + top: { get: () => 80 }, }, { start: testState.springStartMock, @@ -437,4 +448,15 @@ describe('ReplyModal', () => { expect(container.textContent).not.toContain('warning'); expect(container.textContent).not.toContain('Spoiler?'); }); + + it('positions the draggable modal with left/top styles instead of a transform layer', async () => { + await renderReplyModal('/mu/thread/post-1'); + + const modal = container.querySelector('[class*="container"]'); + + expect(modal?.style.left).toBe('120px'); + expect(modal?.style.top).toBe('80px'); + expect(modal?.style.transform).toBe(''); + expect(modal?.style.touchAction).toBe('none'); + }); }); diff --git a/src/components/reply-modal/reply-modal.module.css b/src/components/reply-modal/reply-modal.module.css index 8a7be92f..353b371c 100644 --- a/src/components/reply-modal/reply-modal.module.css +++ b/src/components/reply-modal/reply-modal.module.css @@ -8,9 +8,6 @@ z-index: 999; background-color: var(--challenge-modal-background-color); border: var(--challenge-modal-border); - will-change: transform; - backface-visibility: hidden; - -webkit-backface-visibility: hidden; } .title { diff --git a/src/components/reply-modal/reply-modal.tsx b/src/components/reply-modal/reply-modal.tsx index 1f27a578..7b53b982 100644 --- a/src/components/reply-modal/reply-modal.tsx +++ b/src/components/reply-modal/reply-modal.tsx @@ -111,13 +111,16 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa const nodeRef = useRef(null); const isMobile = useIsMobile(); - const [{ x, y }, api] = useSpring(() => ({ - x: window.innerWidth / 2 - 150, - y: window.innerHeight / 2 - 200, + const [{ left, top }, api] = useSpring(() => ({ + left: Math.round(window.innerWidth / 2 - 150), + top: Math.round(window.innerHeight / 2 - 200), })); const bind = useDrag( ({ active, event, offset: [ox, oy] }) => { + const nextLeft = Math.round(ox); + const nextTop = Math.round(oy); + if (active) { event.preventDefault(); document.body.style.userSelect = 'none'; @@ -126,10 +129,10 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa document.body.style.userSelect = ''; document.body.style.webkitUserSelect = ''; } - api.start({ x: ox, y: oy, immediate: true }); + api.start({ left: nextLeft, top: nextTop, immediate: true }); }, { - from: () => [x.get(), y.get()], + from: () => [left.get(), top.get()], filterTaps: true, bounds: undefined, }, @@ -138,10 +141,10 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa useEffect(() => { if (nodeRef.current && isMobile) { const viewportHeight = window.innerHeight; - const centeredPosition = scrollY + viewportHeight / 2 - 300; - api.start({ y: centeredPosition, immediate: true }); + const centeredPosition = Math.round(scrollY + viewportHeight / 2 - 300); + api.start({ top: centeredPosition, immediate: true }); } - }, [isMobile, scrollY, api]); + }, [api, isMobile, scrollY]); const parentCidRef = useRef(null); @@ -284,8 +287,8 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa className={styles.container} ref={nodeRef} style={{ - x, - y, + left, + top, touchAction: 'none', }} >