import * as i0 from '@angular/core'; import { InjectionToken, inject, NgZone, RendererFactory2, Injectable, ElementRef, EventEmitter, Directive, Output } from '@angular/core'; import { BehaviorSubject, Subject, of } from 'rxjs'; import { skip, distinctUntilChanged, takeUntil } from 'rxjs/operators'; import { DOCUMENT } from '@angular/common'; import { i as isFakeMousedownFromScreenReader, a as isFakeTouchstartFromScreenReader } from './fake-event-detection-DWOdFTFz.mjs'; import { d as ALT, C as CONTROL, M as MAC_META, e as META, f as SHIFT } from './keycodes-CpHkExLC.mjs'; import { _ as _getEventTarget, a as _getShadowRoot } from './shadow-dom-B0oHn41l.mjs'; import { _ as _bindEventWithOptions } from './backwards-compatibility-DHR38MsD.mjs'; import { P as Platform } from './platform-DmdVEw_C.mjs'; import { n as normalizePassiveListenerOptions } from './passive-listeners-esHZRgIN.mjs'; import { a as coerceElement } from './element-x4z00URv.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 [ _bindEventWithOptions(renderer, document, 'keydown', this._onKeydown, modalityEventListenerOptions), _bindEventWithOptions(renderer, document, 'mousedown', this._onMousedown, modalityEventListenerOptions), _bindEventWithOptions(renderer, document, 'touchstart', this._onTouchstart, modalityEventListenerOptions), ]; }); } } ngOnDestroy() { this._modality.complete(); this._listenerCleanups?.forEach(cleanup => cleanup()); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: InputModalityDetector, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: InputModalityDetector, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", 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, { optional: true }); /** 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._getDocument(); 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._getDocument().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)); } /** Access injected document if available or fallback to global document reference */ _getDocument() { return this._document || document; } /** Use defaultView of injected document if available or fallback to global window reference */ _getWindow() { const doc = this._getDocument(); return doc.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: // //