diff --git a/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js b/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js index 7673295ac58..094b0ff991b 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js @@ -296,6 +296,84 @@ moduleFor( this.assertStableRerender(); this.assertCounts({ adds: 2, removes: 0 }); } + + "@test listeners from the invocation run before the element's own listeners, wherever ...attributes is (GH#17877)"( + assert + ) { + let calls = []; + + this.owner.register( + 'component:before-splat', + setComponentTemplate( + precompileTemplate(``), + class extends Component { + inner = () => calls.push('inner'); + } + ) + ); + + this.owner.register( + 'component:after-splat', + setComponentTemplate( + precompileTemplate(``), + class extends Component { + inner = () => calls.push('inner'); + } + ) + ); + + this.render( + ``, + { outer: () => calls.push('outer') } + ); + + runTask(() => this.$('#before').click()); + assert.deepEqual(calls, ['outer', 'inner'], 'own modifier before ...attributes'); + + calls = []; + runTask(() => this.$('#after').click()); + assert.deepEqual(calls, ['outer', 'inner'], 'own modifier after ...attributes'); + } + + "@test listeners on ancestors in a component run after the caller's listeners, unless propagation is stopped (GH#17877)"( + assert + ) { + let calls = []; + + this.owner.register( + 'component:wrapped-button', + setComponentTemplate( + precompileTemplate( + `
` + ), + class extends Component { + inner = () => calls.push('inner'); + wrapper = () => calls.push('wrapper'); + } + ) + ); + + let stop = false; + + this.render(``, { + outer: (event) => { + calls.push('outer'); + if (stop) event.stopPropagation(); + }, + }); + + runTask(() => this.$('button').click()); + assert.deepEqual(calls, ['outer', 'inner', 'wrapper'], 'the ancestor listener runs last'); + + calls = []; + stop = true; + runTask(() => this.$('button').click()); + assert.deepEqual( + calls, + ['outer', 'inner'], + "stopping propagation in the caller's listener prevents the ancestor listener" + ); + } } ); diff --git a/packages/@glimmer/runtime/lib/modifiers/on.ts b/packages/@glimmer/runtime/lib/modifiers/on.ts index 51b4f48d00a..e8c38554895 100644 --- a/packages/@glimmer/runtime/lib/modifiers/on.ts +++ b/packages/@glimmer/runtime/lib/modifiers/on.ts @@ -313,6 +313,49 @@ function addEventListener( In this case, the `saveLike` function will receive two arguments: the click event and the value of `@post`. + ### Listener Order + + Listeners added with `{{on}}` are regular DOM event listeners, so they run in + the order they were added to an element, and an event reaches listeners on the + target element before it bubbles up to listeners on its ancestors. + + When a component forwards modifiers to an element with `...attributes`, the + `{{on}}` listeners passed in by the caller run before the listeners that the + component puts on that element itself. This is true wherever `...attributes` + appears in the element's attribute list: + + ```gjs {data-filename="app/components/my-button.gjs"} + + ``` + + ```gjs + + ``` + + This order is set when the listeners are first added. When the arguments to an + `{{on}}` change, for example because a different callback is passed in, that + listener is removed and added again, so it moves after the other listeners on + the element. + + If a component needs its own handler to run first, for example to decide + whether the caller's handler should run, have the caller pass the handler as + an argument and call it from the component's handler instead of passing it + with `{{on}}`. + + Calling `event.stopPropagation()` in any of these listeners stops the event + from reaching listeners on ancestor elements, including ones the component + put on a wrapping element in its own template. It does not stop the other + listeners on the same element. + ### Function Context In the example above, we used `@action` to ensure that `likePost` is