Repository: sveltejs/svelte (JavaScript)
Date: 2018-12-29 15:28:10+00:00
Task type: Bug Fix

Issue:
`passive: true` is applied to events incorrectly
[REPL](https://v3.svelte.technology/repl?version=3.0.0-alpha12&gist=d52fe6c7022188241e15190f2cbd4de2). In v3, Svelte assumes you're never using the `event` object, so it applies the `passive: true` modifier more liberally.


Issue discussion:

Solution patch:
diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts
--- a/src/compile/nodes/Element.ts
+++ b/src/compile/nodes/Element.ts
@@ -585,7 +585,7 @@ export default class Element extends Node {
 
 				if (modifier === 'passive') {
 					if (passiveEvents.has(handler.name)) {
-						if (!handler.usesEventObject) {
+						if (handler.canMakePassive) {
 							component.warn(handler, {
 								code: 'redundant-event-modifier',
 								message: `Touch event handlers that don't use the 'event' object are passive by default`
@@ -609,7 +609,7 @@ export default class Element extends Node {
 				}
 			});
 
-			if (passiveEvents.has(handler.name) && !handler.usesEventObject && !handler.modifiers.has('preventDefault')) {
+			if (passiveEvents.has(handler.name) && handler.canMakePassive && !handler.modifiers.has('preventDefault')) {
 				// touch/wheel events should be passive by default
 				handler.modifiers.add('passive');
 			}
diff --git a/src/compile/nodes/EventHandler.ts b/src/compile/nodes/EventHandler.ts
--- a/src/compile/nodes/EventHandler.ts
+++ b/src/compile/nodes/EventHandler.ts
@@ -8,7 +8,8 @@ export default class EventHandler extends Node {
 	modifiers: Set<string>;
 	expression: Expression;
 	handler_name: string;
-	usesContext: boolean;
+	usesContext = false;
+	canMakePassive = false;
 
 	constructor(component: Component, parent, template_scope, info) {
 		super(component, parent, template_scope, info);
@@ -19,6 +20,24 @@ export default class EventHandler extends Node {
 		if (info.expression) {
 			this.expression = new Expression(component, this, template_scope, info.expression);
 			this.usesContext = this.expression.usesContext;
+
+			if (/FunctionExpression/.test(info.expression.type) && info.expression.params.length === 0) {
+				// TODO make this detection more accurate — if `event.preventDefault` isn't called, and
+				// `event` is passed to another function, we can make it passive
+				this.canMakePassive = true;
+			} else if (info.expression.type === 'Identifier') {
+				let node = component.node_for_declaration.get(info.expression.name);
+
+				if (node && node.type === 'VariableDeclaration') {
+					// for `const handleClick = () => {...}`, we want the [arrow] function expression node
+					const declarator = node.declarations.find(d => d.id.name === info.expression.name);
+					node = declarator.init;
+				}
+
+				if (node && /Function/.test(node.type) && node.params.length === 0) {
+					this.canMakePassive = true;
+				}
+			}
 		} else {
 			const name = component.getUniqueName(`${this.name}_handler`);
 			component.declarations.push(name);


Test patch:
diff --git a/test/js/samples/event-handler-no-passive/expected.js b/test/js/samples/event-handler-no-passive/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/event-handler-no-passive/expected.js
@@ -0,0 +1,50 @@
+/* generated by Svelte vX.Y.Z */
+import { SvelteComponent as SvelteComponent_1, addListener, createElement, detachNode, identity, init, insert, noop, run, safe_not_equal } from "svelte/internal";
+
+function create_fragment(component, ctx) {
+	var a, current, dispose;
+
+	return {
+		c() {
+			a = createElement("a");
+			a.textContent = "this should not navigate to example.com";
+			a.href = "https://example.com";
+			dispose = addListener(a, "touchstart", touchstart_handler);
+		},
+
+		m(target, anchor) {
+			insert(target, a, anchor);
+			current = true;
+		},
+
+		p: noop,
+
+		i(target, anchor) {
+			if (current) return;
+			this.m(target, anchor);
+		},
+
+		o: run,
+
+		d(detach) {
+			if (detach) {
+				detachNode(a);
+			}
+
+			dispose();
+		}
+	};
+}
+
+function touchstart_handler(e) {
+	return e.preventDefault();
+}
+
+class SvelteComponent extends SvelteComponent_1 {
+	constructor(options) {
+		super();
+		init(this, options, identity, create_fragment, safe_not_equal);
+	}
+}
+
+export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/event-handler-no-passive/input.html b/test/js/samples/event-handler-no-passive/input.html
new file mode 100644
--- /dev/null
+++ b/test/js/samples/event-handler-no-passive/input.html
@@ -0,0 +1,3 @@
+<a href="https://example.com" on:touchstart="{e => e.preventDefault()}">
+	this should not navigate to example.com
+</a>
\ No newline at end of file
diff --git a/test/js/samples/event-modifiers/input.html b/test/js/samples/event-modifiers/input.html
--- a/test/js/samples/event-modifiers/input.html
+++ b/test/js/samples/event-modifiers/input.html
@@ -8,8 +8,8 @@
 	}
 </script>
 
-<div on:touchstart="{handleTouchstart}">
-	<button on:click|stopPropagation|preventDefault="{handleClick}">click me</button>
-	<button on:click|once|capture="{handleClick}">or me</button>
-	<button on:click|capture="{handleClick}">or me!</button>
+<div on:touchstart={handleTouchstart}>
+	<button on:click|stopPropagation|preventDefault={handleClick}>click me</button>
+	<button on:click|once|capture={handleClick}>or me</button>
+	<button on:click|capture={handleClick}>or me!</button>
 </div>
\ No newline at end of file