Skip to content

preventDefault ignored on event attributes due to passive being true #15066

@loganmacdougall

Description

@loganmacdougall

Describe the bug

When creating an application in svelte which requires drawing to a canvas controlled by both mouse and touch device, I found that mobile devices require a call to e.preventDefault in order to prevent the screen from scrolling and refreshing the page. However, when I call e.preventDefault to any of the attribute events used in drawing to the canvas, it is ignored, and gives a different warning based on browser to let you know that e.preventDefault was ignored due to a listener being registered as passive. I've provided a minimal recreation of how I got this warning.

Running the code on a mobile device (though the warning can't be seen), shows that e.preventDefault was ignored since the page is scrolling when I attempt to draw. The browsers on mobile used were safari and chrome.

I tried for a while to figure out what the svelte way of solving this issue, but then found that if I rewrite this code using addEventListener in the onMount function, I then get the exact effect I want, where I don't get any passive listener warnings, and I can call e.preventDefault, fixing my mobile issues. This leads me to believe that attribute events in svelte are either set to the incorrect passive settings by default, or need to bring back the feature to change the passive setting like in previous version of svelte.

Reproduction

Can be run using svelte playground here.

<script lang='ts'>
	import { onMount } from 'svelte';
	let canvas: HTMLCanvasElement; // set by svelte bind
	let context : CanvasRenderingContext2D; // set on mount
	let offset_top: number = $state(0); // set on mount
	let offset_left: number = $state(0); // set on mount
	let is_drawing: boolean = false;
	
	onMount(() => {
		context = canvas.getContext("2d")!;
		const bounding_box = canvas.getBoundingClientRect();
                offset_top = bounding_box.top;
                offset_left = bounding_box.left;
	})
	
	type Pos = {x:number, y:number}
	const handlestart = () => {
		is_drawing = true;
	};
	const handlemove = (e: Event, pos: Pos) => {
		if (!is_drawing) return;
                e.preventDefault();
		context.beginPath();
		context.fillStyle = '#000';
		context.arc(pos.x-5, pos.y-5, 10, 0, 360);
		context.fill();
	};
	const handleend = () => {
		is_drawing = false;
	};

	const calculatetouchpos = (e: TouchEvent) => {
                const { clientX, clientY } = e.touches[0];
                return {
                        x: clientX - offset_left,
                        y: clientY - offset_top,
              };
        }
</script>

<canvas
	bind:this={canvas}
	width=250
	height=250
	style:border="3px solid black"
	onmousedown={(e) => handlestart()}
	onmousemove={(e) => {
		handlemove(e, {x: e.offsetX, y: e.offsetY})
	}}
	onmouseup={(e) => handleend()}
	ontouchstart={(e) => { handlestart(); }}
	ontouchmove={(e) => {
		const pos = calculatetouchpos(e);
		handlemove(e, pos);
	}}
	ontouchend={() => {handleend()}}
	>
</canvas>

The hack to fix the issue temporarily can be run in the svelte playground here.

<svelte:options runes />

<script lang='ts'>
	import { onMount } from 'svelte';
	let canvas: HTMLCanvasElement; // set by svelte bind
	let context : CanvasRenderingContext2D; // set on mount
	let offset_top: number = $state(0); // set on mount
	let offset_left: number = $state(0); // set on mount
	let is_drawing: boolean = false;
	
	onMount(() => {
		context = canvas.getContext("2d")!;
		const bounding_box = canvas.getBoundingClientRect();
	        offset_top = bounding_box.top;
	        offset_left = bounding_box.left;

		canvas.addEventListener('touchmove', (e) => {		
			const pos = calculatetouchpos(e);
			handlemove(e, pos);
	  	});
	})
	
	type Pos = {x:number, y:number}
	const handlestart = () => {
		is_drawing = true;
	};
	const handlemove = (e: Event, pos: Pow) => {
		if (!is_drawing) return;
		e.preventDefault();
		context.beginPath();
		context.fillStyle = '#000';
		context.arc(pos.x-5, pos.y-5, 10, 0, 360);
		context.fill();
	};
	const handleend = () => {
		is_drawing = false;
	};

	const calculatetouchpos = (e: TouchEvent) => {
        	const { clientX, clientY } = e.touches[0];
  	      return {
       			x: clientX - offset_left,
		        y: clientY - offset_top,
	      };
	}
</script>

<canvas
	bind:this={canvas}
	width=250
	height=250
	style:border="3px solid black"
	onmousedown={(e) => handlestart()}
	onmousemove={(e) => {
		handlemove(e, {x: e.offsetX, y: e.offsetY})
	}}
	onmouseup={(e) => handleend()}
	ontouchstart={(e) => { handlestart(); }}
	ontouchend={() => {handleend()}}
	>
</canvas>

Logs

Firefox warning: Ignoring “preventDefault()” call on event of type “touchmove” from a listener registered as “passive”.
Chromium warning: Unable to preventDefault inside passive event listener invocation.

System Info

Firefox: 134.0.1 (64-bit)
Chrome: Version 132.0.6834.83 (Official Build) (64-bit)
Edge: Version 132.0.2957.115 (Official build) (64-bit)
Arc: Based on Chromium version 132.0.6834.84 (Official Build) (64-bit)

Severity

annoyance

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions