Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Roll back event changes #30212

Merged
merged 2 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading