diff --git a/compat/test/browser/events.test.jsx b/compat/test/browser/events.test.jsx index 92fa1166b6..eab93fa842 100644 --- a/compat/test/browser/events.test.jsx +++ b/compat/test/browser/events.test.jsx @@ -138,13 +138,19 @@ describe('preact/compat events', () => { expect(proto.addEventListener.mock.calls.length).to.eql(4); expect(proto.addEventListener.mock.calls[0].length).to.eql(3); expect(proto.addEventListener.mock.calls[0][0]).to.eql('touchstart'); - expect(proto.addEventListener.mock.calls[0][2]).to.eql(false); + expect(proto.addEventListener.mock.calls[0][2]).to.eql({ + passive: true, + capture: false + }); expect(proto.addEventListener.mock.calls[1].length).to.eql(3); expect(proto.addEventListener.mock.calls[1][0]).to.eql('touchend'); expect(proto.addEventListener.mock.calls[1][2]).to.eql(false); expect(proto.addEventListener.mock.calls[2].length).to.eql(3); expect(proto.addEventListener.mock.calls[2][0]).to.eql('touchmove'); - expect(proto.addEventListener.mock.calls[2][2]).to.eql(false); + expect(proto.addEventListener.mock.calls[2][2]).to.eql({ + passive: true, + capture: false + }); expect(proto.addEventListener.mock.calls[3].length).to.eql(3); expect(proto.addEventListener.mock.calls[3][0]).to.eql('touchcancel'); expect(proto.addEventListener.mock.calls[3][2]).to.eql(false); @@ -286,7 +292,7 @@ describe('preact/compat events', () => { expect(proto.addEventListener).toHaveBeenCalledWith( 'touchmove', expect.any(Function), - true + { passive: true, capture: true } ); }); } diff --git a/src/diff/props.js b/src/diff/props.js index bd12d19418..ce11932ff7 100644 --- a/src/diff/props.js +++ b/src/diff/props.js @@ -85,7 +85,13 @@ export function setProperty(dom, name, value, oldValue, namespace) { dom.addEventListener( name, useCapture ? eventProxyCapture : eventProxy, - useCapture + // touchstart/touchmove/wheel are attached passively to keep + // scrolling off the main thread; matches React 18. Calling + // `preventDefault()` in these handlers is a no-op — attach a + // non-passive listener via a ref for that. + name == 'touchstart' || name == 'touchmove' || name == 'wheel' + ? { passive: true, capture: useCapture } + : useCapture ); } else { value[EVENT_ATTACHED] = oldValue[EVENT_ATTACHED]; diff --git a/test/browser/events.test.jsx b/test/browser/events.test.jsx index 456b645c51..790758e4bc 100644 --- a/test/browser/events.test.jsx +++ b/test/browser/events.test.jsx @@ -182,6 +182,80 @@ describe('event handling', () => { ); }); + it('should attach touch and wheel listeners as passive', () => { + render( +
{}} + onTouchMove={() => {}} + onWheel={() => {}} + onTouchEnd={() => {}} + />, + scratch + ); + + expect(proto.addEventListener).toHaveBeenCalledTimes(4); + expect(proto.addEventListener).toHaveBeenCalledWith( + 'touchstart', + expect.any(Function), + { passive: true, capture: false } + ); + expect(proto.addEventListener).toHaveBeenCalledWith( + 'touchmove', + expect.any(Function), + { passive: true, capture: false } + ); + expect(proto.addEventListener).toHaveBeenCalledWith( + 'wheel', + expect.any(Function), + { passive: true, capture: false } + ); + expect(proto.addEventListener).toHaveBeenCalledWith( + 'touchend', + expect.any(Function), + false + ); + }); + + it('should attach capturing passive listeners with capture: true', () => { + render(
{}} />, scratch); + + expect(proto.addEventListener).toHaveBeenCalledWith( + 'touchmove', + expect.any(Function), + { passive: true, capture: true } + ); + }); + + it('should invoke and swap passive handlers without re-attaching', () => { + let first = vi.fn(), + second = vi.fn(); + + render(
, scratch); + render(
, scratch); + + expect(proto.addEventListener).toHaveBeenCalledOnce(); + + fireEvent(scratch.childNodes[0], 'touchmove'); + expect(first).not.toHaveBeenCalled(); + expect(second).toHaveBeenCalledOnce(); + }); + + it('should remove passive listeners', () => { + let wheel = vi.fn(); + + render(
, scratch); + render(
, scratch); + + expect(proto.removeEventListener).toHaveBeenCalledWith( + 'wheel', + expect.any(Function), + false + ); + + fireEvent(scratch.childNodes[0], 'wheel'); + expect(wheel).not.toHaveBeenCalled(); + }); + // Skip test if browser doesn't support passive events if (supportsPassiveEvents()) { it('should use capturing for event props ending with *Capture', () => {