fix(reply-modal): keep drag interactions sharp (#1086)

This commit is contained in:
Tommaso Casaburi
2026-03-16 14:42:30 +08:00
committed by GitHub
parent 3a7400e4a8
commit 37c834d975
3 changed files with 38 additions and 16 deletions
@@ -166,15 +166,26 @@ vi.mock('lodash/debounce', () => ({
vi.mock('@react-spring/web', async () => { vi.mock('@react-spring/web', async () => {
const React = await vi.importActual<typeof import('react')>('react'); const React = await vi.importActual<typeof import('react')>('react');
const normalizeStyle = (style: Record<string, unknown> | 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 { return {
animated: { 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: () => [ useSpring: () => [
{ {
x: { get: () => 120 }, left: { get: () => 120 },
y: { get: () => 80 }, top: { get: () => 80 },
}, },
{ {
start: testState.springStartMock, start: testState.springStartMock,
@@ -437,4 +448,15 @@ describe('ReplyModal', () => {
expect(container.textContent).not.toContain('warning'); expect(container.textContent).not.toContain('warning');
expect(container.textContent).not.toContain('Spoiler?'); 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<HTMLDivElement>('[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');
});
}); });
@@ -8,9 +8,6 @@
z-index: 999; z-index: 999;
background-color: var(--challenge-modal-background-color); background-color: var(--challenge-modal-background-color);
border: var(--challenge-modal-border); border: var(--challenge-modal-border);
will-change: transform;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
} }
.title { .title {
+13 -10
View File
@@ -111,13 +111,16 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
const nodeRef = useRef<HTMLDivElement>(null); const nodeRef = useRef<HTMLDivElement>(null);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [{ x, y }, api] = useSpring(() => ({ const [{ left, top }, api] = useSpring(() => ({
x: window.innerWidth / 2 - 150, left: Math.round(window.innerWidth / 2 - 150),
y: window.innerHeight / 2 - 200, top: Math.round(window.innerHeight / 2 - 200),
})); }));
const bind = useDrag( const bind = useDrag(
({ active, event, offset: [ox, oy] }) => { ({ active, event, offset: [ox, oy] }) => {
const nextLeft = Math.round(ox);
const nextTop = Math.round(oy);
if (active) { if (active) {
event.preventDefault(); event.preventDefault();
document.body.style.userSelect = 'none'; document.body.style.userSelect = 'none';
@@ -126,10 +129,10 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
document.body.style.userSelect = ''; document.body.style.userSelect = '';
document.body.style.webkitUserSelect = ''; 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, filterTaps: true,
bounds: undefined, bounds: undefined,
}, },
@@ -138,10 +141,10 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
useEffect(() => { useEffect(() => {
if (nodeRef.current && isMobile) { if (nodeRef.current && isMobile) {
const viewportHeight = window.innerHeight; const viewportHeight = window.innerHeight;
const centeredPosition = scrollY + viewportHeight / 2 - 300; const centeredPosition = Math.round(scrollY + viewportHeight / 2 - 300);
api.start({ y: centeredPosition, immediate: true }); api.start({ top: centeredPosition, immediate: true });
} }
}, [isMobile, scrollY, api]); }, [api, isMobile, scrollY]);
const parentCidRef = useRef<HTMLSpanElement>(null); const parentCidRef = useRef<HTMLSpanElement>(null);
@@ -284,8 +287,8 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
className={styles.container} className={styles.container}
ref={nodeRef} ref={nodeRef}
style={{ style={{
x, left,
y, top,
touchAction: 'none', touchAction: 'none',
}} }}
> >