Skip to content

Commit

Permalink
Revert "refactor(multiple): use renderer for manually-bound events wi…
Browse files Browse the repository at this point in the history
…th options"

This reverts commit c92253d.
  • Loading branch information
crisbeto committed Dec 18, 2024
1 parent 8c8a1d1 commit 93912ad
Show file tree
Hide file tree
Showing 13 changed files with 335 additions and 460 deletions.
45 changes: 14 additions & 31 deletions src/cdk-experimental/popover-edit/table-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@ import {
TemplateRef,
ViewContainerRef,
inject,
Renderer2,
ListenerOptions,
} from '@angular/core';
import {merge, Observable, Subject} from 'rxjs';
import {fromEvent, fromEventPattern, merge, Subject} from 'rxjs';
import {
debounceTime,
filter,
Expand All @@ -46,7 +44,6 @@ import {
} from './focus-escape-notifier';
import {closest} from './polyfill';
import {EditRef} from './edit-ref';
import {_bindEventWithOptions} from '@angular/cdk/platform';

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

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

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

private _observableFromEvent<T extends Event>(
element: Element,
name: string,
options?: ListenerOptions,
) {
return new Observable<T>(subscriber => {
const handler = (event: T) => subscriber.next(event);
const cleanup = options
? _bindEventWithOptions(this._renderer, element, name, handler, options)
: this._renderer.listen(element, name, handler, options);
return () => {
cleanup();
subscriber.complete();
};
});
}

private _listenForTableEvents(): void {
const element = this.elementRef.nativeElement;
const toClosest = (selector: string) =>
map((event: UIEvent) => closest(event.target, selector));

this.ngZone.runOutsideAngular(() => {
// Track mouse movement over the table to hide/show hover content.
this._observableFromEvent<MouseEvent>(element, 'mouseover')
fromEvent<MouseEvent>(element, 'mouseover')
.pipe(toClosest(ROW_SELECTOR), takeUntil(this.destroyed))
.subscribe(this.editEventDispatcher.hovering);
this._observableFromEvent<MouseEvent>(element, 'mouseleave')
fromEvent<MouseEvent>(element, 'mouseleave')
.pipe(mapTo(null), takeUntil(this.destroyed))
.subscribe(this.editEventDispatcher.hovering);
this._observableFromEvent<MouseEvent>(element, 'mousemove')
fromEvent<MouseEvent>(element, 'mousemove')
.pipe(
throttleTime(MOUSE_MOVE_THROTTLE_TIME_MS),
toClosest(ROW_SELECTOR),
Expand All @@ -137,15 +116,19 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
.subscribe(this.editEventDispatcher.mouseMove);

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

merge(
this._observableFromEvent(element, 'blur', {capture: true}),
this._observableFromEvent<KeyboardEvent>(element, 'keydown').pipe(
filter(event => event.key === 'Escape'),
fromEventPattern<FocusEvent>(
handler => element.addEventListener('blur', handler, true),
handler => element.removeEventListener('blur', handler, true),
),
fromEvent<KeyboardEvent>(element, 'keydown').pipe(filter(event => event.key === 'Escape')),
)
.pipe(mapTo(null), share(), takeUntil(this.destroyed))
.subscribe(this.editEventDispatcher.focused);
Expand All @@ -167,7 +150,7 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
)
.subscribe(this.editEventDispatcher.allRows);

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

// Keydown must be used here or else key auto-repeat does not work properly on some platforms.
this._observableFromEvent<KeyboardEvent>(element, 'keydown')
fromEvent<KeyboardEvent>(element, 'keydown')
.pipe(takeUntil(this.destroyed))
.subscribe(this.focusDispatcher.keyObserver);
});
Expand Down
94 changes: 45 additions & 49 deletions src/cdk/a11y/focus-monitor/focus-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

import {
Platform,
normalizePassiveListenerOptions,
_getShadowRoot,
_getEventTarget,
_bindEventWithOptions,
} from '@angular/cdk/platform';
import {
Directive,
Expand All @@ -23,7 +23,6 @@ import {
Output,
AfterViewInit,
inject,
RendererFactory2,
} from '@angular/core';
import {Observable, of as observableOf, Subject, Subscription} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
Expand Down Expand Up @@ -77,18 +76,16 @@ type MonitoredElementInfo = {
* Event listener options that enable capturing and also
* mark the listener as passive if the browser supports it.
*/
const captureEventListenerOptions = {
const captureEventListenerOptions = normalizePassiveListenerOptions({
passive: true,
capture: true,
};
});

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

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

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

/** Use defaultView of injected document if available or fallback to global window reference */
private _getWindow(): Window {
const doc = this._getDocument();
return doc.defaultView || window;
}

private _getFocusOrigin(focusEventTarget: HTMLElement | null): FocusOrigin {
if (this._origin) {
// If the origin was realized via a touch interaction, we need to perform additional checks
Expand Down Expand Up @@ -471,45 +468,32 @@ export class FocusMonitor implements OnDestroy {
}

const rootNode = elementInfo.rootNode;
const listeners = this._rootNodeFocusListeners.get(rootNode);
const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode) || 0;

if (listeners) {
listeners.count++;
} else {
if (!rootNodeFocusListeners) {
this._ngZone.runOutsideAngular(() => {
this._rootNodeFocusListeners.set(rootNode, {
count: 1,
cleanups: [
_bindEventWithOptions(
this._renderer,
rootNode,
'focus',
this._rootNodeFocusAndBlurListener,
captureEventListenerOptions,
),
_bindEventWithOptions(
this._renderer,
rootNode,
'blur',
this._rootNodeFocusAndBlurListener,
captureEventListenerOptions,
),
],
});
rootNode.addEventListener(
'focus',
this._rootNodeFocusAndBlurListener,
captureEventListenerOptions,
);
rootNode.addEventListener(
'blur',
this._rootNodeFocusAndBlurListener,
captureEventListenerOptions,
);
});
}

this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners + 1);

// Register global listeners when first element is monitored.
if (++this._monitoredElementCount === 1) {
// Note: we listen to events in the capture phase so we
// can detect them even if the user stops propagation.
this._ngZone.runOutsideAngular(() => {
this._cleanupWindowFocus?.();
this._cleanupWindowFocus = this._renderer.listen(
'window',
'focus',
this._windowFocusListener,
);
const window = this._getWindow();
window.addEventListener('focus', this._windowFocusListener);
});

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

private _removeGlobalListeners(elementInfo: MonitoredElementInfo) {
const listeners = this._rootNodeFocusListeners.get(elementInfo.rootNode);
const rootNode = elementInfo.rootNode;

if (listeners) {
if (listeners.count > 1) {
listeners.count--;
if (this._rootNodeFocusListenerCount.has(rootNode)) {
const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode)!;

if (rootNodeFocusListeners > 1) {
this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners - 1);
} else {
listeners.cleanups.forEach(cleanup => cleanup());
this._rootNodeFocusListeners.delete(elementInfo.rootNode);
rootNode.removeEventListener(
'focus',
this._rootNodeFocusAndBlurListener,
captureEventListenerOptions,
);
rootNode.removeEventListener(
'blur',
this._rootNodeFocusAndBlurListener,
captureEventListenerOptions,
);
this._rootNodeFocusListenerCount.delete(rootNode);
}
}

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

// Equivalently, stop our InputModalityDetector subscription.
this._stopInputModalityDetector.next();
Expand Down
53 changes: 14 additions & 39 deletions src/cdk/a11y/input-modality/input-modality-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,8 @@
*/

import {ALT, CONTROL, MAC_META, META, SHIFT} from '@angular/cdk/keycodes';
import {
Injectable,
InjectionToken,
OnDestroy,
NgZone,
inject,
RendererFactory2,
} from '@angular/core';
import {Platform, _bindEventWithOptions, _getEventTarget} from '@angular/cdk/platform';
import {Injectable, InjectionToken, OnDestroy, NgZone, inject} from '@angular/core';
import {normalizePassiveListenerOptions, Platform, _getEventTarget} from '@angular/cdk/platform';
import {DOCUMENT} from '@angular/common';
import {BehaviorSubject, Observable} from 'rxjs';
import {distinctUntilChanged, skip} from 'rxjs/operators';
Expand Down Expand Up @@ -76,10 +69,10 @@ export const TOUCH_BUFFER_MS = 650;
* Event listener options that enable capturing and also mark the listener as passive if the browser
* supports it.
*/
const modalityEventListenerOptions = {
const modalityEventListenerOptions = normalizePassiveListenerOptions({
passive: true,
capture: true,
};
});

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

/** Emits whenever an input modality is detected. */
readonly modalityDetected: Observable<InputModality>;
Expand Down Expand Up @@ -201,38 +193,21 @@ export class InputModalityDetector implements OnDestroy {
// If we're not in a browser, this service should do nothing, as there's no relevant input
// modality to detect.
if (this._platform.isBrowser) {
const renderer = inject(RendererFactory2).createRenderer(null, null);

this._listenerCleanups = ngZone.runOutsideAngular(() => {
return [
_bindEventWithOptions(
renderer,
document,
'keydown',
this._onKeydown,
modalityEventListenerOptions,
),
_bindEventWithOptions(
renderer,
document,
'mousedown',
this._onMousedown,
modalityEventListenerOptions,
),
_bindEventWithOptions(
renderer,
document,
'touchstart',
this._onTouchstart,
modalityEventListenerOptions,
),
];
ngZone.runOutsideAngular(() => {
document.addEventListener('keydown', this._onKeydown, modalityEventListenerOptions);
document.addEventListener('mousedown', this._onMousedown, modalityEventListenerOptions);
document.addEventListener('touchstart', this._onTouchstart, modalityEventListenerOptions);
});
}
}

ngOnDestroy() {
this._modality.complete();
this._listenerCleanups?.forEach(cleanup => cleanup());

if (this._platform.isBrowser) {
document.removeEventListener('keydown', this._onKeydown, modalityEventListenerOptions);
document.removeEventListener('mousedown', this._onMousedown, modalityEventListenerOptions);
document.removeEventListener('touchstart', this._onTouchstart, modalityEventListenerOptions);
}
}
}
Loading

0 comments on commit 93912ad

Please sign in to comment.