focus-monitor-e2l_RpN3.mjs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. import * as i0 from '@angular/core';
  2. import { InjectionToken, inject, NgZone, RendererFactory2, Injectable, ElementRef, EventEmitter, Directive, Output } from '@angular/core';
  3. import { BehaviorSubject, Subject, of } from 'rxjs';
  4. import { skip, distinctUntilChanged, takeUntil } from 'rxjs/operators';
  5. import { DOCUMENT } from '@angular/common';
  6. import { i as isFakeMousedownFromScreenReader, a as isFakeTouchstartFromScreenReader } from './fake-event-detection-DWOdFTFz.mjs';
  7. import { d as ALT, C as CONTROL, M as MAC_META, e as META, f as SHIFT } from './keycodes-CpHkExLC.mjs';
  8. import { _ as _getEventTarget, a as _getShadowRoot } from './shadow-dom-B0oHn41l.mjs';
  9. import { _ as _bindEventWithOptions } from './backwards-compatibility-DHR38MsD.mjs';
  10. import { P as Platform } from './platform-DmdVEw_C.mjs';
  11. import { n as normalizePassiveListenerOptions } from './passive-listeners-esHZRgIN.mjs';
  12. import { a as coerceElement } from './element-x4z00URv.mjs';
  13. /**
  14. * Injectable options for the InputModalityDetector. These are shallowly merged with the default
  15. * options.
  16. */
  17. const INPUT_MODALITY_DETECTOR_OPTIONS = new InjectionToken('cdk-input-modality-detector-options');
  18. /**
  19. * Default options for the InputModalityDetector.
  20. *
  21. * Modifier keys are ignored by default (i.e. when pressed won't cause the service to detect
  22. * keyboard input modality) for two reasons:
  23. *
  24. * 1. Modifier keys are commonly used with mouse to perform actions such as 'right click' or 'open
  25. * in new tab', and are thus less representative of actual keyboard interaction.
  26. * 2. VoiceOver triggers some keyboard events when linearly navigating with Control + Option (but
  27. * confusingly not with Caps Lock). Thus, to have parity with other screen readers, we ignore
  28. * these keys so as to not update the input modality.
  29. *
  30. * Note that we do not by default ignore the right Meta key on Safari because it has the same key
  31. * code as the ContextMenu key on other browsers. When we switch to using event.key, we can
  32. * distinguish between the two.
  33. */
  34. const INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS = {
  35. ignoreKeys: [ALT, CONTROL, MAC_META, META, SHIFT],
  36. };
  37. /**
  38. * The amount of time needed to pass after a touchstart event in order for a subsequent mousedown
  39. * event to be attributed as mouse and not touch.
  40. *
  41. * This is the value used by AngularJS Material. Through trial and error (on iPhone 6S) they found
  42. * that a value of around 650ms seems appropriate.
  43. */
  44. const TOUCH_BUFFER_MS = 650;
  45. /**
  46. * Event listener options that enable capturing and also mark the listener as passive if the browser
  47. * supports it.
  48. */
  49. const modalityEventListenerOptions = {
  50. passive: true,
  51. capture: true,
  52. };
  53. /**
  54. * Service that detects the user's input modality.
  55. *
  56. * This service does not update the input modality when a user navigates with a screen reader
  57. * (e.g. linear navigation with VoiceOver, object navigation / browse mode with NVDA, virtual PC
  58. * cursor mode with JAWS). This is in part due to technical limitations (i.e. keyboard events do not
  59. * fire as expected in these modes) but is also arguably the correct behavior. Navigating with a
  60. * screen reader is akin to visually scanning a page, and should not be interpreted as actual user
  61. * input interaction.
  62. *
  63. * When a user is not navigating but *interacting* with a screen reader, this service attempts to
  64. * update the input modality to keyboard, but in general this service's behavior is largely
  65. * undefined.
  66. */
  67. class InputModalityDetector {
  68. _platform = inject(Platform);
  69. _listenerCleanups;
  70. /** Emits whenever an input modality is detected. */
  71. modalityDetected;
  72. /** Emits when the input modality changes. */
  73. modalityChanged;
  74. /** The most recently detected input modality. */
  75. get mostRecentModality() {
  76. return this._modality.value;
  77. }
  78. /**
  79. * The most recently detected input modality event target. Is null if no input modality has been
  80. * detected or if the associated event target is null for some unknown reason.
  81. */
  82. _mostRecentTarget = null;
  83. /** The underlying BehaviorSubject that emits whenever an input modality is detected. */
  84. _modality = new BehaviorSubject(null);
  85. /** Options for this InputModalityDetector. */
  86. _options;
  87. /**
  88. * The timestamp of the last touch input modality. Used to determine whether mousedown events
  89. * should be attributed to mouse or touch.
  90. */
  91. _lastTouchMs = 0;
  92. /**
  93. * Handles keydown events. Must be an arrow function in order to preserve the context when it gets
  94. * bound.
  95. */
  96. _onKeydown = (event) => {
  97. // If this is one of the keys we should ignore, then ignore it and don't update the input
  98. // modality to keyboard.
  99. if (this._options?.ignoreKeys?.some(keyCode => keyCode === event.keyCode)) {
  100. return;
  101. }
  102. this._modality.next('keyboard');
  103. this._mostRecentTarget = _getEventTarget(event);
  104. };
  105. /**
  106. * Handles mousedown events. Must be an arrow function in order to preserve the context when it
  107. * gets bound.
  108. */
  109. _onMousedown = (event) => {
  110. // Touches trigger both touch and mouse events, so we need to distinguish between mouse events
  111. // that were triggered via mouse vs touch. To do so, check if the mouse event occurs closely
  112. // after the previous touch event.
  113. if (Date.now() - this._lastTouchMs < TOUCH_BUFFER_MS) {
  114. return;
  115. }
  116. // Fake mousedown events are fired by some screen readers when controls are activated by the
  117. // screen reader. Attribute them to keyboard input modality.
  118. this._modality.next(isFakeMousedownFromScreenReader(event) ? 'keyboard' : 'mouse');
  119. this._mostRecentTarget = _getEventTarget(event);
  120. };
  121. /**
  122. * Handles touchstart events. Must be an arrow function in order to preserve the context when it
  123. * gets bound.
  124. */
  125. _onTouchstart = (event) => {
  126. // Same scenario as mentioned in _onMousedown, but on touch screen devices, fake touchstart
  127. // events are fired. Again, attribute to keyboard input modality.
  128. if (isFakeTouchstartFromScreenReader(event)) {
  129. this._modality.next('keyboard');
  130. return;
  131. }
  132. // Store the timestamp of this touch event, as it's used to distinguish between mouse events
  133. // triggered via mouse vs touch.
  134. this._lastTouchMs = Date.now();
  135. this._modality.next('touch');
  136. this._mostRecentTarget = _getEventTarget(event);
  137. };
  138. constructor() {
  139. const ngZone = inject(NgZone);
  140. const document = inject(DOCUMENT);
  141. const options = inject(INPUT_MODALITY_DETECTOR_OPTIONS, { optional: true });
  142. this._options = {
  143. ...INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS,
  144. ...options,
  145. };
  146. // Skip the first emission as it's null.
  147. this.modalityDetected = this._modality.pipe(skip(1));
  148. this.modalityChanged = this.modalityDetected.pipe(distinctUntilChanged());
  149. // If we're not in a browser, this service should do nothing, as there's no relevant input
  150. // modality to detect.
  151. if (this._platform.isBrowser) {
  152. const renderer = inject(RendererFactory2).createRenderer(null, null);
  153. this._listenerCleanups = ngZone.runOutsideAngular(() => {
  154. return [
  155. _bindEventWithOptions(renderer, document, 'keydown', this._onKeydown, modalityEventListenerOptions),
  156. _bindEventWithOptions(renderer, document, 'mousedown', this._onMousedown, modalityEventListenerOptions),
  157. _bindEventWithOptions(renderer, document, 'touchstart', this._onTouchstart, modalityEventListenerOptions),
  158. ];
  159. });
  160. }
  161. }
  162. ngOnDestroy() {
  163. this._modality.complete();
  164. this._listenerCleanups?.forEach(cleanup => cleanup());
  165. }
  166. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: InputModalityDetector, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
  167. static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: InputModalityDetector, providedIn: 'root' });
  168. }
  169. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: InputModalityDetector, decorators: [{
  170. type: Injectable,
  171. args: [{ providedIn: 'root' }]
  172. }], ctorParameters: () => [] });
  173. /** Detection mode used for attributing the origin of a focus event. */
  174. var FocusMonitorDetectionMode;
  175. (function (FocusMonitorDetectionMode) {
  176. /**
  177. * Any mousedown, keydown, or touchstart event that happened in the previous
  178. * tick or the current tick will be used to assign a focus event's origin (to
  179. * either mouse, keyboard, or touch). This is the default option.
  180. */
  181. FocusMonitorDetectionMode[FocusMonitorDetectionMode["IMMEDIATE"] = 0] = "IMMEDIATE";
  182. /**
  183. * A focus event's origin is always attributed to the last corresponding
  184. * mousedown, keydown, or touchstart event, no matter how long ago it occurred.
  185. */
  186. FocusMonitorDetectionMode[FocusMonitorDetectionMode["EVENTUAL"] = 1] = "EVENTUAL";
  187. })(FocusMonitorDetectionMode || (FocusMonitorDetectionMode = {}));
  188. /** InjectionToken for FocusMonitorOptions. */
  189. const FOCUS_MONITOR_DEFAULT_OPTIONS = new InjectionToken('cdk-focus-monitor-default-options');
  190. /**
  191. * Event listener options that enable capturing and also
  192. * mark the listener as passive if the browser supports it.
  193. */
  194. const captureEventListenerOptions = normalizePassiveListenerOptions({
  195. passive: true,
  196. capture: true,
  197. });
  198. /** Monitors mouse and keyboard events to determine the cause of focus events. */
  199. class FocusMonitor {
  200. _ngZone = inject(NgZone);
  201. _platform = inject(Platform);
  202. _inputModalityDetector = inject(InputModalityDetector);
  203. /** The focus origin that the next focus event is a result of. */
  204. _origin = null;
  205. /** The FocusOrigin of the last focus event tracked by the FocusMonitor. */
  206. _lastFocusOrigin;
  207. /** Whether the window has just been focused. */
  208. _windowFocused = false;
  209. /** The timeout id of the window focus timeout. */
  210. _windowFocusTimeoutId;
  211. /** The timeout id of the origin clearing timeout. */
  212. _originTimeoutId;
  213. /**
  214. * Whether the origin was determined via a touch interaction. Necessary as properly attributing
  215. * focus events to touch interactions requires special logic.
  216. */
  217. _originFromTouchInteraction = false;
  218. /** Map of elements being monitored to their info. */
  219. _elementInfo = new Map();
  220. /** The number of elements currently being monitored. */
  221. _monitoredElementCount = 0;
  222. /**
  223. * Keeps track of the root nodes to which we've currently bound a focus/blur handler,
  224. * as well as the number of monitored elements that they contain. We have to treat focus/blur
  225. * handlers differently from the rest of the events, because the browser won't emit events
  226. * to the document when focus moves inside of a shadow root.
  227. */
  228. _rootNodeFocusListenerCount = new Map();
  229. /**
  230. * The specified detection mode, used for attributing the origin of a focus
  231. * event.
  232. */
  233. _detectionMode;
  234. /**
  235. * Event listener for `focus` events on the window.
  236. * Needs to be an arrow function in order to preserve the context when it gets bound.
  237. */
  238. _windowFocusListener = () => {
  239. // Make a note of when the window regains focus, so we can
  240. // restore the origin info for the focused element.
  241. this._windowFocused = true;
  242. this._windowFocusTimeoutId = setTimeout(() => (this._windowFocused = false));
  243. };
  244. /** Used to reference correct document/window */
  245. _document = inject(DOCUMENT, { optional: true });
  246. /** Subject for stopping our InputModalityDetector subscription. */
  247. _stopInputModalityDetector = new Subject();
  248. constructor() {
  249. const options = inject(FOCUS_MONITOR_DEFAULT_OPTIONS, {
  250. optional: true,
  251. });
  252. this._detectionMode = options?.detectionMode || FocusMonitorDetectionMode.IMMEDIATE;
  253. }
  254. /**
  255. * Event listener for `focus` and 'blur' events on the document.
  256. * Needs to be an arrow function in order to preserve the context when it gets bound.
  257. */
  258. _rootNodeFocusAndBlurListener = (event) => {
  259. const target = _getEventTarget(event);
  260. // We need to walk up the ancestor chain in order to support `checkChildren`.
  261. for (let element = target; element; element = element.parentElement) {
  262. if (event.type === 'focus') {
  263. this._onFocus(event, element);
  264. }
  265. else {
  266. this._onBlur(event, element);
  267. }
  268. }
  269. };
  270. monitor(element, checkChildren = false) {
  271. const nativeElement = coerceElement(element);
  272. // Do nothing if we're not on the browser platform or the passed in node isn't an element.
  273. if (!this._platform.isBrowser || nativeElement.nodeType !== 1) {
  274. // Note: we don't want the observable to emit at all so we don't pass any parameters.
  275. return of();
  276. }
  277. // If the element is inside the shadow DOM, we need to bind our focus/blur listeners to
  278. // the shadow root, rather than the `document`, because the browser won't emit focus events
  279. // to the `document`, if focus is moving within the same shadow root.
  280. const rootNode = _getShadowRoot(nativeElement) || this._getDocument();
  281. const cachedInfo = this._elementInfo.get(nativeElement);
  282. // Check if we're already monitoring this element.
  283. if (cachedInfo) {
  284. if (checkChildren) {
  285. // TODO(COMP-318): this can be problematic, because it'll turn all non-checkChildren
  286. // observers into ones that behave as if `checkChildren` was turned on. We need a more
  287. // robust solution.
  288. cachedInfo.checkChildren = true;
  289. }
  290. return cachedInfo.subject;
  291. }
  292. // Create monitored element info.
  293. const info = {
  294. checkChildren: checkChildren,
  295. subject: new Subject(),
  296. rootNode,
  297. };
  298. this._elementInfo.set(nativeElement, info);
  299. this._registerGlobalListeners(info);
  300. return info.subject;
  301. }
  302. stopMonitoring(element) {
  303. const nativeElement = coerceElement(element);
  304. const elementInfo = this._elementInfo.get(nativeElement);
  305. if (elementInfo) {
  306. elementInfo.subject.complete();
  307. this._setClasses(nativeElement);
  308. this._elementInfo.delete(nativeElement);
  309. this._removeGlobalListeners(elementInfo);
  310. }
  311. }
  312. focusVia(element, origin, options) {
  313. const nativeElement = coerceElement(element);
  314. const focusedElement = this._getDocument().activeElement;
  315. // If the element is focused already, calling `focus` again won't trigger the event listener
  316. // which means that the focus classes won't be updated. If that's the case, update the classes
  317. // directly without waiting for an event.
  318. if (nativeElement === focusedElement) {
  319. this._getClosestElementsInfo(nativeElement).forEach(([currentElement, info]) => this._originChanged(currentElement, origin, info));
  320. }
  321. else {
  322. this._setOrigin(origin);
  323. // `focus` isn't available on the server
  324. if (typeof nativeElement.focus === 'function') {
  325. nativeElement.focus(options);
  326. }
  327. }
  328. }
  329. ngOnDestroy() {
  330. this._elementInfo.forEach((_info, element) => this.stopMonitoring(element));
  331. }
  332. /** Access injected document if available or fallback to global document reference */
  333. _getDocument() {
  334. return this._document || document;
  335. }
  336. /** Use defaultView of injected document if available or fallback to global window reference */
  337. _getWindow() {
  338. const doc = this._getDocument();
  339. return doc.defaultView || window;
  340. }
  341. _getFocusOrigin(focusEventTarget) {
  342. if (this._origin) {
  343. // If the origin was realized via a touch interaction, we need to perform additional checks
  344. // to determine whether the focus origin should be attributed to touch or program.
  345. if (this._originFromTouchInteraction) {
  346. return this._shouldBeAttributedToTouch(focusEventTarget) ? 'touch' : 'program';
  347. }
  348. else {
  349. return this._origin;
  350. }
  351. }
  352. // If the window has just regained focus, we can restore the most recent origin from before the
  353. // window blurred. Otherwise, we've reached the point where we can't identify the source of the
  354. // focus. This typically means one of two things happened:
  355. //
  356. // 1) The element was programmatically focused, or
  357. // 2) The element was focused via screen reader navigation (which generally doesn't fire
  358. // events).
  359. //
  360. // Because we can't distinguish between these two cases, we default to setting `program`.
  361. if (this._windowFocused && this._lastFocusOrigin) {
  362. return this._lastFocusOrigin;
  363. }
  364. // If the interaction is coming from an input label, we consider it a mouse interactions.
  365. // This is a special case where focus moves on `click`, rather than `mousedown` which breaks
  366. // our detection, because all our assumptions are for `mousedown`. We need to handle this
  367. // special case, because it's very common for checkboxes and radio buttons.
  368. if (focusEventTarget && this._isLastInteractionFromInputLabel(focusEventTarget)) {
  369. return 'mouse';
  370. }
  371. return 'program';
  372. }
  373. /**
  374. * Returns whether the focus event should be attributed to touch. Recall that in IMMEDIATE mode, a
  375. * touch origin isn't immediately reset at the next tick (see _setOrigin). This means that when we
  376. * handle a focus event following a touch interaction, we need to determine whether (1) the focus
  377. * event was directly caused by the touch interaction or (2) the focus event was caused by a
  378. * subsequent programmatic focus call triggered by the touch interaction.
  379. * @param focusEventTarget The target of the focus event under examination.
  380. */
  381. _shouldBeAttributedToTouch(focusEventTarget) {
  382. // Please note that this check is not perfect. Consider the following edge case:
  383. //
  384. // <div #parent tabindex="0">
  385. // <div #child tabindex="0" (click)="#parent.focus()"></div>
  386. // </div>
  387. //
  388. // Suppose there is a FocusMonitor in IMMEDIATE mode attached to #parent. When the user touches
  389. // #child, #parent is programmatically focused. This code will attribute the focus to touch
  390. // instead of program. This is a relatively minor edge-case that can be worked around by using
  391. // focusVia(parent, 'program') to focus #parent.
  392. return (this._detectionMode === FocusMonitorDetectionMode.EVENTUAL ||
  393. !!focusEventTarget?.contains(this._inputModalityDetector._mostRecentTarget));
  394. }
  395. /**
  396. * Sets the focus classes on the element based on the given focus origin.
  397. * @param element The element to update the classes on.
  398. * @param origin The focus origin.
  399. */
  400. _setClasses(element, origin) {
  401. element.classList.toggle('cdk-focused', !!origin);
  402. element.classList.toggle('cdk-touch-focused', origin === 'touch');
  403. element.classList.toggle('cdk-keyboard-focused', origin === 'keyboard');
  404. element.classList.toggle('cdk-mouse-focused', origin === 'mouse');
  405. element.classList.toggle('cdk-program-focused', origin === 'program');
  406. }
  407. /**
  408. * Updates the focus origin. If we're using immediate detection mode, we schedule an async
  409. * function to clear the origin at the end of a timeout. The duration of the timeout depends on
  410. * the origin being set.
  411. * @param origin The origin to set.
  412. * @param isFromInteraction Whether we are setting the origin from an interaction event.
  413. */
  414. _setOrigin(origin, isFromInteraction = false) {
  415. this._ngZone.runOutsideAngular(() => {
  416. this._origin = origin;
  417. this._originFromTouchInteraction = origin === 'touch' && isFromInteraction;
  418. // If we're in IMMEDIATE mode, reset the origin at the next tick (or in `TOUCH_BUFFER_MS` ms
  419. // for a touch event). We reset the origin at the next tick because Firefox focuses one tick
  420. // after the interaction event. We wait `TOUCH_BUFFER_MS` ms before resetting the origin for
  421. // a touch event because when a touch event is fired, the associated focus event isn't yet in
  422. // the event queue. Before doing so, clear any pending timeouts.
  423. if (this._detectionMode === FocusMonitorDetectionMode.IMMEDIATE) {
  424. clearTimeout(this._originTimeoutId);
  425. const ms = this._originFromTouchInteraction ? TOUCH_BUFFER_MS : 1;
  426. this._originTimeoutId = setTimeout(() => (this._origin = null), ms);
  427. }
  428. });
  429. }
  430. /**
  431. * Handles focus events on a registered element.
  432. * @param event The focus event.
  433. * @param element The monitored element.
  434. */
  435. _onFocus(event, element) {
  436. // NOTE(mmalerba): We currently set the classes based on the focus origin of the most recent
  437. // focus event affecting the monitored element. If we want to use the origin of the first event
  438. // instead we should check for the cdk-focused class here and return if the element already has
  439. // it. (This only matters for elements that have includesChildren = true).
  440. // If we are not counting child-element-focus as focused, make sure that the event target is the
  441. // monitored element itself.
  442. const elementInfo = this._elementInfo.get(element);
  443. const focusEventTarget = _getEventTarget(event);
  444. if (!elementInfo || (!elementInfo.checkChildren && element !== focusEventTarget)) {
  445. return;
  446. }
  447. this._originChanged(element, this._getFocusOrigin(focusEventTarget), elementInfo);
  448. }
  449. /**
  450. * Handles blur events on a registered element.
  451. * @param event The blur event.
  452. * @param element The monitored element.
  453. */
  454. _onBlur(event, element) {
  455. // If we are counting child-element-focus as focused, make sure that we aren't just blurring in
  456. // order to focus another child of the monitored element.
  457. const elementInfo = this._elementInfo.get(element);
  458. if (!elementInfo ||
  459. (elementInfo.checkChildren &&
  460. event.relatedTarget instanceof Node &&
  461. element.contains(event.relatedTarget))) {
  462. return;
  463. }
  464. this._setClasses(element);
  465. this._emitOrigin(elementInfo, null);
  466. }
  467. _emitOrigin(info, origin) {
  468. if (info.subject.observers.length) {
  469. this._ngZone.run(() => info.subject.next(origin));
  470. }
  471. }
  472. _registerGlobalListeners(elementInfo) {
  473. if (!this._platform.isBrowser) {
  474. return;
  475. }
  476. const rootNode = elementInfo.rootNode;
  477. const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode) || 0;
  478. if (!rootNodeFocusListeners) {
  479. this._ngZone.runOutsideAngular(() => {
  480. rootNode.addEventListener('focus', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
  481. rootNode.addEventListener('blur', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
  482. });
  483. }
  484. this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners + 1);
  485. // Register global listeners when first element is monitored.
  486. if (++this._monitoredElementCount === 1) {
  487. // Note: we listen to events in the capture phase so we
  488. // can detect them even if the user stops propagation.
  489. this._ngZone.runOutsideAngular(() => {
  490. const window = this._getWindow();
  491. window.addEventListener('focus', this._windowFocusListener);
  492. });
  493. // The InputModalityDetector is also just a collection of global listeners.
  494. this._inputModalityDetector.modalityDetected
  495. .pipe(takeUntil(this._stopInputModalityDetector))
  496. .subscribe(modality => {
  497. this._setOrigin(modality, true /* isFromInteraction */);
  498. });
  499. }
  500. }
  501. _removeGlobalListeners(elementInfo) {
  502. const rootNode = elementInfo.rootNode;
  503. if (this._rootNodeFocusListenerCount.has(rootNode)) {
  504. const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode);
  505. if (rootNodeFocusListeners > 1) {
  506. this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners - 1);
  507. }
  508. else {
  509. rootNode.removeEventListener('focus', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
  510. rootNode.removeEventListener('blur', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
  511. this._rootNodeFocusListenerCount.delete(rootNode);
  512. }
  513. }
  514. // Unregister global listeners when last element is unmonitored.
  515. if (!--this._monitoredElementCount) {
  516. const window = this._getWindow();
  517. window.removeEventListener('focus', this._windowFocusListener);
  518. // Equivalently, stop our InputModalityDetector subscription.
  519. this._stopInputModalityDetector.next();
  520. // Clear timeouts for all potentially pending timeouts to prevent the leaks.
  521. clearTimeout(this._windowFocusTimeoutId);
  522. clearTimeout(this._originTimeoutId);
  523. }
  524. }
  525. /** Updates all the state on an element once its focus origin has changed. */
  526. _originChanged(element, origin, elementInfo) {
  527. this._setClasses(element, origin);
  528. this._emitOrigin(elementInfo, origin);
  529. this._lastFocusOrigin = origin;
  530. }
  531. /**
  532. * Collects the `MonitoredElementInfo` of a particular element and
  533. * all of its ancestors that have enabled `checkChildren`.
  534. * @param element Element from which to start the search.
  535. */
  536. _getClosestElementsInfo(element) {
  537. const results = [];
  538. this._elementInfo.forEach((info, currentElement) => {
  539. if (currentElement === element || (info.checkChildren && currentElement.contains(element))) {
  540. results.push([currentElement, info]);
  541. }
  542. });
  543. return results;
  544. }
  545. /**
  546. * Returns whether an interaction is likely to have come from the user clicking the `label` of
  547. * an `input` or `textarea` in order to focus it.
  548. * @param focusEventTarget Target currently receiving focus.
  549. */
  550. _isLastInteractionFromInputLabel(focusEventTarget) {
  551. const { _mostRecentTarget: mostRecentTarget, mostRecentModality } = this._inputModalityDetector;
  552. // If the last interaction used the mouse on an element contained by one of the labels
  553. // of an `input`/`textarea` that is currently focused, it is very likely that the
  554. // user redirected focus using the label.
  555. if (mostRecentModality !== 'mouse' ||
  556. !mostRecentTarget ||
  557. mostRecentTarget === focusEventTarget ||
  558. (focusEventTarget.nodeName !== 'INPUT' && focusEventTarget.nodeName !== 'TEXTAREA') ||
  559. focusEventTarget.disabled) {
  560. return false;
  561. }
  562. const labels = focusEventTarget.labels;
  563. if (labels) {
  564. for (let i = 0; i < labels.length; i++) {
  565. if (labels[i].contains(mostRecentTarget)) {
  566. return true;
  567. }
  568. }
  569. }
  570. return false;
  571. }
  572. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: FocusMonitor, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
  573. static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: FocusMonitor, providedIn: 'root' });
  574. }
  575. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: FocusMonitor, decorators: [{
  576. type: Injectable,
  577. args: [{ providedIn: 'root' }]
  578. }], ctorParameters: () => [] });
  579. /**
  580. * Directive that determines how a particular element was focused (via keyboard, mouse, touch, or
  581. * programmatically) and adds corresponding classes to the element.
  582. *
  583. * There are two variants of this directive:
  584. * 1) cdkMonitorElementFocus: does not consider an element to be focused if one of its children is
  585. * focused.
  586. * 2) cdkMonitorSubtreeFocus: considers an element focused if it or any of its children are focused.
  587. */
  588. class CdkMonitorFocus {
  589. _elementRef = inject(ElementRef);
  590. _focusMonitor = inject(FocusMonitor);
  591. _monitorSubscription;
  592. _focusOrigin = null;
  593. cdkFocusChange = new EventEmitter();
  594. constructor() { }
  595. get focusOrigin() {
  596. return this._focusOrigin;
  597. }
  598. ngAfterViewInit() {
  599. const element = this._elementRef.nativeElement;
  600. this._monitorSubscription = this._focusMonitor
  601. .monitor(element, element.nodeType === 1 && element.hasAttribute('cdkMonitorSubtreeFocus'))
  602. .subscribe(origin => {
  603. this._focusOrigin = origin;
  604. this.cdkFocusChange.emit(origin);
  605. });
  606. }
  607. ngOnDestroy() {
  608. this._focusMonitor.stopMonitoring(this._elementRef);
  609. if (this._monitorSubscription) {
  610. this._monitorSubscription.unsubscribe();
  611. }
  612. }
  613. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkMonitorFocus, deps: [], target: i0.ɵɵFactoryTarget.Directive });
  614. static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: CdkMonitorFocus, isStandalone: true, selector: "[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]", outputs: { cdkFocusChange: "cdkFocusChange" }, exportAs: ["cdkMonitorFocus"], ngImport: i0 });
  615. }
  616. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkMonitorFocus, decorators: [{
  617. type: Directive,
  618. args: [{
  619. selector: '[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]',
  620. exportAs: 'cdkMonitorFocus',
  621. }]
  622. }], ctorParameters: () => [], propDecorators: { cdkFocusChange: [{
  623. type: Output
  624. }] } });
  625. export { CdkMonitorFocus as C, FocusMonitor as F, InputModalityDetector as I, INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS as a, INPUT_MODALITY_DETECTOR_OPTIONS as b, FocusMonitorDetectionMode as c, FOCUS_MONITOR_DEFAULT_OPTIONS as d };
  626. //# sourceMappingURL=focus-monitor-e2l_RpN3.mjs.map