Skip to content

Commit

Permalink
fix(runtime): support "capture" style events (#4968)
Browse files Browse the repository at this point in the history
* fix(runtime): handle capture style events

* add some more comments for context

* tests are good to have

* forgot a test for removing event listeners

* PR feedback
  • Loading branch information
tanner-reits committed Oct 27, 2023
1 parent f113b05 commit 2c8cfac
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 5 deletions.
21 changes: 16 additions & 5 deletions src/runtime/vdom/set-accessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,20 @@ export const setAccessor = (
// except for the first character, we keep the event name case
memberName = ln[2] + memberName.slice(3);
}
if (oldValue) {
plt.rel(elm, memberName, oldValue, false);
}
if (newValue) {
plt.ael(elm, memberName, newValue, false);
if (oldValue || newValue) {
// Need to account for "capture" events.
// If the event name ends with "Capture", we'll update the name to remove
// the "Capture" suffix and make sure the event listener is setup to handle the capture event.
const capture = memberName.endsWith(CAPTURE_EVENT_SUFFIX);
// Make sure we only replace the last instance of "Capture"
memberName = memberName.replace(CAPTURE_EVENT_REGEX, '');

if (oldValue) {
plt.rel(elm, memberName, oldValue, capture);
}
if (newValue) {
plt.ael(elm, memberName, newValue, capture);
}
}
} else if (BUILD.vdomPropOrAttr) {
// Set property if it exists and it's not a SVG
Expand Down Expand Up @@ -170,3 +179,5 @@ export const setAccessor = (
};
const parseClassListRegex = /\s/;
const parseClassList = (value: string | undefined | null): string[] => (!value ? [] : value.split(parseClassListRegex));
const CAPTURE_EVENT_SUFFIX = 'Capture';
const CAPTURE_EVENT_REGEX = new RegExp(CAPTURE_EVENT_SUFFIX + '$');
30 changes: 30 additions & 0 deletions src/runtime/vdom/test/set-accessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,36 @@ describe('setAccessor for custom elements', () => {
expect(addEventSpy).toHaveBeenCalledWith('click', newValue, false);
expect(removeEventSpy).not.toHaveBeenCalled();
});

it('should add a capture style event listener', () => {
const addEventSpy = jest.spyOn(elm, 'addEventListener');
const removeEventSpy = jest.spyOn(elm, 'removeEventListener');

const newValue = () => {
/**/
};

setAccessor(elm, 'onClickCapture', undefined, newValue, false, 0);

expect(addEventSpy).toHaveBeenCalledWith('click', newValue, true);
expect(removeEventSpy).not.toHaveBeenCalled();
});

it('should remove a capture style event listener', () => {
const addEventSpy = jest.spyOn(elm, 'addEventListener');
const removeEventSpy = jest.spyOn(elm, 'removeEventListener');

const orgValue = () => {
/**/
};

setAccessor(elm, 'onClickCapture', undefined, orgValue, false, 0);
setAccessor(elm, 'onClickCapture', orgValue, undefined, false, 0);

expect(addEventSpy).toHaveBeenCalledTimes(1);
expect(addEventSpy).toHaveBeenCalledWith('click', orgValue, true);
expect(removeEventSpy).toHaveBeenCalledWith('click', orgValue, true);
});
});

it('should set object property to child', () => {
Expand Down
13 changes: 13 additions & 0 deletions test/karma/test-app/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export namespace Components {
}
interface EventCustomType {
}
interface EventListenerCapture {
}
interface ExternalImportA {
}
interface ExternalImportB {
Expand Down Expand Up @@ -705,6 +707,12 @@ declare global {
prototype: HTMLEventCustomTypeElement;
new (): HTMLEventCustomTypeElement;
};
interface HTMLEventListenerCaptureElement extends Components.EventListenerCapture, HTMLStencilElement {
}
var HTMLEventListenerCaptureElement: {
prototype: HTMLEventListenerCaptureElement;
new (): HTMLEventListenerCaptureElement;
};
interface HTMLExternalImportAElement extends Components.ExternalImportA, HTMLStencilElement {
}
var HTMLExternalImportAElement: {
Expand Down Expand Up @@ -1422,6 +1430,7 @@ declare global {
"esm-import": HTMLEsmImportElement;
"event-basic": HTMLEventBasicElement;
"event-custom-type": HTMLEventCustomTypeElement;
"event-listener-capture": HTMLEventListenerCaptureElement;
"external-import-a": HTMLExternalImportAElement;
"external-import-b": HTMLExternalImportBElement;
"external-import-c": HTMLExternalImportCElement;
Expand Down Expand Up @@ -1652,6 +1661,8 @@ declare namespace LocalJSX {
interface EventCustomType {
"onTestEvent"?: (event: EventCustomTypeCustomEvent<TestEventDetail>) => void;
}
interface EventListenerCapture {
}
interface ExternalImportA {
}
interface ExternalImportB {
Expand Down Expand Up @@ -1948,6 +1959,7 @@ declare namespace LocalJSX {
"esm-import": EsmImport;
"event-basic": EventBasic;
"event-custom-type": EventCustomType;
"event-listener-capture": EventListenerCapture;
"external-import-a": ExternalImportA;
"external-import-b": ExternalImportB;
"external-import-c": ExternalImportC;
Expand Down Expand Up @@ -2102,6 +2114,7 @@ declare module "@stencil/core" {
"esm-import": LocalJSX.EsmImport & JSXBase.HTMLAttributes<HTMLEsmImportElement>;
"event-basic": LocalJSX.EventBasic & JSXBase.HTMLAttributes<HTMLEventBasicElement>;
"event-custom-type": LocalJSX.EventCustomType & JSXBase.HTMLAttributes<HTMLEventCustomTypeElement>;
"event-listener-capture": LocalJSX.EventListenerCapture & JSXBase.HTMLAttributes<HTMLEventListenerCaptureElement>;
"external-import-a": LocalJSX.ExternalImportA & JSXBase.HTMLAttributes<HTMLExternalImportAElement>;
"external-import-b": LocalJSX.ExternalImportB & JSXBase.HTMLAttributes<HTMLExternalImportBElement>;
"external-import-c": LocalJSX.ExternalImportC & JSXBase.HTMLAttributes<HTMLExternalImportCElement>;
Expand Down
21 changes: 21 additions & 0 deletions test/karma/test-app/event-listener-capture/cmp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Component, h, State } from '@stencil/core';

@Component({
tag: 'event-listener-capture',
})
export class EventListenerCapture {
@State() counter = 0;

render() {
return (
<div>
<p>Click the text below to trigger a capture style event</p>
<div>
<p id="incrementer" onClickCapture={() => this.counter++}>
Clicked: <span id="counter">{this.counter}</span> time(s)
</p>
</div>
</div>
);
}
}
6 changes: 6 additions & 0 deletions test/karma/test-app/event-listener-capture/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!doctype html>
<meta charset="utf8" />
<script src="./build/testapp.esm.js" type="module"></script>
<script src="./build/testapp.js" nomodule></script>

<event-listener-capture></event-listener-capture>
31 changes: 31 additions & 0 deletions test/karma/test-app/event-listener-capture/karma.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { setupDomTests, waitForChanges } from '../util';

describe('event-listener-capture', function () {
const { setupDom, tearDownDom } = setupDomTests(document);

let app: HTMLElement | undefined;
let host: HTMLElement | undefined;

beforeEach(async () => {
app = await setupDom('/event-listener-capture/index.html');
host = app.querySelector('event-listener-capture');
});

afterEach(tearDownDom);

it('should render', () => {
expect(host).toBeDefined();
});

it('should increment counter on click', async () => {
const counter = host.querySelector('#counter');
expect(counter.textContent).toBe('0');

const p = host.querySelector('#incrementer') as HTMLParagraphElement;
expect(p).toBeDefined();
p.click();
await waitForChanges();

expect(counter.textContent).toBe('1');
});
});

0 comments on commit 2c8cfac

Please sign in to comment.