Repository: vuejs/vue
Pull request: support v-on passive modifier

- [ ] Bugfix
- [x] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

Changed files:
filename: src/compiler/helpers.js
patch: @@ -1,5 +1,6 @@
 /* @flow */
 
+import { warn } from 'core/util/index'
 import { parseFilters } from './parser/filter-parser'
 
 export function baseWarn (msg: string) {
@@ -41,6 +42,13 @@ export function addHandler (
   modifiers: ?ASTModifiers,
   important: ?boolean
 ) {
+  // warn prevent and passive modifier
+  if (process.env.NODE_ENV !== 'production' && modifiers && modifiers.prevent && modifiers.passive) {
+    warn(
+      'passive and prevent can\'t be used together. ' +
+      'Passive handler can\'t prevent default event.'
+    )
+  }
   // check capture modifier
   if (modifiers && modifiers.capture) {
     delete modifiers.capture
@@ -50,6 +58,10 @@ export function addHandler (
     delete modifiers.once
     name = '~' + name // mark the event as once
   }
+  if (modifiers && modifiers.passive) {
+    delete modifiers.passive
+    name = '&' + name // mark the event as passive
+  }
   let events
   if (modifiers && modifiers.native) {
     delete modifiers.native

filename: src/core/util/env.js
patch: @@ -16,6 +16,17 @@ export const isAndroid = UA && UA.indexOf('android') > 0
 export const isIOS = UA && /iphone|ipad|ipod|ios/.test(UA)
 export const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
 
+export let supportsPassive = false
+if (inBrowser) {
+  try {
+    const opts = {}
+    Object.defineProperty(opts, 'passive', ({
+      get: function () { supportsPassive = true }
+    } : Object)) // https://github.com/facebook/flow/issues/285
+    window.addEventListener('test-passive', null, opts)
+  } catch (e) {}
+}
+
 // this needs to be lazy-evaled because vue may be required before
 // vue-server-renderer can set VUE_ENV
 let _isServer

filename: src/core/vdom/helpers/update-listeners.js
patch: @@ -6,16 +6,20 @@ import { warn } from 'core/util/index'
 const normalizeEvent = cached((name: string): {
   name: string,
   once: boolean,
-  capture: boolean
+  capture: boolean,
+  passive: boolean
 } => {
+  const passive = name.charAt(0) === '&'
+  name = passive ? name.slice(1) : name
   const once = name.charAt(0) === '~' // Prefixed last, checked first
   name = once ? name.slice(1) : name
   const capture = name.charAt(0) === '!'
   name = capture ? name.slice(1) : name
   return {
     name,
     once,
-    capture
+    capture,
+    passive
   }
 })
 
@@ -56,7 +60,7 @@ export function updateListeners (
       if (!cur.fns) {
         cur = on[name] = createFnInvoker(cur)
       }
-      add(event.name, cur, event.once, event.capture)
+      add(event.name, cur, event.once, event.capture, event.passive)
     } else if (cur !== old) {
       old.fns = cur
       on[name] = old

filename: src/platforms/web/runtime/modules/events.js
patch: @@ -1,6 +1,6 @@
 /* @flow */
 
-import { isChrome, isIE } from 'core/util/env'
+import { isChrome, isIE, supportsPassive } from 'core/util/env'
 import { updateListeners } from 'core/vdom/helpers/index'
 import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model'
 
@@ -31,7 +31,8 @@ function add (
   event: string,
   handler: Function,
   once: boolean,
-  capture: boolean
+  capture: boolean,
+  passive: boolean
 ) {
   if (once) {
     const oldHandler = handler
@@ -45,7 +46,7 @@ function add (
       }
     }
   }
-  target.addEventListener(event, handler, capture)
+  target.addEventListener(event, handler, supportsPassive ? { capture, passive } : capture)
 }
 
 function remove (

filename: test/unit/features/directives/on.spec.js
patch: @@ -1,4 +1,5 @@
 import Vue from 'vue'
+import { supportsPassive } from 'core/util/env'
 
 describe('Directive v-on', () => {
   let vm, spy, el
@@ -531,6 +532,38 @@ describe('Directive v-on', () => {
     expect(spyDown.calls.count()).toBe(1)
   })
 
+  // This test case should only run when the test browser supports passive.
+  if (supportsPassive) {
+    it('should support passive', () => {
+      vm = new Vue({
+        el,
+        template: `
+          <div>
+            <input type="checkbox" ref="normal" @click="foo"/>
+            <input type="checkbox" ref="passive" @click.passive="foo"/>
+            <input type="checkbox" ref="exclusive" @click.prevent.passive/>
+          </div>
+        `,
+        methods: {
+          foo (e) {
+            e.preventDefault()
+          }
+        }
+      })
+
+      vm.$refs.normal.checked = false
+      vm.$refs.passive.checked = false
+      vm.$refs.exclusive.checked = false
+      vm.$refs.normal.click()
+      vm.$refs.passive.click()
+      vm.$refs.exclusive.click()
+      expect(vm.$refs.normal.checked).toBe(false)
+      expect(vm.$refs.passive.checked).toBe(true)
+      expect(vm.$refs.exclusive.checked).toBe(true)
+      expect('passive and prevent can\'t be used together. Passive handler can\'t prevent default event.').toHaveBeenWarned()
+    })
+  }
+
   // Github Issues #5146
   it('should only prevent when match keycode', () => {
     let prevented = false