mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(core): Remove global event delegation code. (#57893)
This is no longer needed since we are no longer experimenting with it. PR Close #57893
This commit is contained in:
parent
e40a4fa3c7
commit
2e7cfcb2ef
19 changed files with 5 additions and 588 deletions
|
|
@ -72,11 +72,7 @@ export {
|
|||
withI18nSupport as ɵwithI18nSupport,
|
||||
} from './hydration/api';
|
||||
export {withEventReplay as ɵwithEventReplay} from './hydration/event_replay';
|
||||
export {
|
||||
GLOBAL_EVENT_DELEGATION as ɵGLOBAL_EVENT_DELEGATION,
|
||||
JSACTION_EVENT_CONTRACT as ɵJSACTION_EVENT_CONTRACT,
|
||||
} from './event_delegation_utils';
|
||||
export {provideGlobalEventDelegation as ɵprovideGlobalEventDelegation} from './event_dispatch/event_delegation';
|
||||
export {JSACTION_EVENT_CONTRACT as ɵJSACTION_EVENT_CONTRACT} from './event_delegation_utils';
|
||||
export {IS_HYDRATION_DOM_REUSE_ENABLED as ɵIS_HYDRATION_DOM_REUSE_ENABLED} from './hydration/tokens';
|
||||
export {
|
||||
HydratedNode as ɵHydratedNode,
|
||||
|
|
|
|||
|
|
@ -7,19 +7,10 @@
|
|||
*/
|
||||
|
||||
// tslint:disable:no-duplicate-imports
|
||||
import {
|
||||
EventContract,
|
||||
EventContractContainer,
|
||||
EventDispatcher,
|
||||
isEarlyEventType,
|
||||
getActionCache,
|
||||
registerDispatcher,
|
||||
} from '@angular/core/primitives/event-dispatch';
|
||||
import {EventContract} from '@angular/core/primitives/event-dispatch';
|
||||
import {Attribute} from '@angular/core/primitives/event-dispatch';
|
||||
import {Injectable, InjectionToken, Injector, inject} from './di';
|
||||
import {InjectionToken} from './di';
|
||||
import {RElement} from './render3/interfaces/renderer_dom';
|
||||
import {EVENT_REPLAY_ENABLED_DEFAULT, IS_EVENT_REPLAY_ENABLED} from './hydration/tokens';
|
||||
import {OnDestroy} from './interface/lifecycle_hooks';
|
||||
|
||||
declare global {
|
||||
interface Element {
|
||||
|
|
@ -71,60 +62,3 @@ export const JSACTION_EVENT_CONTRACT = new InjectionToken<EventContractDetails>(
|
|||
factory: () => ({}),
|
||||
},
|
||||
);
|
||||
|
||||
export const GLOBAL_EVENT_DELEGATION = new InjectionToken<GlobalEventDelegation>(
|
||||
ngDevMode ? 'GLOBAL_EVENT_DELEGATION' : '',
|
||||
);
|
||||
|
||||
/**
|
||||
* This class is the delegate for `EventDelegationPlugin`. It represents the
|
||||
* noop version of this class, with the enabled version set when
|
||||
* `provideGlobalEventDelegation` is called.
|
||||
*/
|
||||
@Injectable()
|
||||
export class GlobalEventDelegation implements OnDestroy {
|
||||
private eventContractDetails = inject(JSACTION_EVENT_CONTRACT);
|
||||
|
||||
ngOnDestroy() {
|
||||
this.eventContractDetails.instance?.cleanUp();
|
||||
}
|
||||
|
||||
supports(eventType: string): boolean {
|
||||
return isEarlyEventType(eventType);
|
||||
}
|
||||
|
||||
addEventListener(element: HTMLElement, eventType: string, handler: Function): Function {
|
||||
// Note: contrary to the type, Window and Document can be passed in
|
||||
// as well.
|
||||
if (element.nodeType === Node.ELEMENT_NODE) {
|
||||
this.eventContractDetails.instance!.addEvent(eventType);
|
||||
getActionCache(element)[eventType] = '';
|
||||
sharedStashFunction(element, eventType, handler);
|
||||
} else {
|
||||
element.addEventListener(eventType, handler as EventListener);
|
||||
}
|
||||
return () => this.removeEventListener(element, eventType, handler);
|
||||
}
|
||||
|
||||
removeEventListener(element: HTMLElement, eventType: string, callback: Function): void {
|
||||
if (element.nodeType === Node.ELEMENT_NODE) {
|
||||
getActionCache(element)[eventType] = undefined;
|
||||
} else {
|
||||
element.removeEventListener(eventType, callback as EventListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const initGlobalEventDelegation = (
|
||||
eventContractDetails: EventContractDetails,
|
||||
injector: Injector,
|
||||
) => {
|
||||
if (injector.get(IS_EVENT_REPLAY_ENABLED, EVENT_REPLAY_ENABLED_DEFAULT)) {
|
||||
return;
|
||||
}
|
||||
const eventContract = (eventContractDetails.instance = new EventContract(
|
||||
new EventContractContainer(document.body),
|
||||
));
|
||||
const dispatcher = new EventDispatcher(invokeRegisteredListeners, /** clickModSupport */ false);
|
||||
registerDispatcher(eventContract, dispatcher);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {EventContract} from '@angular/core/primitives/event-dispatch';
|
||||
import {ENVIRONMENT_INITIALIZER, Injector} from '../di';
|
||||
import {inject} from '../di/injector_compatibility';
|
||||
import {Provider} from '../di/interface/provider';
|
||||
import {
|
||||
GLOBAL_EVENT_DELEGATION,
|
||||
GlobalEventDelegation,
|
||||
JSACTION_EVENT_CONTRACT,
|
||||
initGlobalEventDelegation,
|
||||
} from '../event_delegation_utils';
|
||||
|
||||
import {IS_GLOBAL_EVENT_DELEGATION_ENABLED} from '../hydration/tokens';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__jsaction_contract: EventContract | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of providers required to setup support for event delegation.
|
||||
* @param multiContract - Experimental support to provide one event contract
|
||||
* when there are multiple binaries on the page.
|
||||
*/
|
||||
export function provideGlobalEventDelegation(multiContract = false): Provider[] {
|
||||
return [
|
||||
{
|
||||
provide: IS_GLOBAL_EVENT_DELEGATION_ENABLED,
|
||||
useValue: true,
|
||||
},
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
useValue: () => {
|
||||
const injector = inject(Injector);
|
||||
const eventContractDetails = injector.get(JSACTION_EVENT_CONTRACT);
|
||||
if (multiContract && window.__jsaction_contract) {
|
||||
eventContractDetails.instance = window.__jsaction_contract;
|
||||
return;
|
||||
}
|
||||
initGlobalEventDelegation(eventContractDetails, injector);
|
||||
window.__jsaction_contract = eventContractDetails.instance;
|
||||
},
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: GLOBAL_EVENT_DELEGATION,
|
||||
useClass: GlobalEventDelegation,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -27,11 +27,7 @@ import {CLEANUP, LView, TView} from '../render3/interfaces/view';
|
|||
import {isPlatformBrowser} from '../render3/util/misc_utils';
|
||||
import {unwrapRNode} from '../render3/util/view_utils';
|
||||
|
||||
import {
|
||||
EVENT_REPLAY_ENABLED_DEFAULT,
|
||||
IS_EVENT_REPLAY_ENABLED,
|
||||
IS_GLOBAL_EVENT_DELEGATION_ENABLED,
|
||||
} from './tokens';
|
||||
import {EVENT_REPLAY_ENABLED_DEFAULT, IS_EVENT_REPLAY_ENABLED} from './tokens';
|
||||
import {
|
||||
sharedStashFunction,
|
||||
removeListeners,
|
||||
|
|
@ -47,18 +43,11 @@ import {performanceMarkFeature} from '../util/performance';
|
|||
*/
|
||||
const jsactionSet = new Set<Element>();
|
||||
|
||||
function isGlobalEventDelegationEnabled(injector: Injector) {
|
||||
return injector.get(IS_GLOBAL_EVENT_DELEGATION_ENABLED, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether Event Replay feature should be activated on the client.
|
||||
*/
|
||||
function shouldEnableEventReplay(injector: Injector) {
|
||||
return (
|
||||
injector.get(IS_EVENT_REPLAY_ENABLED, EVENT_REPLAY_ENABLED_DEFAULT) &&
|
||||
!isGlobalEventDelegationEnabled(injector)
|
||||
);
|
||||
return injector.get(IS_EVENT_REPLAY_ENABLED, EVENT_REPLAY_ENABLED_DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -49,10 +49,3 @@ export const IS_EVENT_REPLAY_ENABLED = new InjectionToken<boolean>(
|
|||
);
|
||||
|
||||
export const EVENT_REPLAY_ENABLED_DEFAULT = false;
|
||||
|
||||
/**
|
||||
* Internal token that indicates whether global event delegation support is enabled.
|
||||
*/
|
||||
export const IS_GLOBAL_EVENT_DELEGATION_ENABLED = new InjectionToken<boolean>(
|
||||
typeof ngDevMode === 'undefined' || !!ngDevMode ? 'IS_GLOBAL_EVENT_DELEGATION_ENABLED' : '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -179,9 +179,6 @@
|
|||
{
|
||||
"name": "DomAdapter"
|
||||
},
|
||||
{
|
||||
"name": "DomEventsPlugin"
|
||||
},
|
||||
{
|
||||
"name": "DomRendererFactory2"
|
||||
},
|
||||
|
|
@ -236,9 +233,6 @@
|
|||
{
|
||||
"name": "ErrorHandler"
|
||||
},
|
||||
{
|
||||
"name": "EventDelegationPlugin"
|
||||
},
|
||||
{
|
||||
"name": "EventEmitter"
|
||||
},
|
||||
|
|
@ -251,9 +245,6 @@
|
|||
{
|
||||
"name": "FALSE_BOOLEAN_VALUES"
|
||||
},
|
||||
{
|
||||
"name": "GLOBAL_EVENT_DELEGATION"
|
||||
},
|
||||
{
|
||||
"name": "GenericBrowserDomAdapter"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -257,9 +257,6 @@
|
|||
{
|
||||
"name": "ErrorHandler"
|
||||
},
|
||||
{
|
||||
"name": "EventDelegationPlugin"
|
||||
},
|
||||
{
|
||||
"name": "EventEmitter"
|
||||
},
|
||||
|
|
@ -272,9 +269,6 @@
|
|||
{
|
||||
"name": "FALSE_BOOLEAN_VALUES"
|
||||
},
|
||||
{
|
||||
"name": "GLOBAL_EVENT_DELEGATION"
|
||||
},
|
||||
{
|
||||
"name": "GenericBrowserDomAdapter"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -164,9 +164,6 @@
|
|||
{
|
||||
"name": "ErrorHandler"
|
||||
},
|
||||
{
|
||||
"name": "EventDelegationPlugin"
|
||||
},
|
||||
{
|
||||
"name": "EventEmitter"
|
||||
},
|
||||
|
|
@ -176,9 +173,6 @@
|
|||
{
|
||||
"name": "EventManagerPlugin"
|
||||
},
|
||||
{
|
||||
"name": "GLOBAL_EVENT_DELEGATION"
|
||||
},
|
||||
{
|
||||
"name": "GenericBrowserDomAdapter"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -197,9 +197,6 @@
|
|||
{
|
||||
"name": "ErrorHandler"
|
||||
},
|
||||
{
|
||||
"name": "EventDelegationPlugin"
|
||||
},
|
||||
{
|
||||
"name": "EventEmitter"
|
||||
},
|
||||
|
|
@ -209,9 +206,6 @@
|
|||
{
|
||||
"name": "EventManagerPlugin"
|
||||
},
|
||||
{
|
||||
"name": "GLOBAL_EVENT_DELEGATION"
|
||||
},
|
||||
{
|
||||
"name": "GenericBrowserDomAdapter"
|
||||
},
|
||||
|
|
@ -1436,9 +1430,6 @@
|
|||
{
|
||||
"name": "init_event_contract_defines"
|
||||
},
|
||||
{
|
||||
"name": "init_event_delegation"
|
||||
},
|
||||
{
|
||||
"name": "init_event_delegation_utils"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -224,9 +224,6 @@
|
|||
{
|
||||
"name": "ErrorHandler"
|
||||
},
|
||||
{
|
||||
"name": "EventDelegationPlugin"
|
||||
},
|
||||
{
|
||||
"name": "EventEmitter"
|
||||
},
|
||||
|
|
@ -272,9 +269,6 @@
|
|||
{
|
||||
"name": "FormsExampleModule"
|
||||
},
|
||||
{
|
||||
"name": "GLOBAL_EVENT_DELEGATION"
|
||||
},
|
||||
{
|
||||
"name": "GenericBrowserDomAdapter"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -227,9 +227,6 @@
|
|||
{
|
||||
"name": "ErrorHandler"
|
||||
},
|
||||
{
|
||||
"name": "EventDelegationPlugin"
|
||||
},
|
||||
{
|
||||
"name": "EventEmitter"
|
||||
},
|
||||
|
|
@ -251,9 +248,6 @@
|
|||
{
|
||||
"name": "FormsModule"
|
||||
},
|
||||
{
|
||||
"name": "GLOBAL_EVENT_DELEGATION"
|
||||
},
|
||||
{
|
||||
"name": "GenericBrowserDomAdapter"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -125,9 +125,6 @@
|
|||
{
|
||||
"name": "DomAdapter"
|
||||
},
|
||||
{
|
||||
"name": "DomEventsPlugin"
|
||||
},
|
||||
{
|
||||
"name": "DomRendererFactory2"
|
||||
},
|
||||
|
|
@ -170,9 +167,6 @@
|
|||
{
|
||||
"name": "ErrorHandler"
|
||||
},
|
||||
{
|
||||
"name": "EventDelegationPlugin"
|
||||
},
|
||||
{
|
||||
"name": "EventEmitter"
|
||||
},
|
||||
|
|
@ -182,9 +176,6 @@
|
|||
{
|
||||
"name": "EventManagerPlugin"
|
||||
},
|
||||
{
|
||||
"name": "GLOBAL_EVENT_DELEGATION"
|
||||
},
|
||||
{
|
||||
"name": "GenericBrowserDomAdapter"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -188,9 +188,6 @@
|
|||
{
|
||||
"name": "DomAdapter"
|
||||
},
|
||||
{
|
||||
"name": "DomEventsPlugin"
|
||||
},
|
||||
{
|
||||
"name": "DomRendererFactory2"
|
||||
},
|
||||
|
|
@ -236,9 +233,6 @@
|
|||
{
|
||||
"name": "ErrorHandler"
|
||||
},
|
||||
{
|
||||
"name": "EventDelegationPlugin"
|
||||
},
|
||||
{
|
||||
"name": "EventEmitter"
|
||||
},
|
||||
|
|
@ -251,9 +245,6 @@
|
|||
{
|
||||
"name": "EventType2"
|
||||
},
|
||||
{
|
||||
"name": "GLOBAL_EVENT_DELEGATION"
|
||||
},
|
||||
{
|
||||
"name": "GenericBrowserDomAdapter"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -107,9 +107,6 @@
|
|||
{
|
||||
"name": "DomAdapter"
|
||||
},
|
||||
{
|
||||
"name": "DomEventsPlugin"
|
||||
},
|
||||
{
|
||||
"name": "DomRendererFactory2"
|
||||
},
|
||||
|
|
@ -149,9 +146,6 @@
|
|||
{
|
||||
"name": "ErrorHandler"
|
||||
},
|
||||
{
|
||||
"name": "EventDelegationPlugin"
|
||||
},
|
||||
{
|
||||
"name": "EventEmitter"
|
||||
},
|
||||
|
|
@ -161,9 +155,6 @@
|
|||
{
|
||||
"name": "EventManagerPlugin"
|
||||
},
|
||||
{
|
||||
"name": "GLOBAL_EVENT_DELEGATION"
|
||||
},
|
||||
{
|
||||
"name": "GenericBrowserDomAdapter"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -167,9 +167,6 @@
|
|||
{
|
||||
"name": "ErrorHandler"
|
||||
},
|
||||
{
|
||||
"name": "EventDelegationPlugin"
|
||||
},
|
||||
{
|
||||
"name": "EventEmitter"
|
||||
},
|
||||
|
|
@ -179,9 +176,6 @@
|
|||
{
|
||||
"name": "EventManagerPlugin"
|
||||
},
|
||||
{
|
||||
"name": "GLOBAL_EVENT_DELEGATION"
|
||||
},
|
||||
{
|
||||
"name": "GenericBrowserDomAdapter"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
load("//tools:defaults.bzl", "karma_web_test_suite", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = [
|
||||
"event_dispatch_spec.ts",
|
||||
],
|
||||
deps = [
|
||||
"//packages/core",
|
||||
"//packages/core/primitives/event-dispatch",
|
||||
"//packages/core/testing",
|
||||
"//packages/platform-browser",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
|
|
@ -1,303 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {
|
||||
Component,
|
||||
HostListener,
|
||||
Renderer2,
|
||||
inject,
|
||||
ɵprovideGlobalEventDelegation,
|
||||
} from '@angular/core';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
function configureTestingModule(components: unknown[]) {
|
||||
TestBed.configureTestingModule({
|
||||
imports: components,
|
||||
providers: [ɵprovideGlobalEventDelegation()],
|
||||
});
|
||||
}
|
||||
|
||||
describe('event dispatch', () => {
|
||||
let fixture: ComponentFixture<unknown>;
|
||||
|
||||
it(`executes an onclick handler`, async () => {
|
||||
const onClickSpy = jasmine.createSpy();
|
||||
@Component({
|
||||
selector: 'app',
|
||||
standalone: true,
|
||||
template: `
|
||||
<button id="btn" (click)="onClick()"></button>
|
||||
`,
|
||||
})
|
||||
class AppComponent {
|
||||
onClick = onClickSpy;
|
||||
}
|
||||
configureTestingModule([AppComponent]);
|
||||
const addEventListenerSpy = spyOn(
|
||||
HTMLButtonElement.prototype,
|
||||
'addEventListener',
|
||||
).and.callThrough();
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
const button = (fixture.debugElement.nativeElement as Element)
|
||||
.firstElementChild as HTMLButtonElement;
|
||||
button.click();
|
||||
expect(onClickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(addEventListenerSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should work for elements with local refs', async () => {
|
||||
const onClickSpy = jasmine.createSpy();
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
standalone: true,
|
||||
template: `
|
||||
<button id="btn" (click)="onClick()" #localRef></button>
|
||||
`,
|
||||
})
|
||||
class AppComponent {
|
||||
onClick = onClickSpy;
|
||||
}
|
||||
configureTestingModule([AppComponent]);
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.debugElement.nativeElement.querySelector('#btn').click();
|
||||
expect(onClickSpy).toHaveBeenCalled();
|
||||
});
|
||||
it('should route to the appropriate component with content projection', async () => {
|
||||
const outerOnClickSpy = jasmine.createSpy();
|
||||
const innerOnClickSpy = jasmine.createSpy();
|
||||
@Component({
|
||||
selector: 'app-card',
|
||||
standalone: true,
|
||||
template: `
|
||||
<div class="card">
|
||||
<button id="inner-button" (click)="onClick()"></button>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class CardComponent {
|
||||
onClick = innerOnClickSpy;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
imports: [CardComponent],
|
||||
standalone: true,
|
||||
template: `
|
||||
<app-card>
|
||||
<h2>Card Title</h2>
|
||||
<p>This is some card content.</p>
|
||||
<button id="outer-button" (click)="onClick()">Click Me</button>
|
||||
</app-card>
|
||||
`,
|
||||
})
|
||||
class AppComponent {
|
||||
onClick = outerOnClickSpy;
|
||||
}
|
||||
configureTestingModule([AppComponent]);
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
const nativeElement = fixture.debugElement.nativeElement;
|
||||
const outer = nativeElement.querySelector('#outer-button')!;
|
||||
const inner = nativeElement.querySelector('#inner-button')!;
|
||||
outer.click();
|
||||
inner.click();
|
||||
expect(outerOnClickSpy).toHaveBeenCalledBefore(innerOnClickSpy);
|
||||
});
|
||||
|
||||
describe('bubbling behavior', () => {
|
||||
it('should propagate events', async () => {
|
||||
const onClickSpy = jasmine.createSpy();
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<div id="top" (click)="onClick()">
|
||||
<div id="bottom" (click)="onClick()"></div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
onClick = onClickSpy;
|
||||
}
|
||||
configureTestingModule([SimpleComponent]);
|
||||
fixture = TestBed.createComponent(SimpleComponent);
|
||||
const nativeElement = fixture.debugElement.nativeElement;
|
||||
const bottomEl = nativeElement.querySelector('#bottom')!;
|
||||
bottomEl.click();
|
||||
expect(onClickSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not propagate events if stopPropagation is called', async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<div id="top" (click)="onClick($event)">
|
||||
<div id="bottom" (click)="onClick($event)"></div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
onClick(e: Event) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
const onClickSpy = spyOn(SimpleComponent.prototype, 'onClick').and.callThrough();
|
||||
configureTestingModule([SimpleComponent]);
|
||||
fixture = TestBed.createComponent(SimpleComponent);
|
||||
const nativeElement = fixture.debugElement.nativeElement;
|
||||
const bottomEl = nativeElement.querySelector('#bottom')!;
|
||||
bottomEl.click();
|
||||
expect(onClickSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should call the original stopPropagation method', async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<div id="top" (click)="onClick($event)">
|
||||
<div id="bottom" (click)="onClick($event)"></div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
onClick(e: Event) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
configureTestingModule([SimpleComponent]);
|
||||
fixture = TestBed.createComponent(SimpleComponent);
|
||||
const nativeElement = fixture.debugElement.nativeElement;
|
||||
const bottomEl = nativeElement.querySelector('#bottom')!;
|
||||
const event = new MouseEvent('click', {bubbles: true});
|
||||
spyOn(event, 'stopPropagation');
|
||||
const stopPropagation = event.stopPropagation;
|
||||
spyOn(event, 'preventDefault');
|
||||
const preventDefault = event.preventDefault;
|
||||
bottomEl.dispatchEvent(event);
|
||||
expect(stopPropagation).toHaveBeenCalled();
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('manual listening', () => {
|
||||
it('should trigger events when manually registered', async () => {
|
||||
const onClickSpy = jasmine.createSpy();
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<div id="top">
|
||||
<div id="bottom"></div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
renderer = inject(Renderer2);
|
||||
destroy!: Function;
|
||||
listen(el: Element) {
|
||||
this.destroy = this.renderer.listen(el, 'click', onClickSpy);
|
||||
}
|
||||
}
|
||||
configureTestingModule([SimpleComponent]);
|
||||
fixture = TestBed.createComponent(SimpleComponent);
|
||||
const nativeElement = fixture.debugElement.nativeElement;
|
||||
(fixture.componentInstance as SimpleComponent).listen(nativeElement);
|
||||
const bottomEl = nativeElement.querySelector('#bottom')!;
|
||||
bottomEl.click();
|
||||
expect(onClickSpy).toHaveBeenCalledTimes(1);
|
||||
bottomEl.dispatchEvent(new MouseEvent('click', {bubbles: true, shiftKey: true}));
|
||||
expect(onClickSpy).toHaveBeenCalledTimes(2);
|
||||
(fixture.componentInstance as SimpleComponent).destroy();
|
||||
bottomEl.click();
|
||||
expect(onClickSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it('should allow host listening on the window', async () => {
|
||||
const onClickSpy = jasmine.createSpy();
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<div id="top">
|
||||
<div id="bottom"></div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
renderer = inject(Renderer2);
|
||||
destroy!: Function;
|
||||
@HostListener('window:click', ['$event.target'])
|
||||
listen(el: Element) {
|
||||
onClickSpy();
|
||||
}
|
||||
}
|
||||
configureTestingModule([SimpleComponent]);
|
||||
fixture = TestBed.createComponent(SimpleComponent);
|
||||
const nativeElement = fixture.debugElement.nativeElement;
|
||||
const bottomEl = nativeElement.querySelector('#bottom')!;
|
||||
bottomEl.click();
|
||||
expect(onClickSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should allow host listening on the window', async () => {
|
||||
const onClickSpy = jasmine.createSpy();
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<div id="top">
|
||||
<div id="bottom"></div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
renderer = inject(Renderer2);
|
||||
destroy!: Function;
|
||||
@HostListener('window:click', ['$event.target'])
|
||||
listen(el: Element) {
|
||||
onClickSpy();
|
||||
}
|
||||
}
|
||||
configureTestingModule([SimpleComponent]);
|
||||
fixture = TestBed.createComponent(SimpleComponent);
|
||||
const nativeElement = fixture.debugElement.nativeElement;
|
||||
const bottomEl = nativeElement.querySelector('#bottom')!;
|
||||
bottomEl.click();
|
||||
expect(onClickSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('capture behavior', () => {
|
||||
let fixture: ComponentFixture<unknown>;
|
||||
it('should not bubble', async () => {
|
||||
const onFocusSpy = jasmine.createSpy();
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<div id="top" (focus)="onFocus()">
|
||||
<div id="bottom"></div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
onFocus = onFocusSpy;
|
||||
}
|
||||
configureTestingModule([SimpleComponent]);
|
||||
fixture = TestBed.createComponent(SimpleComponent);
|
||||
const nativeElement = fixture.debugElement.nativeElement;
|
||||
const bottomEl = nativeElement.querySelector('#bottom')!;
|
||||
const topEl = nativeElement.querySelector('#top')!;
|
||||
bottomEl.dispatchEvent(new FocusEvent('focus'));
|
||||
expect(onFocusSpy).toHaveBeenCalledTimes(0);
|
||||
topEl.dispatchEvent(new FocusEvent('focus'));
|
||||
expect(onFocusSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -49,7 +49,6 @@ import {BrowserGetTestability} from './browser/testability';
|
|||
import {BrowserXhr} from './browser/xhr';
|
||||
import {DomRendererFactory2} from './dom/dom_renderer';
|
||||
import {DomEventsPlugin} from './dom/events/dom_events';
|
||||
import {EventDelegationPlugin} from './dom/events/event_delegation';
|
||||
import {EVENT_MANAGER_PLUGINS, EventManager} from './dom/events/event_manager';
|
||||
import {KeyEventsPlugin} from './dom/events/key_events';
|
||||
import {SharedStylesHost} from './dom/shared_styles_host';
|
||||
|
|
@ -241,11 +240,6 @@ const BROWSER_MODULE_PROVIDERS: Provider[] = [
|
|||
deps: [DOCUMENT, NgZone, PLATFORM_ID],
|
||||
},
|
||||
{provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true, deps: [DOCUMENT]},
|
||||
{
|
||||
provide: EVENT_MANAGER_PLUGINS,
|
||||
useClass: EventDelegationPlugin,
|
||||
multi: true,
|
||||
},
|
||||
DomRendererFactory2,
|
||||
SharedStylesHost,
|
||||
EventManager,
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, inject, ɵGLOBAL_EVENT_DELEGATION} from '@angular/core';
|
||||
import {EventManagerPlugin} from './event_manager';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
|
||||
@Injectable()
|
||||
export class EventDelegationPlugin extends EventManagerPlugin {
|
||||
private delegate = inject(ɵGLOBAL_EVENT_DELEGATION, {optional: true});
|
||||
constructor(@Inject(DOCUMENT) doc: any) {
|
||||
super(doc);
|
||||
}
|
||||
|
||||
override supports(eventName: string): boolean {
|
||||
// If `GlobalDelegationEventPlugin` implementation is not provided,
|
||||
// this plugin is kept disabled.
|
||||
return this.delegate ? this.delegate.supports(eventName) : false;
|
||||
}
|
||||
|
||||
override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
|
||||
return this.delegate!.addEventListener(element, eventName, handler);
|
||||
}
|
||||
|
||||
removeEventListener(element: HTMLElement, eventName: string, callback: Function): void {
|
||||
return this.delegate!.removeEventListener(element, eventName, callback);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue