feat(controller): add redispatchEvent helper

PiperOrigin-RevId: 419005706
This commit is contained in:
Liz Mitchell 2021-12-30 14:16:11 -08:00 committed by Copybara-Service
parent e817ace1c6
commit b258e15219
2 changed files with 151 additions and 0 deletions

View File

@ -0,0 +1,39 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Re-dispatches an event from the `EventTarget` this function is bound to.
*
* This function is useful for forwarding non-composed events, such as `change`
* events.
*
* @example
* class MyInput extends LitElement {
* render() {
* return html`<input @change=${this.redispatchEvent}>`;
* }
*
* protected redispatchEvent = redispatchEvent.bind(this);
* }
*
* @param event The event to re-dispatch.
* @return Whether or not the event was dispatched (if cancelable).
*/
export function redispatchEvent(this: Element, event: Event) {
// For bubbling events in SSR light DOM (or composed), stop their propagation
// and dispatch the copy.
if (event.bubbles && (!this.shadowRoot || event.composed)) {
event.stopPropagation();
}
const copy = Reflect.construct(event.constructor, [event.type, event]);
const dispatched = this.dispatchEvent(copy);
if (!dispatched) {
event.preventDefault();
}
return dispatched;
}

View File

@ -0,0 +1,112 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import 'jasmine';
import {redispatchEvent} from '../events';
describe('redispatchEvent()', () => {
let instance: HTMLDivElement;
beforeEach(() => {
instance = document.createElement('div');
instance.attachShadow({mode: 'open'});
// To have event.target set correctly, the EventTarget instance must be
// attached to the DOM.
document.body.appendChild(instance);
});
afterEach(() => {
document.body.removeChild(instance);
});
it('should re-dispatch events', () => {
const event = new Event('foo', {composed: false, bubbles: true});
const fooHandler = jasmine.createSpy('fooHandler');
instance.addEventListener('foo', fooHandler);
redispatchEvent.bind(instance)(event);
expect(fooHandler).toHaveBeenCalled();
const redispatchedEvent = fooHandler.calls.first().args[0] as Event;
expect(redispatchedEvent)
.withContext('redispatched event should be a new instance')
.not.toBe(event);
expect(redispatchedEvent.target)
.withContext(
'target should be the instance that redispatched the event')
.toBe(instance);
expect(redispatchedEvent.type)
.withContext('should be the same event type')
.toBe(event.type);
expect(redispatchedEvent.composed)
.withContext('should not be composed')
.toBeFalse();
expect(redispatchedEvent.bubbles)
.withContext('should keep other flags set to true')
.toBeTrue();
});
it('should not dispatch multiple events if bubbling and composed', () => {
const event = new Event('foo', {composed: true, bubbles: true});
const fooHandler = jasmine.createSpy('fooHandler');
instance.addEventListener('foo', fooHandler);
redispatchEvent.bind(instance)(event);
expect(fooHandler).toHaveBeenCalledTimes(1);
});
it('should not dispatch multiple events if bubbling in light DOM', () => {
const lightDomInstance = document.createElement('div');
try {
document.body.appendChild(lightDomInstance);
const event = new Event('foo', {composed: true, bubbles: true});
const fooHandler = jasmine.createSpy('fooHandler');
instance.addEventListener('foo', fooHandler);
redispatchEvent.bind(instance)(event);
expect(fooHandler).toHaveBeenCalledTimes(1);
} finally {
document.body.removeChild(lightDomInstance);
}
});
it('should preventDefault() on the original event if canceled', () => {
const event = new Event('foo', {cancelable: true});
const fooHandler =
jasmine.createSpy('fooHandler').and.callFake((event: Event) => {
event.preventDefault();
});
instance.addEventListener('foo', fooHandler);
const result = redispatchEvent.bind(instance)(event);
expect(result)
.withContext('should return false since event was canceled')
.toBeFalse();
expect(fooHandler).toHaveBeenCalled();
const redispatchedEvent = fooHandler.calls.first().args[0] as Event;
expect(redispatchedEvent.defaultPrevented)
.withContext('redispatched event should be canceled by handler')
.toBeTrue();
expect(event.defaultPrevented)
.withContext('original event should be canceled')
.toBeTrue();
});
it('should preserve event instance types', () => {
const event = new CustomEvent('foo', {detail: 'bar'});
const fooHandler = jasmine.createSpy('fooHandler');
instance.addEventListener('foo', fooHandler);
redispatchEvent.bind(instance)(event);
expect(fooHandler).toHaveBeenCalled();
const redispatchedEvent = fooHandler.calls.first().args[0] as CustomEvent;
expect(redispatchedEvent)
.withContext('should create the same instance type')
.toBeInstanceOf(CustomEvent);
expect(redispatchedEvent.detail)
.withContext('should copy event type-specific properties')
.toBe('bar');
});
});