Explorar el Código

fix(compiler-vapor): avoid delegating same-event handlers when sibling uses `stop` modifiers (#14610)

close #14609
edison hace 1 mes
padre
commit
bf7a066625

+ 29 - 18
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap

@@ -116,9 +116,9 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
   const n19 = t3()
   const n20 = t3()
   const n21 = t3()
-  n0.$evtclick = _createInvoker(_withModifiers(_ctx.handleEvent, ["stop"]))
+  _on(n0, "click", _createInvoker(_withModifiers(_ctx.handleEvent, ["stop"])))
   _on(n1, "submit", _createInvoker(_withModifiers(_ctx.handleEvent, ["prevent"])))
-  n2.$evtclick = _createInvoker(_withModifiers(_ctx.handleEvent, ["stop","prevent"]))
+  _on(n2, "click", _createInvoker(_withModifiers(_ctx.handleEvent, ["stop","prevent"])))
   n3.$evtclick = _createInvoker(_withModifiers(_ctx.handleEvent, ["self"]))
   _on(n4, "click", _createInvoker(_ctx.handleEvent), {
     capture: true
@@ -299,6 +299,30 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`v-on > should not delegate .stop when have multiple events of same name 1`] = `
+"import { createInvoker as _createInvoker, on as _on, withModifiers as _withModifiers, template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _on(n0, "click", _createInvoker(e => _ctx.test(e)))
+  _on(n0, "click", _createInvoker(_withModifiers(e => _ctx.test(e), ["stop"])))
+  return n0
+}"
+`;
+
+exports[`v-on > should not delegate normalized static event when sibling uses .stop 1`] = `
+"import { createInvoker as _createInvoker, withModifiers as _withModifiers, on as _on, template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _on(n0, "contextmenu", _createInvoker(_withModifiers(e => _ctx.test(e), ["right"])))
+  _on(n0, "contextmenu", _createInvoker(_withModifiers(e => _ctx.test(e), ["stop"])))
+  return n0
+}"
+`;
+
 exports[`v-on > should not prefix member expression 1`] = `
 "import { createInvoker as _createInvoker, delegateEvents as _delegateEvents, template as _template } from 'vue';
 const t0 = _template("<div>", true)
@@ -324,13 +348,13 @@ export function render(_ctx) {
 `;
 
 exports[`v-on > should support multiple events and modifiers options w/ prefixIdentifiers: true 1`] = `
-"import { createInvoker as _createInvoker, withModifiers as _withModifiers, withKeys as _withKeys, delegateEvents as _delegateEvents, template as _template } from 'vue';
+"import { createInvoker as _createInvoker, withModifiers as _withModifiers, on as _on, withKeys as _withKeys, delegateEvents as _delegateEvents, template as _template } from 'vue';
 const t0 = _template("<div>", true)
-_delegateEvents("click", "keyup")
+_delegateEvents("keyup")
 
 export function render(_ctx) {
   const n0 = t0()
-  n0.$evtclick = _createInvoker(_withModifiers(e => _ctx.test(e), ["stop"]))
+  _on(n0, "click", _createInvoker(_withModifiers(e => _ctx.test(e), ["stop"])))
   n0.$evtkeyup = _createInvoker(_withKeys(e => _ctx.test(e), ["enter"]))
   return n0
 }"
@@ -406,19 +430,6 @@ export function render(_ctx) {
 }"
 `;
 
-exports[`v-on > should use delegate helper when have multiple events of same name 1`] = `
-"import { createInvoker as _createInvoker, delegate as _delegate, withModifiers as _withModifiers, delegateEvents as _delegateEvents, template as _template } from 'vue';
-const t0 = _template("<div>", true)
-_delegateEvents("click")
-
-export function render(_ctx) {
-  const n0 = t0()
-  _delegate(n0, "click", _createInvoker(e => _ctx.test(e)))
-  _delegate(n0, "click", _createInvoker(_withModifiers(e => _ctx.test(e), ["stop"])))
-  return n0
-}"
-`;
-
 exports[`v-on > should wrap as function if expression is inline statement 1`] = `
 "import { createInvoker as _createInvoker, delegateEvents as _delegateEvents, template as _template } from 'vue';
 const t0 = _template("<div>", true)

+ 22 - 5
packages/compiler-vapor/__tests__/transforms/vOn.spec.ts

@@ -463,6 +463,7 @@ describe('v-on', () => {
           nonKeys: ['stop'],
           options: [],
         },
+        delegate: false,
       },
       {
         type: IRNodeTypes.SET_EVENT,
@@ -481,12 +482,13 @@ describe('v-on', () => {
           nonKeys: [],
           options: [],
         },
+        delegate: true,
       },
     ])
 
     expect(code).matchSnapshot()
     expect(code).contains(
-      `n0.$evtclick = _createInvoker(_withModifiers(e => _ctx.test(e), ["stop"]))
+      `_on(n0, "click", _createInvoker(_withModifiers(e => _ctx.test(e), ["stop"])))
   n0.$evtkeyup = _createInvoker(_withKeys(e => _ctx.test(e), ["enter"]))`,
     )
   })
@@ -687,17 +689,32 @@ describe('v-on', () => {
     ])
   })
 
-  test('should use delegate helper when have multiple events of same name', () => {
+  test('should not delegate .stop when have multiple events of same name', () => {
     const { code, helpers } = compileWithVOn(
       `<div @click="test" @click.stop="test" />`,
     )
-    expect(helpers).contains('delegate')
+    expect(helpers).not.contains('delegate')
+    expect(helpers).not.contains('delegateEvents')
+    expect(code).toMatchSnapshot()
+    expect(code).contains('_on(n0, "click", _createInvoker(e => _ctx.test(e)))')
+    expect(code).contains(
+      '_on(n0, "click", _createInvoker(_withModifiers(e => _ctx.test(e), ["stop"])))',
+    )
+  })
+
+  test('should not delegate normalized static event when sibling uses .stop', () => {
+    const { code, helpers } = compileWithVOn(
+      `<div @click.right="test" @contextmenu.stop="test" />`,
+    )
+
+    expect(helpers).not.contains('delegate')
+    expect(helpers).not.contains('delegateEvents')
     expect(code).toMatchSnapshot()
     expect(code).contains(
-      '_delegate(n0, "click", _createInvoker(e => _ctx.test(e)))',
+      '_on(n0, "contextmenu", _createInvoker(_withModifiers(e => _ctx.test(e), ["right"])))',
     )
     expect(code).contains(
-      '_delegate(n0, "click", _createInvoker(_withModifiers(e => _ctx.test(e), ["stop"])))',
+      '_on(n0, "contextmenu", _createInvoker(_withModifiers(e => _ctx.test(e), ["stop"])))',
     )
   })
 

+ 59 - 8
packages/compiler-vapor/src/transforms/vOn.ts

@@ -1,13 +1,16 @@
 import {
+  type ElementNode,
   ElementTypes,
   ErrorCodes,
+  NodeTypes,
+  type SimpleExpressionNode,
   createCompilerError,
   isKeyboardEvent,
   isStaticExp,
+  resolveModifiers,
 } from '@vue/compiler-dom'
 import type { DirectiveTransform } from '../transform'
 import { IRNodeTypes, type KeyOverride, type SetEventIRNode } from '../ir'
-import { resolveModifiers } from '@vue/compiler-dom'
 import { extend, makeMap } from '@vue/shared'
 import { resolveExpression } from '../utils'
 import { EMPTY_EXPRESSION } from './utils'
@@ -47,19 +50,16 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => {
     if (keyOverride) {
       // TODO error here
     }
-    if (isStaticClick) {
-      arg = extend({}, arg, { content: 'mouseup' })
-    } else if (!arg.isStatic) {
+    if (!isStaticClick && !arg.isStatic) {
       keyOverride = ['click', 'mouseup']
     }
   }
   if (nonKeyModifiers.includes('right')) {
-    if (isStaticClick) {
-      arg = extend({}, arg, { content: 'contextmenu' })
-    } else if (!arg.isStatic) {
+    if (!isStaticClick && !arg.isStatic) {
       keyOverride = ['click', 'contextmenu']
     }
   }
+  arg = normalizeStaticEventArg(arg, nonKeyModifiers)
 
   // don't gen keys guard for non-keyboard events
   // if event name is dynamic, always wrap with keys guard
@@ -88,9 +88,13 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => {
   // Only delegate if:
   // - no dynamic event name
   // - no event option modifiers (passive, capture, once)
+  // - no handlers for the same static event on this element that use .stop
   // - is a delegatable event
   const delegate =
-    arg.isStatic && !eventOptionModifiers.length && delegatedEvents(arg.content)
+    arg.isStatic &&
+    !eventOptionModifiers.length &&
+    !hasStopHandlerForStaticEvent(node, arg.content) &&
+    delegatedEvents(arg.content)
 
   const operation: SetEventIRNode = {
     type: IRNodeTypes.SET_EVENT,
@@ -109,3 +113,50 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => {
 
   context.registerEffect([arg], operation)
 }
+
+function normalizeStaticEventArg(
+  arg: SimpleExpressionNode,
+  nonKeyModifiers: string[],
+): SimpleExpressionNode {
+  if (!arg.isStatic) return arg
+
+  let normalized = arg
+  const isStaticClick = arg.content.toLowerCase() === 'click'
+
+  if (nonKeyModifiers.includes('middle') && isStaticClick) {
+    normalized = extend({}, normalized, { content: 'mouseup' })
+  }
+  if (nonKeyModifiers.includes('right') && isStaticClick) {
+    normalized = extend({}, normalized, { content: 'contextmenu' })
+  }
+
+  return normalized
+}
+
+function hasStopHandlerForStaticEvent(node: ElementNode, eventName: string) {
+  return node.props.some(prop => {
+    if (
+      prop.type !== NodeTypes.DIRECTIVE ||
+      prop.name !== 'on' ||
+      !prop.arg ||
+      prop.arg.type !== NodeTypes.SIMPLE_EXPRESSION
+    ) {
+      return false
+    }
+
+    const arg = resolveExpression(prop.arg)
+    if (!arg.isStatic) return false
+
+    const { nonKeyModifiers } = resolveModifiers(
+      `on${arg.content}`,
+      prop.modifiers,
+      null,
+      prop.loc,
+    )
+
+    return (
+      nonKeyModifiers.includes('stop') &&
+      normalizeStaticEventArg(arg, nonKeyModifiers).content === eventName
+    )
+  })
+}