Skip to content

Commit

Permalink
fix(core): Do not bubble capture events. (angular#57476)
Browse files Browse the repository at this point in the history
These should only fire if the target is the same as the targetElement. Also, delete an out of date test since capture/non-capture tests are separately covered.

PR Close angular#57476
  • Loading branch information
iteriani authored and alxhub committed Aug 23, 2024
1 parent 0cebfd7 commit 7a99815
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {ActionResolver} from './action_resolver';
import {Dispatcher} from './dispatcher';
import {EventInfo, EventInfoWrapper} from './event_info';
import {isCaptureEventType} from './event_type';
import {UnrenamedEventContract} from './eventcontract';
import {Restriction} from './restriction';

Expand Down Expand Up @@ -81,6 +82,13 @@ export class EventDispatcher {
prepareEventForBubbling(eventInfoWrapper);
while (eventInfoWrapper.getAction()) {
prepareEventForDispatch(eventInfoWrapper);
// If this is a capture event, ONLY dispatch if the action element is the target.
if (
isCaptureEventType(eventInfoWrapper.getEventType()) &&
eventInfoWrapper.getAction()!.element !== eventInfoWrapper.getTargetElement()
) {
return;
}
this.dispatchDelegate(eventInfoWrapper.getEvent(), eventInfoWrapper.getAction()!.name);
if (propagationStopped(eventInfoWrapper)) {
return;
Expand Down
60 changes: 28 additions & 32 deletions packages/core/test/event_dispatch/event_dispatch_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,38 +109,6 @@ describe('event dispatch', () => {
inner.click();
expect(outerOnClickSpy).toHaveBeenCalledBefore(innerOnClickSpy);
});
it('should serialize event types to be listened to and jsaction cache entry', async () => {
const clickSpy = jasmine.createSpy('onClick');
const focusSpy = jasmine.createSpy('onFocus');
@Component({
standalone: true,
selector: 'app',
template: `
<div (click)="onClick()" id="click-element">
<div id="focus-container">
<div id="focus-action-element" (focus)="onFocus()">
<button id="focus-target-element">Focus Button</button>
</div>
</div>
</div>
`,
})
class SimpleComponent {
onClick = clickSpy;
onFocus = focusSpy;
}
configureTestingModule([SimpleComponent]);
fixture = TestBed.createComponent(SimpleComponent);
const nativeElement = fixture.debugElement.nativeElement;
const el = nativeElement.querySelector('#click-element')!;
const button = nativeElement.querySelector('#focus-target-element')!;
const clickEvent = new CustomEvent('click', {bubbles: true});
el.dispatchEvent(clickEvent);
const focusEvent = new CustomEvent('focus');
button.dispatchEvent(focusEvent);
expect(clickSpy).toHaveBeenCalled();
expect(focusSpy).toHaveBeenCalled();
});

describe('bubbling behavior', () => {
it('should propagate events', async () => {
Expand Down Expand Up @@ -305,3 +273,31 @@ describe('event dispatch', () => {
});
});
});

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);
});
});
40 changes: 0 additions & 40 deletions packages/platform-server/test/event_replay_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,46 +208,6 @@ describe('event replay', () => {
expect(outerOnClickSpy).toHaveBeenCalledBefore(innerOnClickSpy);
});

it('should serialize event types to be listened to and jsaction attribute', async () => {
const clickSpy = jasmine.createSpy('onClick');
const focusSpy = jasmine.createSpy('onFocus');
@Component({
standalone: true,
selector: 'app',
template: `
<div (click)="onClick()" id="click-element">
<div id="focus-container">
<div id="focus-action-element" (focus)="onFocus()">
<button id="focus-target-element">Focus Button</button>
</div>
</div>
</div>
`,
})
class SimpleComponent {
onClick = clickSpy;
onFocus = focusSpy;
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);

render(doc, ssrContents);
const el = doc.getElementById('click-element')!;
const button = doc.getElementById('focus-target-element')!;
const clickEvent = new CustomEvent('click', {bubbles: true});
el.dispatchEvent(clickEvent);
const focusEvent = new CustomEvent('focus');
button.dispatchEvent(focusEvent);
expect(clickSpy).not.toHaveBeenCalled();
expect(focusSpy).not.toHaveBeenCalled();
resetTViewsFor(SimpleComponent);
await hydrate(doc, SimpleComponent, {
hydrationFeatures: [withEventReplay()],
});
expect(clickSpy).toHaveBeenCalled();
expect(focusSpy).toHaveBeenCalled();
});

it('should remove jsaction attributes, but continue listening to events.', async () => {
@Component({
standalone: true,
Expand Down

0 comments on commit 7a99815

Please sign in to comment.