mirror of
https://github.com/material-components/material-web.git
synced 2026-03-09 00:09:23 +08:00
137 lines
4.2 KiB
TypeScript
137 lines
4.2 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2021 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* Re-dispatches an event from the provided element.
|
|
*
|
|
* 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(event: Event) {
|
|
* redispatchEvent(this, event);
|
|
* }
|
|
* }
|
|
*
|
|
* @param element The element to dispatch the event from.
|
|
* @param event The event to re-dispatch.
|
|
* @return Whether or not the event was dispatched (if cancelable).
|
|
*/
|
|
export function redispatchEvent(element: Element, event: Event) {
|
|
// For bubbling events in SSR light DOM (or composed), stop their propagation
|
|
// and dispatch the copy.
|
|
if (event.bubbles && (!element.shadowRoot || event.composed)) {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
const copy = Reflect.construct(event.constructor, [event.type, event]);
|
|
const dispatched = element.dispatchEvent(copy);
|
|
if (!dispatched) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
return dispatched;
|
|
}
|
|
|
|
/**
|
|
* Dispatches a click event to the given element that triggers a native action,
|
|
* but is not composed and therefore is not seen outside the element.
|
|
*
|
|
* This is useful for responding to an external click event on the host element
|
|
* that should trigger an internal action like a button click.
|
|
*
|
|
* Note, a helper is provided because setting this up correctly is a bit tricky.
|
|
* In particular, calling `click` on an element creates a composed event, which
|
|
* is not desirable, and a manually dispatched event must specifically be a
|
|
* `MouseEvent` to trigger a native action.
|
|
*
|
|
* @example
|
|
* hostClickListener = (event: MouseEvent) {
|
|
* if (isActivationClick(event)) {
|
|
* this.dispatchActivationClick(this.buttonElement);
|
|
* }
|
|
* }
|
|
*
|
|
*/
|
|
export function dispatchActivationClick(element: HTMLElement) {
|
|
const event = new MouseEvent('click', {bubbles: true});
|
|
element.dispatchEvent(event);
|
|
return event;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the click event should trigger an activation behavior. The
|
|
* behavior is defined by the element and is whatever it should do when
|
|
* clicked.
|
|
*
|
|
* Typically when an element needs to handle a click, the click is generated
|
|
* from within the element and an event listener within the element implements
|
|
* the needed behavior; however, it's possible to fire a click directly
|
|
* at the element that the element should handle. This method helps
|
|
* distinguish these "external" clicks.
|
|
*
|
|
* An "external" click can be triggered in a number of ways: via a click
|
|
* on an associated label for a form associated element, calling
|
|
* `element.click()`, or calling
|
|
* `element.dispatchEvent(new MouseEvent('click', ...))`.
|
|
*
|
|
* Also works around Firefox issue
|
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=1804576 by squelching
|
|
* events for a microtask after called.
|
|
*
|
|
* @example
|
|
* hostClickListener = (event: MouseEvent) {
|
|
* if (isActivationClick(event)) {
|
|
* this.dispatchActivationClick(this.buttonElement);
|
|
* }
|
|
* }
|
|
*
|
|
*/
|
|
export function isActivationClick(event: Event) {
|
|
// Event must start at the event target.
|
|
if (event.currentTarget !== event.target) {
|
|
return false;
|
|
}
|
|
// Event must not be retargeted from shadowRoot.
|
|
if (event.composedPath()[0] !== event.target) {
|
|
return false;
|
|
}
|
|
// Target must not be disabled; this should only occur for a synthetically
|
|
// dispatched click.
|
|
if ((event.target as EventTarget & {disabled: boolean}).disabled) {
|
|
return false;
|
|
}
|
|
// This is an activation if the event should not be squelched.
|
|
return !squelchEvent(event);
|
|
}
|
|
|
|
// TODO(https://bugzilla.mozilla.org/show_bug.cgi?id=1804576)
|
|
// Remove when Firefox bug is addressed.
|
|
function squelchEvent(event: Event) {
|
|
const squelched = isSquelchingEvents;
|
|
if (squelched) {
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
}
|
|
squelchEventsForMicrotask();
|
|
return squelched;
|
|
}
|
|
|
|
// Ignore events for one microtask only.
|
|
let isSquelchingEvents = false;
|
|
async function squelchEventsForMicrotask() {
|
|
isSquelchingEvents = true;
|
|
// Need to pause for just one microtask.
|
|
// tslint:disable-next-line
|
|
await null;
|
|
isSquelchingEvents = false;
|
|
}
|