From f05a2e731e139352c09c16867707d2d31b32eb6b Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Wed, 20 Nov 2024 14:01:34 -0800 Subject: [PATCH 1/2] fix(runtime): ensure event listener are not registered twice --- src/runtime/bootstrap-custom-element.ts | 8 ++- .../event-re-register.css | 10 ++++ .../event-re-register.test.tsx | 49 +++++++++++++++++++ .../event-re-register.tsx | 34 +++++++++++++ test/wdio/setup.ts | 5 +- 5 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 test/wdio/event-listener-capture/event-re-register.css create mode 100644 test/wdio/event-listener-capture/event-re-register.test.tsx create mode 100644 test/wdio/event-listener-capture/event-re-register.tsx diff --git a/src/runtime/bootstrap-custom-element.ts b/src/runtime/bootstrap-custom-element.ts index 0f6964e5be2..b9a5cc7186e 100644 --- a/src/runtime/bootstrap-custom-element.ts +++ b/src/runtime/bootstrap-custom-element.ts @@ -67,13 +67,17 @@ export const proxyCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMet const originalConnectedCallback = Cstr.prototype.connectedCallback; const originalDisconnectedCallback = Cstr.prototype.disconnectedCallback; + let hasHostListenerAttached = false Object.assign(Cstr.prototype, { __registerHost() { registerHost(this, cmpMeta); }, connectedCallback() { - const hostRef = getHostRef(this); - addHostEventListeners(this, hostRef, cmpMeta.$listeners$, false); + if (!hasHostListenerAttached) { + const hostRef = getHostRef(this); + addHostEventListeners(this, hostRef, cmpMeta.$listeners$, false); + hasHostListenerAttached = true; + } connectedCallback(this); if (BUILD.connectedCallback && originalConnectedCallback) { diff --git a/test/wdio/event-listener-capture/event-re-register.css b/test/wdio/event-listener-capture/event-re-register.css new file mode 100644 index 00000000000..04dfd663dd3 --- /dev/null +++ b/test/wdio/event-listener-capture/event-re-register.css @@ -0,0 +1,10 @@ +:host { + display: block; + padding: 5px; + background: bisque; + cursor: pointer; + max-width: 300px; +} +:host(:focus) { + outline: 2px solid blue; +} diff --git a/test/wdio/event-listener-capture/event-re-register.test.tsx b/test/wdio/event-listener-capture/event-re-register.test.tsx new file mode 100644 index 00000000000..5e6ddd11ec0 --- /dev/null +++ b/test/wdio/event-listener-capture/event-re-register.test.tsx @@ -0,0 +1,49 @@ +// @ts-expect-error will be resolved by WDIO +import { defineCustomElement } from '/test-components/event-re-register.js'; + +defineCustomElement(); + +describe('event-listener-capture using lazy load components', function () { + const eventListenerCaptureCmp = () => $('event-re-register'); + + afterEach(() => { + const elem = document.querySelector('event-re-register') as HTMLElement; + if (elem) { + elem.remove(); + } + }); + + it('should only attach keydown event listener once', async () => { + const elem = document.createElement('event-re-register') as HTMLElement; + document.body.appendChild(elem); + + const reattach = eventListenerCaptureCmp(); + await expect(reattach).toBePresent(); + + // focus on element + await reattach.click(); + await browser.action('key').down('a').pause(100).up('a').perform(); + await browser.action('key').down('a').pause(100).up('a').perform(); + await browser.action('key').down('a').pause(100).up('a').perform(); + + // check if event fired 3 times + await expect(reattach).toHaveText( + expect.stringContaining('Event fired times: 3')); + + // remove node from DOM + elem.remove(); + + // reattach node to DOM + document.body.appendChild(elem); + + // retrigger event + await reattach.click(); + await browser.action('key').down('a').pause(100).up('a').perform(); + await browser.action('key').down('a').pause(100).up('a').perform(); + await browser.action('key').down('a').pause(100).up('a').perform(); + + // check if event fired 6 times + await expect(reattach).toHaveText( + expect.stringContaining('Event fired times: 6')); + }); +}); diff --git a/test/wdio/event-listener-capture/event-re-register.tsx b/test/wdio/event-listener-capture/event-re-register.tsx new file mode 100644 index 00000000000..6601c813b75 --- /dev/null +++ b/test/wdio/event-listener-capture/event-re-register.tsx @@ -0,0 +1,34 @@ +import { Component, ComponentInterface, h, Host, Listen, State } from '@stencil/core'; + +@Component({ + tag: 'event-re-register', + styleUrl: 'event-re-register.css', + shadow: true, +}) +export class EventReRegister implements ComponentInterface { + @State() eventFiredTimes: number = 0; + @Listen('keydown') + handleKeydown(event: KeyboardEvent) { + this.eventFiredTimes++; + console.log(event); + } + + connectedCallback() { + console.log('connected'); + } + disconnectedCallback() { + console.log('disconnected'); + } + render() { + return + +

Event fired times: {this.eventFiredTimes}

+
; + } +} diff --git a/test/wdio/setup.ts b/test/wdio/setup.ts index fe4500d79b8..7f936432828 100644 --- a/test/wdio/setup.ts +++ b/test/wdio/setup.ts @@ -15,10 +15,11 @@ const testRequiresManualSetup = window.__wdioSpec__.includes('custom-elements-output') || window.__wdioSpec__.includes('global-script') || window.__wdioSpec__.endsWith('custom-tag-name.test.tsx') || - window.__wdioSpec__.endsWith('page-list.test.ts'); + window.__wdioSpec__.endsWith('page-list.test.ts') || + window.__wdioSpec__.endsWith('event-re-register.test.tsx'); /** - * setup all components defined in tests except for those where we want ot manually setup + * setup all components defined in tests except for those where we want to manually setup * the components in the test */ if (!testRequiresManualSetup) { From 1b2c8ed1312ebe641b0734730d990e2893e176d2 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Wed, 20 Nov 2024 14:10:48 -0800 Subject: [PATCH 2/2] prettier --- src/runtime/bootstrap-custom-element.ts | 2 +- .../event-re-register.test.tsx | 6 ++--- .../event-re-register.tsx | 22 ++++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/runtime/bootstrap-custom-element.ts b/src/runtime/bootstrap-custom-element.ts index b9a5cc7186e..4db002b21a2 100644 --- a/src/runtime/bootstrap-custom-element.ts +++ b/src/runtime/bootstrap-custom-element.ts @@ -67,7 +67,7 @@ export const proxyCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMet const originalConnectedCallback = Cstr.prototype.connectedCallback; const originalDisconnectedCallback = Cstr.prototype.disconnectedCallback; - let hasHostListenerAttached = false + let hasHostListenerAttached = false; Object.assign(Cstr.prototype, { __registerHost() { registerHost(this, cmpMeta); diff --git a/test/wdio/event-listener-capture/event-re-register.test.tsx b/test/wdio/event-listener-capture/event-re-register.test.tsx index 5e6ddd11ec0..56ca474c4c7 100644 --- a/test/wdio/event-listener-capture/event-re-register.test.tsx +++ b/test/wdio/event-listener-capture/event-re-register.test.tsx @@ -27,8 +27,7 @@ describe('event-listener-capture using lazy load components', function () { await browser.action('key').down('a').pause(100).up('a').perform(); // check if event fired 3 times - await expect(reattach).toHaveText( - expect.stringContaining('Event fired times: 3')); + await expect(reattach).toHaveText(expect.stringContaining('Event fired times: 3')); // remove node from DOM elem.remove(); @@ -43,7 +42,6 @@ describe('event-listener-capture using lazy load components', function () { await browser.action('key').down('a').pause(100).up('a').perform(); // check if event fired 6 times - await expect(reattach).toHaveText( - expect.stringContaining('Event fired times: 6')); + await expect(reattach).toHaveText(expect.stringContaining('Event fired times: 6')); }); }); diff --git a/test/wdio/event-listener-capture/event-re-register.tsx b/test/wdio/event-listener-capture/event-re-register.tsx index 6601c813b75..20429e1ba45 100644 --- a/test/wdio/event-listener-capture/event-re-register.tsx +++ b/test/wdio/event-listener-capture/event-re-register.tsx @@ -20,15 +20,17 @@ export class EventReRegister implements ComponentInterface { console.log('disconnected'); } render() { - return -
    -
  • Focus this component;
  • -
  • Press key;
  • -
  • See console output
  • -
  • Press 'Reconnect' button
  • -
  • Repeat steps 1-3
  • -
-

Event fired times: {this.eventFiredTimes}

-
; + return ( + +
    +
  • Focus this component;
  • +
  • Press key;
  • +
  • See console output
  • +
  • Press 'Reconnect' button
  • +
  • Repeat steps 1-3
  • +
+

Event fired times: {this.eventFiredTimes}

+
+ ); } }