mirror of
https://github.com/material-components/material-web.git
synced 2024-10-27 22:17:25 +03:00
feat(controller): add redispatchEvent helper
PiperOrigin-RevId: 419005706
This commit is contained in:
parent
e817ace1c6
commit
b258e15219
39
components/controller/events.ts
Normal file
39
components/controller/events.ts
Normal 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;
|
||||
}
|
112
components/controller/test/events.test.ts
Normal file
112
components/controller/test/events.test.ts
Normal 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');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user