623 lines
30 KiB
JavaScript
Executable File
623 lines
30 KiB
JavaScript
Executable File
import * as i0 from '@angular/core';
|
|
import { InjectionToken, inject, NgZone, DOCUMENT, RendererFactory2, Injectable, ElementRef, EventEmitter, Directive, Output } from '@angular/core';
|
|
import { BehaviorSubject, Subject, of } from 'rxjs';
|
|
import { skip, distinctUntilChanged, takeUntil } from 'rxjs/operators';
|
|
import { isFakeMousedownFromScreenReader, isFakeTouchstartFromScreenReader } from './fake-event-detection.mjs';
|
|
import { ALT, CONTROL, MAC_META, META, SHIFT } from './keycodes2.mjs';
|
|
import { _getEventTarget, _getShadowRoot } from './shadow-dom.mjs';
|
|
import { Platform } from './platform2.mjs';
|
|
import { normalizePassiveListenerOptions } from './passive-listeners.mjs';
|
|
import { coerceElement } from './element.mjs';
|
|
|
|
/**
|
|
* Injectable options for the InputModalityDetector. These are shallowly merged with the default
|
|
* options.
|
|
*/
|
|
const INPUT_MODALITY_DETECTOR_OPTIONS = new InjectionToken('cdk-input-modality-detector-options');
|
|
/**
|
|
* Default options for the InputModalityDetector.
|
|
*
|
|
* Modifier keys are ignored by default (i.e. when pressed won't cause the service to detect
|
|
* keyboard input modality) for two reasons:
|
|
*
|
|
* 1. Modifier keys are commonly used with mouse to perform actions such as 'right click' or 'open
|
|
* in new tab', and are thus less representative of actual keyboard interaction.
|
|
* 2. VoiceOver triggers some keyboard events when linearly navigating with Control + Option (but
|
|
* confusingly not with Caps Lock). Thus, to have parity with other screen readers, we ignore
|
|
* these keys so as to not update the input modality.
|
|
*
|
|
* Note that we do not by default ignore the right Meta key on Safari because it has the same key
|
|
* code as the ContextMenu key on other browsers. When we switch to using event.key, we can
|
|
* distinguish between the two.
|
|
*/
|
|
const INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS = {
|
|
ignoreKeys: [ALT, CONTROL, MAC_META, META, SHIFT],
|
|
};
|
|
/**
|
|
* The amount of time needed to pass after a touchstart event in order for a subsequent mousedown
|
|
* event to be attributed as mouse and not touch.
|
|
*
|
|
* This is the value used by AngularJS Material. Through trial and error (on iPhone 6S) they found
|
|
* that a value of around 650ms seems appropriate.
|
|
*/
|
|
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 = {
|
|
passive: true,
|
|
capture: true,
|
|
};
|
|
/**
|
|
* Service that detects the user's input modality.
|
|
*
|
|
* This service does not update the input modality when a user navigates with a screen reader
|
|
* (e.g. linear navigation with VoiceOver, object navigation / browse mode with NVDA, virtual PC
|
|
* cursor mode with JAWS). This is in part due to technical limitations (i.e. keyboard events do not
|
|
* fire as expected in these modes) but is also arguably the correct behavior. Navigating with a
|
|
* screen reader is akin to visually scanning a page, and should not be interpreted as actual user
|
|
* input interaction.
|
|
*
|
|
* When a user is not navigating but *interacting* with a screen reader, this service attempts to
|
|
* update the input modality to keyboard, but in general this service's behavior is largely
|
|
* undefined.
|
|
*/
|
|
class InputModalityDetector {
|
|
_platform = inject(Platform);
|
|
_listenerCleanups;
|
|
/** Emits whenever an input modality is detected. */
|
|
modalityDetected;
|
|
/** Emits when the input modality changes. */
|
|
modalityChanged;
|
|
/** The most recently detected input modality. */
|
|
get mostRecentModality() {
|
|
return this._modality.value;
|
|
}
|
|
/**
|
|
* The most recently detected input modality event target. Is null if no input modality has been
|
|
* detected or if the associated event target is null for some unknown reason.
|
|
*/
|
|
_mostRecentTarget = null;
|
|
/** The underlying BehaviorSubject that emits whenever an input modality is detected. */
|
|
_modality = new BehaviorSubject(null);
|
|
/** Options for this InputModalityDetector. */
|
|
_options;
|
|
/**
|
|
* The timestamp of the last touch input modality. Used to determine whether mousedown events
|
|
* should be attributed to mouse or touch.
|
|
*/
|
|
_lastTouchMs = 0;
|
|
/**
|
|
* Handles keydown events. Must be an arrow function in order to preserve the context when it gets
|
|
* bound.
|
|
*/
|
|
_onKeydown = (event) => {
|
|
// If this is one of the keys we should ignore, then ignore it and don't update the input
|
|
// modality to keyboard.
|
|
if (this._options?.ignoreKeys?.some(keyCode => keyCode === event.keyCode)) {
|
|
return;
|
|
}
|
|
this._modality.next('keyboard');
|
|
this._mostRecentTarget = _getEventTarget(event);
|
|
};
|
|
/**
|
|
* Handles mousedown events. Must be an arrow function in order to preserve the context when it
|
|
* gets bound.
|
|
*/
|
|
_onMousedown = (event) => {
|
|
// Touches trigger both touch and mouse events, so we need to distinguish between mouse events
|
|
// that were triggered via mouse vs touch. To do so, check if the mouse event occurs closely
|
|
// after the previous touch event.
|
|
if (Date.now() - this._lastTouchMs < TOUCH_BUFFER_MS) {
|
|
return;
|
|
}
|
|
// Fake mousedown events are fired by some screen readers when controls are activated by the
|
|
// screen reader. Attribute them to keyboard input modality.
|
|
this._modality.next(isFakeMousedownFromScreenReader(event) ? 'keyboard' : 'mouse');
|
|
this._mostRecentTarget = _getEventTarget(event);
|
|
};
|
|
/**
|
|
* Handles touchstart events. Must be an arrow function in order to preserve the context when it
|
|
* gets bound.
|
|
*/
|
|
_onTouchstart = (event) => {
|
|
// Same scenario as mentioned in _onMousedown, but on touch screen devices, fake touchstart
|
|
// events are fired. Again, attribute to keyboard input modality.
|
|
if (isFakeTouchstartFromScreenReader(event)) {
|
|
this._modality.next('keyboard');
|
|
return;
|
|
}
|
|
// Store the timestamp of this touch event, as it's used to distinguish between mouse events
|
|
// triggered via mouse vs touch.
|
|
this._lastTouchMs = Date.now();
|
|
this._modality.next('touch');
|
|
this._mostRecentTarget = _getEventTarget(event);
|
|
};
|
|
constructor() {
|
|
const ngZone = inject(NgZone);
|
|
const document = inject(DOCUMENT);
|
|
const options = inject(INPUT_MODALITY_DETECTOR_OPTIONS, { optional: true });
|
|
this._options = {
|
|
...INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS,
|
|
...options,
|
|
};
|
|
// Skip the first emission as it's null.
|
|
this.modalityDetected = this._modality.pipe(skip(1));
|
|
this.modalityChanged = this.modalityDetected.pipe(distinctUntilChanged());
|
|
// 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 [
|
|
renderer.listen(document, 'keydown', this._onKeydown, modalityEventListenerOptions),
|
|
renderer.listen(document, 'mousedown', this._onMousedown, modalityEventListenerOptions),
|
|
renderer.listen(document, 'touchstart', this._onTouchstart, modalityEventListenerOptions),
|
|
];
|
|
});
|
|
}
|
|
}
|
|
ngOnDestroy() {
|
|
this._modality.complete();
|
|
this._listenerCleanups?.forEach(cleanup => cleanup());
|
|
}
|
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: InputModalityDetector, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: InputModalityDetector, providedIn: 'root' });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: InputModalityDetector, decorators: [{
|
|
type: Injectable,
|
|
args: [{ providedIn: 'root' }]
|
|
}], ctorParameters: () => [] });
|
|
|
|
/** Detection mode used for attributing the origin of a focus event. */
|
|
var FocusMonitorDetectionMode;
|
|
(function (FocusMonitorDetectionMode) {
|
|
/**
|
|
* Any mousedown, keydown, or touchstart event that happened in the previous
|
|
* tick or the current tick will be used to assign a focus event's origin (to
|
|
* either mouse, keyboard, or touch). This is the default option.
|
|
*/
|
|
FocusMonitorDetectionMode[FocusMonitorDetectionMode["IMMEDIATE"] = 0] = "IMMEDIATE";
|
|
/**
|
|
* A focus event's origin is always attributed to the last corresponding
|
|
* mousedown, keydown, or touchstart event, no matter how long ago it occurred.
|
|
*/
|
|
FocusMonitorDetectionMode[FocusMonitorDetectionMode["EVENTUAL"] = 1] = "EVENTUAL";
|
|
})(FocusMonitorDetectionMode || (FocusMonitorDetectionMode = {}));
|
|
/** InjectionToken for FocusMonitorOptions. */
|
|
const FOCUS_MONITOR_DEFAULT_OPTIONS = new InjectionToken('cdk-focus-monitor-default-options');
|
|
/**
|
|
* Event listener options that enable capturing and also
|
|
* mark the listener as passive if the browser supports it.
|
|
*/
|
|
const captureEventListenerOptions = normalizePassiveListenerOptions({
|
|
passive: true,
|
|
capture: true,
|
|
});
|
|
/** Monitors mouse and keyboard events to determine the cause of focus events. */
|
|
class FocusMonitor {
|
|
_ngZone = inject(NgZone);
|
|
_platform = inject(Platform);
|
|
_inputModalityDetector = inject(InputModalityDetector);
|
|
/** The focus origin that the next focus event is a result of. */
|
|
_origin = null;
|
|
/** The FocusOrigin of the last focus event tracked by the FocusMonitor. */
|
|
_lastFocusOrigin;
|
|
/** Whether the window has just been focused. */
|
|
_windowFocused = false;
|
|
/** The timeout id of the window focus timeout. */
|
|
_windowFocusTimeoutId;
|
|
/** The timeout id of the origin clearing timeout. */
|
|
_originTimeoutId;
|
|
/**
|
|
* Whether the origin was determined via a touch interaction. Necessary as properly attributing
|
|
* focus events to touch interactions requires special logic.
|
|
*/
|
|
_originFromTouchInteraction = false;
|
|
/** Map of elements being monitored to their info. */
|
|
_elementInfo = new Map();
|
|
/** The number of elements currently being monitored. */
|
|
_monitoredElementCount = 0;
|
|
/**
|
|
* Keeps track of the root nodes to which we've currently bound a focus/blur handler,
|
|
* as well as the number of monitored elements that they contain. We have to treat focus/blur
|
|
* 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.
|
|
*/
|
|
_rootNodeFocusListenerCount = new Map();
|
|
/**
|
|
* The specified detection mode, used for attributing the origin of a focus
|
|
* event.
|
|
*/
|
|
_detectionMode;
|
|
/**
|
|
* Event listener for `focus` events on the window.
|
|
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
*/
|
|
_windowFocusListener = () => {
|
|
// Make a note of when the window regains focus, so we can
|
|
// restore the origin info for the focused element.
|
|
this._windowFocused = true;
|
|
this._windowFocusTimeoutId = setTimeout(() => (this._windowFocused = false));
|
|
};
|
|
/** Used to reference correct document/window */
|
|
_document = inject(DOCUMENT);
|
|
/** Subject for stopping our InputModalityDetector subscription. */
|
|
_stopInputModalityDetector = new Subject();
|
|
constructor() {
|
|
const options = inject(FOCUS_MONITOR_DEFAULT_OPTIONS, {
|
|
optional: true,
|
|
});
|
|
this._detectionMode = options?.detectionMode || FocusMonitorDetectionMode.IMMEDIATE;
|
|
}
|
|
/**
|
|
* Event listener for `focus` and 'blur' events on the document.
|
|
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
*/
|
|
_rootNodeFocusAndBlurListener = (event) => {
|
|
const target = _getEventTarget(event);
|
|
// We need to walk up the ancestor chain in order to support `checkChildren`.
|
|
for (let element = target; element; element = element.parentElement) {
|
|
if (event.type === 'focus') {
|
|
this._onFocus(event, element);
|
|
}
|
|
else {
|
|
this._onBlur(event, element);
|
|
}
|
|
}
|
|
};
|
|
monitor(element, checkChildren = false) {
|
|
const nativeElement = coerceElement(element);
|
|
// Do nothing if we're not on the browser platform or the passed in node isn't an element.
|
|
if (!this._platform.isBrowser || nativeElement.nodeType !== 1) {
|
|
// Note: we don't want the observable to emit at all so we don't pass any parameters.
|
|
return of();
|
|
}
|
|
// If the element is inside the shadow DOM, we need to bind our focus/blur listeners to
|
|
// the shadow root, rather than the `document`, because the browser won't emit focus events
|
|
// to the `document`, if focus is moving within the same shadow root.
|
|
const rootNode = _getShadowRoot(nativeElement) || this._document;
|
|
const cachedInfo = this._elementInfo.get(nativeElement);
|
|
// Check if we're already monitoring this element.
|
|
if (cachedInfo) {
|
|
if (checkChildren) {
|
|
// TODO(COMP-318): this can be problematic, because it'll turn all non-checkChildren
|
|
// observers into ones that behave as if `checkChildren` was turned on. We need a more
|
|
// robust solution.
|
|
cachedInfo.checkChildren = true;
|
|
}
|
|
return cachedInfo.subject;
|
|
}
|
|
// Create monitored element info.
|
|
const info = {
|
|
checkChildren: checkChildren,
|
|
subject: new Subject(),
|
|
rootNode,
|
|
};
|
|
this._elementInfo.set(nativeElement, info);
|
|
this._registerGlobalListeners(info);
|
|
return info.subject;
|
|
}
|
|
stopMonitoring(element) {
|
|
const nativeElement = coerceElement(element);
|
|
const elementInfo = this._elementInfo.get(nativeElement);
|
|
if (elementInfo) {
|
|
elementInfo.subject.complete();
|
|
this._setClasses(nativeElement);
|
|
this._elementInfo.delete(nativeElement);
|
|
this._removeGlobalListeners(elementInfo);
|
|
}
|
|
}
|
|
focusVia(element, origin, options) {
|
|
const nativeElement = coerceElement(element);
|
|
const focusedElement = this._document.activeElement;
|
|
// If the element is focused already, calling `focus` again won't trigger the event listener
|
|
// which means that the focus classes won't be updated. If that's the case, update the classes
|
|
// directly without waiting for an event.
|
|
if (nativeElement === focusedElement) {
|
|
this._getClosestElementsInfo(nativeElement).forEach(([currentElement, info]) => this._originChanged(currentElement, origin, info));
|
|
}
|
|
else {
|
|
this._setOrigin(origin);
|
|
// `focus` isn't available on the server
|
|
if (typeof nativeElement.focus === 'function') {
|
|
nativeElement.focus(options);
|
|
}
|
|
}
|
|
}
|
|
ngOnDestroy() {
|
|
this._elementInfo.forEach((_info, element) => this.stopMonitoring(element));
|
|
}
|
|
/** Use defaultView of injected document if available or fallback to global window reference */
|
|
_getWindow() {
|
|
return this._document.defaultView || window;
|
|
}
|
|
_getFocusOrigin(focusEventTarget) {
|
|
if (this._origin) {
|
|
// If the origin was realized via a touch interaction, we need to perform additional checks
|
|
// to determine whether the focus origin should be attributed to touch or program.
|
|
if (this._originFromTouchInteraction) {
|
|
return this._shouldBeAttributedToTouch(focusEventTarget) ? 'touch' : 'program';
|
|
}
|
|
else {
|
|
return this._origin;
|
|
}
|
|
}
|
|
// If the window has just regained focus, we can restore the most recent origin from before the
|
|
// window blurred. Otherwise, we've reached the point where we can't identify the source of the
|
|
// focus. This typically means one of two things happened:
|
|
//
|
|
// 1) The element was programmatically focused, or
|
|
// 2) The element was focused via screen reader navigation (which generally doesn't fire
|
|
// events).
|
|
//
|
|
// Because we can't distinguish between these two cases, we default to setting `program`.
|
|
if (this._windowFocused && this._lastFocusOrigin) {
|
|
return this._lastFocusOrigin;
|
|
}
|
|
// If the interaction is coming from an input label, we consider it a mouse interactions.
|
|
// This is a special case where focus moves on `click`, rather than `mousedown` which breaks
|
|
// our detection, because all our assumptions are for `mousedown`. We need to handle this
|
|
// special case, because it's very common for checkboxes and radio buttons.
|
|
if (focusEventTarget && this._isLastInteractionFromInputLabel(focusEventTarget)) {
|
|
return 'mouse';
|
|
}
|
|
return 'program';
|
|
}
|
|
/**
|
|
* Returns whether the focus event should be attributed to touch. Recall that in IMMEDIATE mode, a
|
|
* touch origin isn't immediately reset at the next tick (see _setOrigin). This means that when we
|
|
* handle a focus event following a touch interaction, we need to determine whether (1) the focus
|
|
* event was directly caused by the touch interaction or (2) the focus event was caused by a
|
|
* subsequent programmatic focus call triggered by the touch interaction.
|
|
* @param focusEventTarget The target of the focus event under examination.
|
|
*/
|
|
_shouldBeAttributedToTouch(focusEventTarget) {
|
|
// Please note that this check is not perfect. Consider the following edge case:
|
|
//
|
|
// <div #parent tabindex="0">
|
|
// <div #child tabindex="0" (click)="#parent.focus()"></div>
|
|
// </div>
|
|
//
|
|
// Suppose there is a FocusMonitor in IMMEDIATE mode attached to #parent. When the user touches
|
|
// #child, #parent is programmatically focused. This code will attribute the focus to touch
|
|
// instead of program. This is a relatively minor edge-case that can be worked around by using
|
|
// focusVia(parent, 'program') to focus #parent.
|
|
return (this._detectionMode === FocusMonitorDetectionMode.EVENTUAL ||
|
|
!!focusEventTarget?.contains(this._inputModalityDetector._mostRecentTarget));
|
|
}
|
|
/**
|
|
* Sets the focus classes on the element based on the given focus origin.
|
|
* @param element The element to update the classes on.
|
|
* @param origin The focus origin.
|
|
*/
|
|
_setClasses(element, origin) {
|
|
element.classList.toggle('cdk-focused', !!origin);
|
|
element.classList.toggle('cdk-touch-focused', origin === 'touch');
|
|
element.classList.toggle('cdk-keyboard-focused', origin === 'keyboard');
|
|
element.classList.toggle('cdk-mouse-focused', origin === 'mouse');
|
|
element.classList.toggle('cdk-program-focused', origin === 'program');
|
|
}
|
|
/**
|
|
* Updates the focus origin. If we're using immediate detection mode, we schedule an async
|
|
* function to clear the origin at the end of a timeout. The duration of the timeout depends on
|
|
* the origin being set.
|
|
* @param origin The origin to set.
|
|
* @param isFromInteraction Whether we are setting the origin from an interaction event.
|
|
*/
|
|
_setOrigin(origin, isFromInteraction = false) {
|
|
this._ngZone.runOutsideAngular(() => {
|
|
this._origin = origin;
|
|
this._originFromTouchInteraction = origin === 'touch' && isFromInteraction;
|
|
// If we're in IMMEDIATE mode, reset the origin at the next tick (or in `TOUCH_BUFFER_MS` ms
|
|
// for a touch event). We reset the origin at the next tick because Firefox focuses one tick
|
|
// after the interaction event. We wait `TOUCH_BUFFER_MS` ms before resetting the origin for
|
|
// a touch event because when a touch event is fired, the associated focus event isn't yet in
|
|
// the event queue. Before doing so, clear any pending timeouts.
|
|
if (this._detectionMode === FocusMonitorDetectionMode.IMMEDIATE) {
|
|
clearTimeout(this._originTimeoutId);
|
|
const ms = this._originFromTouchInteraction ? TOUCH_BUFFER_MS : 1;
|
|
this._originTimeoutId = setTimeout(() => (this._origin = null), ms);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Handles focus events on a registered element.
|
|
* @param event The focus event.
|
|
* @param element The monitored element.
|
|
*/
|
|
_onFocus(event, element) {
|
|
// NOTE(mmalerba): We currently set the classes based on the focus origin of the most recent
|
|
// focus event affecting the monitored element. If we want to use the origin of the first event
|
|
// instead we should check for the cdk-focused class here and return if the element already has
|
|
// it. (This only matters for elements that have includesChildren = true).
|
|
// If we are not counting child-element-focus as focused, make sure that the event target is the
|
|
// monitored element itself.
|
|
const elementInfo = this._elementInfo.get(element);
|
|
const focusEventTarget = _getEventTarget(event);
|
|
if (!elementInfo || (!elementInfo.checkChildren && element !== focusEventTarget)) {
|
|
return;
|
|
}
|
|
this._originChanged(element, this._getFocusOrigin(focusEventTarget), elementInfo);
|
|
}
|
|
/**
|
|
* Handles blur events on a registered element.
|
|
* @param event The blur event.
|
|
* @param element The monitored element.
|
|
*/
|
|
_onBlur(event, element) {
|
|
// If we are counting child-element-focus as focused, make sure that we aren't just blurring in
|
|
// order to focus another child of the monitored element.
|
|
const elementInfo = this._elementInfo.get(element);
|
|
if (!elementInfo ||
|
|
(elementInfo.checkChildren &&
|
|
event.relatedTarget instanceof Node &&
|
|
element.contains(event.relatedTarget))) {
|
|
return;
|
|
}
|
|
this._setClasses(element);
|
|
this._emitOrigin(elementInfo, null);
|
|
}
|
|
_emitOrigin(info, origin) {
|
|
if (info.subject.observers.length) {
|
|
this._ngZone.run(() => info.subject.next(origin));
|
|
}
|
|
}
|
|
_registerGlobalListeners(elementInfo) {
|
|
if (!this._platform.isBrowser) {
|
|
return;
|
|
}
|
|
const rootNode = elementInfo.rootNode;
|
|
const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode) || 0;
|
|
if (!rootNodeFocusListeners) {
|
|
this._ngZone.runOutsideAngular(() => {
|
|
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(() => {
|
|
const window = this._getWindow();
|
|
window.addEventListener('focus', this._windowFocusListener);
|
|
});
|
|
// The InputModalityDetector is also just a collection of global listeners.
|
|
this._inputModalityDetector.modalityDetected
|
|
.pipe(takeUntil(this._stopInputModalityDetector))
|
|
.subscribe(modality => {
|
|
this._setOrigin(modality, true /* isFromInteraction */);
|
|
});
|
|
}
|
|
}
|
|
_removeGlobalListeners(elementInfo) {
|
|
const rootNode = elementInfo.rootNode;
|
|
if (this._rootNodeFocusListenerCount.has(rootNode)) {
|
|
const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode);
|
|
if (rootNodeFocusListeners > 1) {
|
|
this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners - 1);
|
|
}
|
|
else {
|
|
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) {
|
|
const window = this._getWindow();
|
|
window.removeEventListener('focus', this._windowFocusListener);
|
|
// Equivalently, stop our InputModalityDetector subscription.
|
|
this._stopInputModalityDetector.next();
|
|
// Clear timeouts for all potentially pending timeouts to prevent the leaks.
|
|
clearTimeout(this._windowFocusTimeoutId);
|
|
clearTimeout(this._originTimeoutId);
|
|
}
|
|
}
|
|
/** Updates all the state on an element once its focus origin has changed. */
|
|
_originChanged(element, origin, elementInfo) {
|
|
this._setClasses(element, origin);
|
|
this._emitOrigin(elementInfo, origin);
|
|
this._lastFocusOrigin = origin;
|
|
}
|
|
/**
|
|
* Collects the `MonitoredElementInfo` of a particular element and
|
|
* all of its ancestors that have enabled `checkChildren`.
|
|
* @param element Element from which to start the search.
|
|
*/
|
|
_getClosestElementsInfo(element) {
|
|
const results = [];
|
|
this._elementInfo.forEach((info, currentElement) => {
|
|
if (currentElement === element || (info.checkChildren && currentElement.contains(element))) {
|
|
results.push([currentElement, info]);
|
|
}
|
|
});
|
|
return results;
|
|
}
|
|
/**
|
|
* Returns whether an interaction is likely to have come from the user clicking the `label` of
|
|
* an `input` or `textarea` in order to focus it.
|
|
* @param focusEventTarget Target currently receiving focus.
|
|
*/
|
|
_isLastInteractionFromInputLabel(focusEventTarget) {
|
|
const { _mostRecentTarget: mostRecentTarget, mostRecentModality } = this._inputModalityDetector;
|
|
// If the last interaction used the mouse on an element contained by one of the labels
|
|
// of an `input`/`textarea` that is currently focused, it is very likely that the
|
|
// user redirected focus using the label.
|
|
if (mostRecentModality !== 'mouse' ||
|
|
!mostRecentTarget ||
|
|
mostRecentTarget === focusEventTarget ||
|
|
(focusEventTarget.nodeName !== 'INPUT' && focusEventTarget.nodeName !== 'TEXTAREA') ||
|
|
focusEventTarget.disabled) {
|
|
return false;
|
|
}
|
|
const labels = focusEventTarget.labels;
|
|
if (labels) {
|
|
for (let i = 0; i < labels.length; i++) {
|
|
if (labels[i].contains(mostRecentTarget)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: FocusMonitor, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: FocusMonitor, providedIn: 'root' });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: FocusMonitor, decorators: [{
|
|
type: Injectable,
|
|
args: [{ providedIn: 'root' }]
|
|
}], ctorParameters: () => [] });
|
|
/**
|
|
* Directive that determines how a particular element was focused (via keyboard, mouse, touch, or
|
|
* programmatically) and adds corresponding classes to the element.
|
|
*
|
|
* There are two variants of this directive:
|
|
* 1) cdkMonitorElementFocus: does not consider an element to be focused if one of its children is
|
|
* focused.
|
|
* 2) cdkMonitorSubtreeFocus: considers an element focused if it or any of its children are focused.
|
|
*/
|
|
class CdkMonitorFocus {
|
|
_elementRef = inject(ElementRef);
|
|
_focusMonitor = inject(FocusMonitor);
|
|
_monitorSubscription;
|
|
_focusOrigin = null;
|
|
cdkFocusChange = new EventEmitter();
|
|
constructor() { }
|
|
get focusOrigin() {
|
|
return this._focusOrigin;
|
|
}
|
|
ngAfterViewInit() {
|
|
const element = this._elementRef.nativeElement;
|
|
this._monitorSubscription = this._focusMonitor
|
|
.monitor(element, element.nodeType === 1 && element.hasAttribute('cdkMonitorSubtreeFocus'))
|
|
.subscribe(origin => {
|
|
this._focusOrigin = origin;
|
|
this.cdkFocusChange.emit(origin);
|
|
});
|
|
}
|
|
ngOnDestroy() {
|
|
this._focusMonitor.stopMonitoring(this._elementRef);
|
|
if (this._monitorSubscription) {
|
|
this._monitorSubscription.unsubscribe();
|
|
}
|
|
}
|
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: CdkMonitorFocus, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.0-next.2", type: CdkMonitorFocus, isStandalone: true, selector: "[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]", outputs: { cdkFocusChange: "cdkFocusChange" }, exportAs: ["cdkMonitorFocus"], ngImport: i0 });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: CdkMonitorFocus, decorators: [{
|
|
type: Directive,
|
|
args: [{
|
|
selector: '[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]',
|
|
exportAs: 'cdkMonitorFocus',
|
|
}]
|
|
}], ctorParameters: () => [], propDecorators: { cdkFocusChange: [{
|
|
type: Output
|
|
}] } });
|
|
|
|
export { CdkMonitorFocus, FOCUS_MONITOR_DEFAULT_OPTIONS, FocusMonitor, FocusMonitorDetectionMode, INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS, INPUT_MODALITY_DETECTOR_OPTIONS, InputModalityDetector };
|
|
//# sourceMappingURL=focus-monitor.mjs.map
|