Skip to content

Commit 36bdbd8

Browse files
committed
refactor(multiple): use renderer for manually-bound events with options
Switches all manually-bound event handlers that were passing options to go through the renderer.
1 parent dbc2e21 commit 36bdbd8

File tree

13 files changed

+460
-335
lines changed

13 files changed

+460
-335
lines changed

src/cdk-experimental/popover-edit/table-directives.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import {
1919
TemplateRef,
2020
ViewContainerRef,
2121
inject,
22+
Renderer2,
23+
ListenerOptions,
2224
} from '@angular/core';
23-
import {fromEvent, fromEventPattern, merge, Subject} from 'rxjs';
25+
import {merge, Observable, Subject} from 'rxjs';
2426
import {
2527
debounceTime,
2628
filter,
@@ -44,6 +46,7 @@ import {
4446
} from './focus-escape-notifier';
4547
import {closest} from './polyfill';
4648
import {EditRef} from './edit-ref';
49+
import {_bindEventWithOptions} from '@angular/cdk/platform';
4750

4851
/**
4952
* Describes the number of columns before and after the originating cell that the
@@ -73,6 +76,7 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
7376
inject<EditEventDispatcher<EditRef<unknown>>>(EditEventDispatcher);
7477
protected readonly focusDispatcher = inject(FocusDispatcher);
7578
protected readonly ngZone = inject(NgZone);
79+
private readonly _renderer = inject(Renderer2);
7680

7781
protected readonly destroyed = new Subject<void>();
7882

@@ -94,20 +98,37 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
9498
this._rendered.complete();
9599
}
96100

101+
private _observableFromEvent<T extends Event>(
102+
element: Element,
103+
name: string,
104+
options?: ListenerOptions,
105+
) {
106+
return new Observable<T>(subscriber => {
107+
const handler = (event: T) => subscriber.next(event);
108+
const cleanup = options
109+
? _bindEventWithOptions(this._renderer, element, name, handler, options)
110+
: this._renderer.listen(element, name, handler, options);
111+
return () => {
112+
cleanup();
113+
subscriber.complete();
114+
};
115+
});
116+
}
117+
97118
private _listenForTableEvents(): void {
98119
const element = this.elementRef.nativeElement;
99120
const toClosest = (selector: string) =>
100121
map((event: UIEvent) => closest(event.target, selector));
101122

102123
this.ngZone.runOutsideAngular(() => {
103124
// Track mouse movement over the table to hide/show hover content.
104-
fromEvent<MouseEvent>(element, 'mouseover')
125+
this._observableFromEvent<MouseEvent>(element, 'mouseover')
105126
.pipe(toClosest(ROW_SELECTOR), takeUntil(this.destroyed))
106127
.subscribe(this.editEventDispatcher.hovering);
107-
fromEvent<MouseEvent>(element, 'mouseleave')
128+
this._observableFromEvent<MouseEvent>(element, 'mouseleave')
108129
.pipe(mapTo(null), takeUntil(this.destroyed))
109130
.subscribe(this.editEventDispatcher.hovering);
110-
fromEvent<MouseEvent>(element, 'mousemove')
131+
this._observableFromEvent<MouseEvent>(element, 'mousemove')
111132
.pipe(
112133
throttleTime(MOUSE_MOVE_THROTTLE_TIME_MS),
113134
toClosest(ROW_SELECTOR),
@@ -116,19 +137,15 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
116137
.subscribe(this.editEventDispatcher.mouseMove);
117138

118139
// Track focus within the table to hide/show/make focusable hover content.
119-
fromEventPattern<FocusEvent>(
120-
handler => element.addEventListener('focus', handler, true),
121-
handler => element.removeEventListener('focus', handler, true),
122-
)
140+
this._observableFromEvent<FocusEvent>(element, 'focus', {capture: true})
123141
.pipe(toClosest(ROW_SELECTOR), share(), takeUntil(this.destroyed))
124142
.subscribe(this.editEventDispatcher.focused);
125143

126144
merge(
127-
fromEventPattern<FocusEvent>(
128-
handler => element.addEventListener('blur', handler, true),
129-
handler => element.removeEventListener('blur', handler, true),
145+
this._observableFromEvent(element, 'blur', {capture: true}),
146+
this._observableFromEvent<KeyboardEvent>(element, 'keydown').pipe(
147+
filter(event => event.key === 'Escape'),
130148
),
131-
fromEvent<KeyboardEvent>(element, 'keydown').pipe(filter(event => event.key === 'Escape')),
132149
)
133150
.pipe(mapTo(null), share(), takeUntil(this.destroyed))
134151
.subscribe(this.editEventDispatcher.focused);
@@ -150,7 +167,7 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
150167
)
151168
.subscribe(this.editEventDispatcher.allRows);
152169

153-
fromEvent<KeyboardEvent>(element, 'keydown')
170+
this._observableFromEvent<KeyboardEvent>(element, 'keydown')
154171
.pipe(
155172
filter(event => event.key === 'Enter'),
156173
toClosest(CELL_SELECTOR),
@@ -159,7 +176,7 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
159176
.subscribe(this.editEventDispatcher.editing);
160177

161178
// Keydown must be used here or else key auto-repeat does not work properly on some platforms.
162-
fromEvent<KeyboardEvent>(element, 'keydown')
179+
this._observableFromEvent<KeyboardEvent>(element, 'keydown')
163180
.pipe(takeUntil(this.destroyed))
164181
.subscribe(this.focusDispatcher.keyObserver);
165182
});

src/cdk/a11y/focus-monitor/focus-monitor.ts

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
import {
1010
Platform,
11-
normalizePassiveListenerOptions,
1211
_getShadowRoot,
1312
_getEventTarget,
13+
_bindEventWithOptions,
1414
} from '@angular/cdk/platform';
1515
import {
1616
Directive,
@@ -23,6 +23,7 @@ import {
2323
Output,
2424
AfterViewInit,
2525
inject,
26+
RendererFactory2,
2627
} from '@angular/core';
2728
import {Observable, of as observableOf, Subject, Subscription} from 'rxjs';
2829
import {takeUntil} from 'rxjs/operators';
@@ -76,16 +77,18 @@ type MonitoredElementInfo = {
7677
* Event listener options that enable capturing and also
7778
* mark the listener as passive if the browser supports it.
7879
*/
79-
const captureEventListenerOptions = normalizePassiveListenerOptions({
80+
const captureEventListenerOptions = {
8081
passive: true,
8182
capture: true,
82-
});
83+
};
8384

8485
/** Monitors mouse and keyboard events to determine the cause of focus events. */
8586
@Injectable({providedIn: 'root'})
8687
export class FocusMonitor implements OnDestroy {
8788
private _ngZone = inject(NgZone);
8889
private _platform = inject(Platform);
90+
private _renderer = inject(RendererFactory2).createRenderer(null, null);
91+
private _cleanupWindowFocus: (() => void) | undefined;
8992
private readonly _inputModalityDetector = inject(InputModalityDetector);
9093

9194
/** The focus origin that the next focus event is a result of. */
@@ -121,7 +124,13 @@ export class FocusMonitor implements OnDestroy {
121124
* handlers differently from the rest of the events, because the browser won't emit events
122125
* to the document when focus moves inside of a shadow root.
123126
*/
124-
private _rootNodeFocusListenerCount = new Map<HTMLElement | Document | ShadowRoot, number>();
127+
private _rootNodeFocusListeners = new Map<
128+
HTMLElement | Document | ShadowRoot,
129+
{
130+
count: number;
131+
cleanups: (() => void)[];
132+
}
133+
>();
125134

126135
/**
127136
* The specified detection mode, used for attributing the origin of a focus
@@ -307,12 +316,6 @@ export class FocusMonitor implements OnDestroy {
307316
return this._document || document;
308317
}
309318

310-
/** Use defaultView of injected document if available or fallback to global window reference */
311-
private _getWindow(): Window {
312-
const doc = this._getDocument();
313-
return doc.defaultView || window;
314-
}
315-
316319
private _getFocusOrigin(focusEventTarget: HTMLElement | null): FocusOrigin {
317320
if (this._origin) {
318321
// If the origin was realized via a touch interaction, we need to perform additional checks
@@ -468,32 +471,45 @@ export class FocusMonitor implements OnDestroy {
468471
}
469472

470473
const rootNode = elementInfo.rootNode;
471-
const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode) || 0;
474+
const listeners = this._rootNodeFocusListeners.get(rootNode);
472475

473-
if (!rootNodeFocusListeners) {
476+
if (listeners) {
477+
listeners.count++;
478+
} else {
474479
this._ngZone.runOutsideAngular(() => {
475-
rootNode.addEventListener(
476-
'focus',
477-
this._rootNodeFocusAndBlurListener,
478-
captureEventListenerOptions,
479-
);
480-
rootNode.addEventListener(
481-
'blur',
482-
this._rootNodeFocusAndBlurListener,
483-
captureEventListenerOptions,
484-
);
480+
this._rootNodeFocusListeners.set(rootNode, {
481+
count: 1,
482+
cleanups: [
483+
_bindEventWithOptions(
484+
this._renderer,
485+
rootNode,
486+
'focus',
487+
this._rootNodeFocusAndBlurListener,
488+
captureEventListenerOptions,
489+
),
490+
_bindEventWithOptions(
491+
this._renderer,
492+
rootNode,
493+
'blur',
494+
this._rootNodeFocusAndBlurListener,
495+
captureEventListenerOptions,
496+
),
497+
],
498+
});
485499
});
486500
}
487501

488-
this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners + 1);
489-
490502
// Register global listeners when first element is monitored.
491503
if (++this._monitoredElementCount === 1) {
492504
// Note: we listen to events in the capture phase so we
493505
// can detect them even if the user stops propagation.
494506
this._ngZone.runOutsideAngular(() => {
495-
const window = this._getWindow();
496-
window.addEventListener('focus', this._windowFocusListener);
507+
this._cleanupWindowFocus?.();
508+
this._cleanupWindowFocus = this._renderer.listen(
509+
'window',
510+
'focus',
511+
this._windowFocusListener,
512+
);
497513
});
498514

499515
// The InputModalityDetector is also just a collection of global listeners.
@@ -506,32 +522,20 @@ export class FocusMonitor implements OnDestroy {
506522
}
507523

508524
private _removeGlobalListeners(elementInfo: MonitoredElementInfo) {
509-
const rootNode = elementInfo.rootNode;
525+
const listeners = this._rootNodeFocusListeners.get(elementInfo.rootNode);
510526

511-
if (this._rootNodeFocusListenerCount.has(rootNode)) {
512-
const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode)!;
513-
514-
if (rootNodeFocusListeners > 1) {
515-
this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners - 1);
527+
if (listeners) {
528+
if (listeners.count > 1) {
529+
listeners.count--;
516530
} else {
517-
rootNode.removeEventListener(
518-
'focus',
519-
this._rootNodeFocusAndBlurListener,
520-
captureEventListenerOptions,
521-
);
522-
rootNode.removeEventListener(
523-
'blur',
524-
this._rootNodeFocusAndBlurListener,
525-
captureEventListenerOptions,
526-
);
527-
this._rootNodeFocusListenerCount.delete(rootNode);
531+
listeners.cleanups.forEach(cleanup => cleanup());
532+
this._rootNodeFocusListeners.delete(elementInfo.rootNode);
528533
}
529534
}
530535

531536
// Unregister global listeners when last element is unmonitored.
532537
if (!--this._monitoredElementCount) {
533-
const window = this._getWindow();
534-
window.removeEventListener('focus', this._windowFocusListener);
538+
this._cleanupWindowFocus?.();
535539

536540
// Equivalently, stop our InputModalityDetector subscription.
537541
this._stopInputModalityDetector.next();

src/cdk/a11y/input-modality/input-modality-detector.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@
77
*/
88

99
import {ALT, CONTROL, MAC_META, META, SHIFT} from '@angular/cdk/keycodes';
10-
import {Injectable, InjectionToken, OnDestroy, NgZone, inject} from '@angular/core';
11-
import {normalizePassiveListenerOptions, Platform, _getEventTarget} from '@angular/cdk/platform';
10+
import {
11+
Injectable,
12+
InjectionToken,
13+
OnDestroy,
14+
NgZone,
15+
inject,
16+
RendererFactory2,
17+
} from '@angular/core';
18+
import {Platform, _bindEventWithOptions, _getEventTarget} from '@angular/cdk/platform';
1219
import {DOCUMENT} from '@angular/common';
1320
import {BehaviorSubject, Observable} from 'rxjs';
1421
import {distinctUntilChanged, skip} from 'rxjs/operators';
@@ -69,10 +76,10 @@ export const TOUCH_BUFFER_MS = 650;
6976
* Event listener options that enable capturing and also mark the listener as passive if the browser
7077
* supports it.
7178
*/
72-
const modalityEventListenerOptions = normalizePassiveListenerOptions({
79+
const modalityEventListenerOptions = {
7380
passive: true,
7481
capture: true,
75-
});
82+
};
7683

7784
/**
7885
* Service that detects the user's input modality.
@@ -91,6 +98,7 @@ const modalityEventListenerOptions = normalizePassiveListenerOptions({
9198
@Injectable({providedIn: 'root'})
9299
export class InputModalityDetector implements OnDestroy {
93100
private readonly _platform = inject(Platform);
101+
private readonly _listenerCleanups: (() => void)[] | undefined;
94102

95103
/** Emits whenever an input modality is detected. */
96104
readonly modalityDetected: Observable<InputModality>;
@@ -193,21 +201,38 @@ export class InputModalityDetector implements OnDestroy {
193201
// If we're not in a browser, this service should do nothing, as there's no relevant input
194202
// modality to detect.
195203
if (this._platform.isBrowser) {
196-
ngZone.runOutsideAngular(() => {
197-
document.addEventListener('keydown', this._onKeydown, modalityEventListenerOptions);
198-
document.addEventListener('mousedown', this._onMousedown, modalityEventListenerOptions);
199-
document.addEventListener('touchstart', this._onTouchstart, modalityEventListenerOptions);
204+
const renderer = inject(RendererFactory2).createRenderer(null, null);
205+
206+
this._listenerCleanups = ngZone.runOutsideAngular(() => {
207+
return [
208+
_bindEventWithOptions(
209+
renderer,
210+
document,
211+
'keydown',
212+
this._onKeydown,
213+
modalityEventListenerOptions,
214+
),
215+
_bindEventWithOptions(
216+
renderer,
217+
document,
218+
'mousedown',
219+
this._onMousedown,
220+
modalityEventListenerOptions,
221+
),
222+
_bindEventWithOptions(
223+
renderer,
224+
document,
225+
'touchstart',
226+
this._onTouchstart,
227+
modalityEventListenerOptions,
228+
),
229+
];
200230
});
201231
}
202232
}
203233

204234
ngOnDestroy() {
205235
this._modality.complete();
206-
207-
if (this._platform.isBrowser) {
208-
document.removeEventListener('keydown', this._onKeydown, modalityEventListenerOptions);
209-
document.removeEventListener('mousedown', this._onMousedown, modalityEventListenerOptions);
210-
document.removeEventListener('touchstart', this._onTouchstart, modalityEventListenerOptions);
211-
}
236+
this._listenerCleanups?.forEach(cleanup => cleanup());
212237
}
213238
}

0 commit comments

Comments
 (0)