a11y.mjs 109 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389
  1. import { DOCUMENT } from '@angular/common';
  2. import * as i0 from '@angular/core';
  3. import { inject, APP_ID, Injectable, Inject, QueryList, Directive, Input, InjectionToken, Optional, EventEmitter, Output, NgModule } from '@angular/core';
  4. import * as i1 from '@angular/cdk/platform';
  5. import { _getFocusedElementPierceShadowDom, normalizePassiveListenerOptions, _getEventTarget, _getShadowRoot } from '@angular/cdk/platform';
  6. import { Subject, Subscription, BehaviorSubject, of } from 'rxjs';
  7. import { hasModifierKey, A, Z, ZERO, NINE, PAGE_DOWN, PAGE_UP, END, HOME, LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, TAB, ALT, CONTROL, MAC_META, META, SHIFT } from '@angular/cdk/keycodes';
  8. import { tap, debounceTime, filter, map, take, skip, distinctUntilChanged, takeUntil } from 'rxjs/operators';
  9. import { coerceBooleanProperty, coerceElement } from '@angular/cdk/coercion';
  10. import * as i1$1 from '@angular/cdk/observers';
  11. import { ObserversModule } from '@angular/cdk/observers';
  12. import { BreakpointObserver } from '@angular/cdk/layout';
  13. /** IDs are delimited by an empty space, as per the spec. */
  14. const ID_DELIMITER = ' ';
  15. /**
  16. * Adds the given ID to the specified ARIA attribute on an element.
  17. * Used for attributes such as aria-labelledby, aria-owns, etc.
  18. */
  19. function addAriaReferencedId(el, attr, id) {
  20. const ids = getAriaReferenceIds(el, attr);
  21. if (ids.some(existingId => existingId.trim() == id.trim())) {
  22. return;
  23. }
  24. ids.push(id.trim());
  25. el.setAttribute(attr, ids.join(ID_DELIMITER));
  26. }
  27. /**
  28. * Removes the given ID from the specified ARIA attribute on an element.
  29. * Used for attributes such as aria-labelledby, aria-owns, etc.
  30. */
  31. function removeAriaReferencedId(el, attr, id) {
  32. const ids = getAriaReferenceIds(el, attr);
  33. const filteredIds = ids.filter(val => val != id.trim());
  34. if (filteredIds.length) {
  35. el.setAttribute(attr, filteredIds.join(ID_DELIMITER));
  36. }
  37. else {
  38. el.removeAttribute(attr);
  39. }
  40. }
  41. /**
  42. * Gets the list of IDs referenced by the given ARIA attribute on an element.
  43. * Used for attributes such as aria-labelledby, aria-owns, etc.
  44. */
  45. function getAriaReferenceIds(el, attr) {
  46. // Get string array of all individual ids (whitespace delimited) in the attribute value
  47. return (el.getAttribute(attr) || '').match(/\S+/g) || [];
  48. }
  49. /**
  50. * ID used for the body container where all messages are appended.
  51. * @deprecated No longer being used. To be removed.
  52. * @breaking-change 14.0.0
  53. */
  54. const MESSAGES_CONTAINER_ID = 'cdk-describedby-message-container';
  55. /**
  56. * ID prefix used for each created message element.
  57. * @deprecated To be turned into a private variable.
  58. * @breaking-change 14.0.0
  59. */
  60. const CDK_DESCRIBEDBY_ID_PREFIX = 'cdk-describedby-message';
  61. /**
  62. * Attribute given to each host element that is described by a message element.
  63. * @deprecated To be turned into a private variable.
  64. * @breaking-change 14.0.0
  65. */
  66. const CDK_DESCRIBEDBY_HOST_ATTRIBUTE = 'cdk-describedby-host';
  67. /** Global incremental identifier for each registered message element. */
  68. let nextId = 0;
  69. /**
  70. * Utility that creates visually hidden elements with a message content. Useful for elements that
  71. * want to use aria-describedby to further describe themselves without adding additional visual
  72. * content.
  73. */
  74. class AriaDescriber {
  75. constructor(_document,
  76. /**
  77. * @deprecated To be turned into a required parameter.
  78. * @breaking-change 14.0.0
  79. */
  80. _platform) {
  81. this._platform = _platform;
  82. /** Map of all registered message elements that have been placed into the document. */
  83. this._messageRegistry = new Map();
  84. /** Container for all registered messages. */
  85. this._messagesContainer = null;
  86. /** Unique ID for the service. */
  87. this._id = `${nextId++}`;
  88. this._document = _document;
  89. this._id = inject(APP_ID) + '-' + nextId++;
  90. }
  91. describe(hostElement, message, role) {
  92. if (!this._canBeDescribed(hostElement, message)) {
  93. return;
  94. }
  95. const key = getKey(message, role);
  96. if (typeof message !== 'string') {
  97. // We need to ensure that the element has an ID.
  98. setMessageId(message, this._id);
  99. this._messageRegistry.set(key, { messageElement: message, referenceCount: 0 });
  100. }
  101. else if (!this._messageRegistry.has(key)) {
  102. this._createMessageElement(message, role);
  103. }
  104. if (!this._isElementDescribedByMessage(hostElement, key)) {
  105. this._addMessageReference(hostElement, key);
  106. }
  107. }
  108. removeDescription(hostElement, message, role) {
  109. if (!message || !this._isElementNode(hostElement)) {
  110. return;
  111. }
  112. const key = getKey(message, role);
  113. if (this._isElementDescribedByMessage(hostElement, key)) {
  114. this._removeMessageReference(hostElement, key);
  115. }
  116. // If the message is a string, it means that it's one that we created for the
  117. // consumer so we can remove it safely, otherwise we should leave it in place.
  118. if (typeof message === 'string') {
  119. const registeredMessage = this._messageRegistry.get(key);
  120. if (registeredMessage && registeredMessage.referenceCount === 0) {
  121. this._deleteMessageElement(key);
  122. }
  123. }
  124. if (this._messagesContainer?.childNodes.length === 0) {
  125. this._messagesContainer.remove();
  126. this._messagesContainer = null;
  127. }
  128. }
  129. /** Unregisters all created message elements and removes the message container. */
  130. ngOnDestroy() {
  131. const describedElements = this._document.querySelectorAll(`[${CDK_DESCRIBEDBY_HOST_ATTRIBUTE}="${this._id}"]`);
  132. for (let i = 0; i < describedElements.length; i++) {
  133. this._removeCdkDescribedByReferenceIds(describedElements[i]);
  134. describedElements[i].removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
  135. }
  136. this._messagesContainer?.remove();
  137. this._messagesContainer = null;
  138. this._messageRegistry.clear();
  139. }
  140. /**
  141. * Creates a new element in the visually hidden message container element with the message
  142. * as its content and adds it to the message registry.
  143. */
  144. _createMessageElement(message, role) {
  145. const messageElement = this._document.createElement('div');
  146. setMessageId(messageElement, this._id);
  147. messageElement.textContent = message;
  148. if (role) {
  149. messageElement.setAttribute('role', role);
  150. }
  151. this._createMessagesContainer();
  152. this._messagesContainer.appendChild(messageElement);
  153. this._messageRegistry.set(getKey(message, role), { messageElement, referenceCount: 0 });
  154. }
  155. /** Deletes the message element from the global messages container. */
  156. _deleteMessageElement(key) {
  157. this._messageRegistry.get(key)?.messageElement?.remove();
  158. this._messageRegistry.delete(key);
  159. }
  160. /** Creates the global container for all aria-describedby messages. */
  161. _createMessagesContainer() {
  162. if (this._messagesContainer) {
  163. return;
  164. }
  165. const containerClassName = 'cdk-describedby-message-container';
  166. const serverContainers = this._document.querySelectorAll(`.${containerClassName}[platform="server"]`);
  167. for (let i = 0; i < serverContainers.length; i++) {
  168. // When going from the server to the client, we may end up in a situation where there's
  169. // already a container on the page, but we don't have a reference to it. Clear the
  170. // old container so we don't get duplicates. Doing this, instead of emptying the previous
  171. // container, should be slightly faster.
  172. serverContainers[i].remove();
  173. }
  174. const messagesContainer = this._document.createElement('div');
  175. // We add `visibility: hidden` in order to prevent text in this container from
  176. // being searchable by the browser's Ctrl + F functionality.
  177. // Screen-readers will still read the description for elements with aria-describedby even
  178. // when the description element is not visible.
  179. messagesContainer.style.visibility = 'hidden';
  180. // Even though we use `visibility: hidden`, we still apply `cdk-visually-hidden` so that
  181. // the description element doesn't impact page layout.
  182. messagesContainer.classList.add(containerClassName);
  183. messagesContainer.classList.add('cdk-visually-hidden');
  184. // @breaking-change 14.0.0 Remove null check for `_platform`.
  185. if (this._platform && !this._platform.isBrowser) {
  186. messagesContainer.setAttribute('platform', 'server');
  187. }
  188. this._document.body.appendChild(messagesContainer);
  189. this._messagesContainer = messagesContainer;
  190. }
  191. /** Removes all cdk-describedby messages that are hosted through the element. */
  192. _removeCdkDescribedByReferenceIds(element) {
  193. // Remove all aria-describedby reference IDs that are prefixed by CDK_DESCRIBEDBY_ID_PREFIX
  194. const originalReferenceIds = getAriaReferenceIds(element, 'aria-describedby').filter(id => id.indexOf(CDK_DESCRIBEDBY_ID_PREFIX) != 0);
  195. element.setAttribute('aria-describedby', originalReferenceIds.join(' '));
  196. }
  197. /**
  198. * Adds a message reference to the element using aria-describedby and increments the registered
  199. * message's reference count.
  200. */
  201. _addMessageReference(element, key) {
  202. const registeredMessage = this._messageRegistry.get(key);
  203. // Add the aria-describedby reference and set the
  204. // describedby_host attribute to mark the element.
  205. addAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
  206. element.setAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE, this._id);
  207. registeredMessage.referenceCount++;
  208. }
  209. /**
  210. * Removes a message reference from the element using aria-describedby
  211. * and decrements the registered message's reference count.
  212. */
  213. _removeMessageReference(element, key) {
  214. const registeredMessage = this._messageRegistry.get(key);
  215. registeredMessage.referenceCount--;
  216. removeAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
  217. element.removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
  218. }
  219. /** Returns true if the element has been described by the provided message ID. */
  220. _isElementDescribedByMessage(element, key) {
  221. const referenceIds = getAriaReferenceIds(element, 'aria-describedby');
  222. const registeredMessage = this._messageRegistry.get(key);
  223. const messageId = registeredMessage && registeredMessage.messageElement.id;
  224. return !!messageId && referenceIds.indexOf(messageId) != -1;
  225. }
  226. /** Determines whether a message can be described on a particular element. */
  227. _canBeDescribed(element, message) {
  228. if (!this._isElementNode(element)) {
  229. return false;
  230. }
  231. if (message && typeof message === 'object') {
  232. // We'd have to make some assumptions about the description element's text, if the consumer
  233. // passed in an element. Assume that if an element is passed in, the consumer has verified
  234. // that it can be used as a description.
  235. return true;
  236. }
  237. const trimmedMessage = message == null ? '' : `${message}`.trim();
  238. const ariaLabel = element.getAttribute('aria-label');
  239. // We shouldn't set descriptions if they're exactly the same as the `aria-label` of the
  240. // element, because screen readers will end up reading out the same text twice in a row.
  241. return trimmedMessage ? !ariaLabel || ariaLabel.trim() !== trimmedMessage : false;
  242. }
  243. /** Checks whether a node is an Element node. */
  244. _isElementNode(element) {
  245. return element.nodeType === this._document.ELEMENT_NODE;
  246. }
  247. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: AriaDescriber, deps: [{ token: DOCUMENT }, { token: i1.Platform }], target: i0.ɵɵFactoryTarget.Injectable }); }
  248. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: AriaDescriber, providedIn: 'root' }); }
  249. }
  250. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: AriaDescriber, decorators: [{
  251. type: Injectable,
  252. args: [{ providedIn: 'root' }]
  253. }], ctorParameters: function () { return [{ type: undefined, decorators: [{
  254. type: Inject,
  255. args: [DOCUMENT]
  256. }] }, { type: i1.Platform }]; } });
  257. /** Gets a key that can be used to look messages up in the registry. */
  258. function getKey(message, role) {
  259. return typeof message === 'string' ? `${role || ''}/${message}` : message;
  260. }
  261. /** Assigns a unique ID to an element, if it doesn't have one already. */
  262. function setMessageId(element, serviceId) {
  263. if (!element.id) {
  264. element.id = `${CDK_DESCRIBEDBY_ID_PREFIX}-${serviceId}-${nextId++}`;
  265. }
  266. }
  267. /**
  268. * This class manages keyboard events for selectable lists. If you pass it a query list
  269. * of items, it will set the active item correctly when arrow events occur.
  270. */
  271. class ListKeyManager {
  272. constructor(_items) {
  273. this._items = _items;
  274. this._activeItemIndex = -1;
  275. this._activeItem = null;
  276. this._wrap = false;
  277. this._letterKeyStream = new Subject();
  278. this._typeaheadSubscription = Subscription.EMPTY;
  279. this._vertical = true;
  280. this._allowedModifierKeys = [];
  281. this._homeAndEnd = false;
  282. this._pageUpAndDown = { enabled: false, delta: 10 };
  283. /**
  284. * Predicate function that can be used to check whether an item should be skipped
  285. * by the key manager. By default, disabled items are skipped.
  286. */
  287. this._skipPredicateFn = (item) => item.disabled;
  288. // Buffer for the letters that the user has pressed when the typeahead option is turned on.
  289. this._pressedLetters = [];
  290. /**
  291. * Stream that emits any time the TAB key is pressed, so components can react
  292. * when focus is shifted off of the list.
  293. */
  294. this.tabOut = new Subject();
  295. /** Stream that emits whenever the active item of the list manager changes. */
  296. this.change = new Subject();
  297. // We allow for the items to be an array because, in some cases, the consumer may
  298. // not have access to a QueryList of the items they want to manage (e.g. when the
  299. // items aren't being collected via `ViewChildren` or `ContentChildren`).
  300. if (_items instanceof QueryList) {
  301. this._itemChangesSubscription = _items.changes.subscribe((newItems) => {
  302. if (this._activeItem) {
  303. const itemArray = newItems.toArray();
  304. const newIndex = itemArray.indexOf(this._activeItem);
  305. if (newIndex > -1 && newIndex !== this._activeItemIndex) {
  306. this._activeItemIndex = newIndex;
  307. }
  308. }
  309. });
  310. }
  311. }
  312. /**
  313. * Sets the predicate function that determines which items should be skipped by the
  314. * list key manager.
  315. * @param predicate Function that determines whether the given item should be skipped.
  316. */
  317. skipPredicate(predicate) {
  318. this._skipPredicateFn = predicate;
  319. return this;
  320. }
  321. /**
  322. * Configures wrapping mode, which determines whether the active item will wrap to
  323. * the other end of list when there are no more items in the given direction.
  324. * @param shouldWrap Whether the list should wrap when reaching the end.
  325. */
  326. withWrap(shouldWrap = true) {
  327. this._wrap = shouldWrap;
  328. return this;
  329. }
  330. /**
  331. * Configures whether the key manager should be able to move the selection vertically.
  332. * @param enabled Whether vertical selection should be enabled.
  333. */
  334. withVerticalOrientation(enabled = true) {
  335. this._vertical = enabled;
  336. return this;
  337. }
  338. /**
  339. * Configures the key manager to move the selection horizontally.
  340. * Passing in `null` will disable horizontal movement.
  341. * @param direction Direction in which the selection can be moved.
  342. */
  343. withHorizontalOrientation(direction) {
  344. this._horizontal = direction;
  345. return this;
  346. }
  347. /**
  348. * Modifier keys which are allowed to be held down and whose default actions will be prevented
  349. * as the user is pressing the arrow keys. Defaults to not allowing any modifier keys.
  350. */
  351. withAllowedModifierKeys(keys) {
  352. this._allowedModifierKeys = keys;
  353. return this;
  354. }
  355. /**
  356. * Turns on typeahead mode which allows users to set the active item by typing.
  357. * @param debounceInterval Time to wait after the last keystroke before setting the active item.
  358. */
  359. withTypeAhead(debounceInterval = 200) {
  360. if ((typeof ngDevMode === 'undefined' || ngDevMode) &&
  361. this._items.length &&
  362. this._items.some(item => typeof item.getLabel !== 'function')) {
  363. throw Error('ListKeyManager items in typeahead mode must implement the `getLabel` method.');
  364. }
  365. this._typeaheadSubscription.unsubscribe();
  366. // Debounce the presses of non-navigational keys, collect the ones that correspond to letters
  367. // and convert those letters back into a string. Afterwards find the first item that starts
  368. // with that string and select it.
  369. this._typeaheadSubscription = this._letterKeyStream
  370. .pipe(tap(letter => this._pressedLetters.push(letter)), debounceTime(debounceInterval), filter(() => this._pressedLetters.length > 0), map(() => this._pressedLetters.join('')))
  371. .subscribe(inputString => {
  372. const items = this._getItemsArray();
  373. // Start at 1 because we want to start searching at the item immediately
  374. // following the current active item.
  375. for (let i = 1; i < items.length + 1; i++) {
  376. const index = (this._activeItemIndex + i) % items.length;
  377. const item = items[index];
  378. if (!this._skipPredicateFn(item) &&
  379. item.getLabel().toUpperCase().trim().indexOf(inputString) === 0) {
  380. this.setActiveItem(index);
  381. break;
  382. }
  383. }
  384. this._pressedLetters = [];
  385. });
  386. return this;
  387. }
  388. /** Cancels the current typeahead sequence. */
  389. cancelTypeahead() {
  390. this._pressedLetters = [];
  391. return this;
  392. }
  393. /**
  394. * Configures the key manager to activate the first and last items
  395. * respectively when the Home or End key is pressed.
  396. * @param enabled Whether pressing the Home or End key activates the first/last item.
  397. */
  398. withHomeAndEnd(enabled = true) {
  399. this._homeAndEnd = enabled;
  400. return this;
  401. }
  402. /**
  403. * Configures the key manager to activate every 10th, configured or first/last element in up/down direction
  404. * respectively when the Page-Up or Page-Down key is pressed.
  405. * @param enabled Whether pressing the Page-Up or Page-Down key activates the first/last item.
  406. * @param delta Whether pressing the Home or End key activates the first/last item.
  407. */
  408. withPageUpDown(enabled = true, delta = 10) {
  409. this._pageUpAndDown = { enabled, delta };
  410. return this;
  411. }
  412. setActiveItem(item) {
  413. const previousActiveItem = this._activeItem;
  414. this.updateActiveItem(item);
  415. if (this._activeItem !== previousActiveItem) {
  416. this.change.next(this._activeItemIndex);
  417. }
  418. }
  419. /**
  420. * Sets the active item depending on the key event passed in.
  421. * @param event Keyboard event to be used for determining which element should be active.
  422. */
  423. onKeydown(event) {
  424. const keyCode = event.keyCode;
  425. const modifiers = ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'];
  426. const isModifierAllowed = modifiers.every(modifier => {
  427. return !event[modifier] || this._allowedModifierKeys.indexOf(modifier) > -1;
  428. });
  429. switch (keyCode) {
  430. case TAB:
  431. this.tabOut.next();
  432. return;
  433. case DOWN_ARROW:
  434. if (this._vertical && isModifierAllowed) {
  435. this.setNextItemActive();
  436. break;
  437. }
  438. else {
  439. return;
  440. }
  441. case UP_ARROW:
  442. if (this._vertical && isModifierAllowed) {
  443. this.setPreviousItemActive();
  444. break;
  445. }
  446. else {
  447. return;
  448. }
  449. case RIGHT_ARROW:
  450. if (this._horizontal && isModifierAllowed) {
  451. this._horizontal === 'rtl' ? this.setPreviousItemActive() : this.setNextItemActive();
  452. break;
  453. }
  454. else {
  455. return;
  456. }
  457. case LEFT_ARROW:
  458. if (this._horizontal && isModifierAllowed) {
  459. this._horizontal === 'rtl' ? this.setNextItemActive() : this.setPreviousItemActive();
  460. break;
  461. }
  462. else {
  463. return;
  464. }
  465. case HOME:
  466. if (this._homeAndEnd && isModifierAllowed) {
  467. this.setFirstItemActive();
  468. break;
  469. }
  470. else {
  471. return;
  472. }
  473. case END:
  474. if (this._homeAndEnd && isModifierAllowed) {
  475. this.setLastItemActive();
  476. break;
  477. }
  478. else {
  479. return;
  480. }
  481. case PAGE_UP:
  482. if (this._pageUpAndDown.enabled && isModifierAllowed) {
  483. const targetIndex = this._activeItemIndex - this._pageUpAndDown.delta;
  484. this._setActiveItemByIndex(targetIndex > 0 ? targetIndex : 0, 1);
  485. break;
  486. }
  487. else {
  488. return;
  489. }
  490. case PAGE_DOWN:
  491. if (this._pageUpAndDown.enabled && isModifierAllowed) {
  492. const targetIndex = this._activeItemIndex + this._pageUpAndDown.delta;
  493. const itemsLength = this._getItemsArray().length;
  494. this._setActiveItemByIndex(targetIndex < itemsLength ? targetIndex : itemsLength - 1, -1);
  495. break;
  496. }
  497. else {
  498. return;
  499. }
  500. default:
  501. if (isModifierAllowed || hasModifierKey(event, 'shiftKey')) {
  502. // Attempt to use the `event.key` which also maps it to the user's keyboard language,
  503. // otherwise fall back to resolving alphanumeric characters via the keyCode.
  504. if (event.key && event.key.length === 1) {
  505. this._letterKeyStream.next(event.key.toLocaleUpperCase());
  506. }
  507. else if ((keyCode >= A && keyCode <= Z) || (keyCode >= ZERO && keyCode <= NINE)) {
  508. this._letterKeyStream.next(String.fromCharCode(keyCode));
  509. }
  510. }
  511. // Note that we return here, in order to avoid preventing
  512. // the default action of non-navigational keys.
  513. return;
  514. }
  515. this._pressedLetters = [];
  516. event.preventDefault();
  517. }
  518. /** Index of the currently active item. */
  519. get activeItemIndex() {
  520. return this._activeItemIndex;
  521. }
  522. /** The active item. */
  523. get activeItem() {
  524. return this._activeItem;
  525. }
  526. /** Gets whether the user is currently typing into the manager using the typeahead feature. */
  527. isTyping() {
  528. return this._pressedLetters.length > 0;
  529. }
  530. /** Sets the active item to the first enabled item in the list. */
  531. setFirstItemActive() {
  532. this._setActiveItemByIndex(0, 1);
  533. }
  534. /** Sets the active item to the last enabled item in the list. */
  535. setLastItemActive() {
  536. this._setActiveItemByIndex(this._items.length - 1, -1);
  537. }
  538. /** Sets the active item to the next enabled item in the list. */
  539. setNextItemActive() {
  540. this._activeItemIndex < 0 ? this.setFirstItemActive() : this._setActiveItemByDelta(1);
  541. }
  542. /** Sets the active item to a previous enabled item in the list. */
  543. setPreviousItemActive() {
  544. this._activeItemIndex < 0 && this._wrap
  545. ? this.setLastItemActive()
  546. : this._setActiveItemByDelta(-1);
  547. }
  548. updateActiveItem(item) {
  549. const itemArray = this._getItemsArray();
  550. const index = typeof item === 'number' ? item : itemArray.indexOf(item);
  551. const activeItem = itemArray[index];
  552. // Explicitly check for `null` and `undefined` because other falsy values are valid.
  553. this._activeItem = activeItem == null ? null : activeItem;
  554. this._activeItemIndex = index;
  555. }
  556. /** Cleans up the key manager. */
  557. destroy() {
  558. this._typeaheadSubscription.unsubscribe();
  559. this._itemChangesSubscription?.unsubscribe();
  560. this._letterKeyStream.complete();
  561. this.tabOut.complete();
  562. this.change.complete();
  563. this._pressedLetters = [];
  564. }
  565. /**
  566. * This method sets the active item, given a list of items and the delta between the
  567. * currently active item and the new active item. It will calculate differently
  568. * depending on whether wrap mode is turned on.
  569. */
  570. _setActiveItemByDelta(delta) {
  571. this._wrap ? this._setActiveInWrapMode(delta) : this._setActiveInDefaultMode(delta);
  572. }
  573. /**
  574. * Sets the active item properly given "wrap" mode. In other words, it will continue to move
  575. * down the list until it finds an item that is not disabled, and it will wrap if it
  576. * encounters either end of the list.
  577. */
  578. _setActiveInWrapMode(delta) {
  579. const items = this._getItemsArray();
  580. for (let i = 1; i <= items.length; i++) {
  581. const index = (this._activeItemIndex + delta * i + items.length) % items.length;
  582. const item = items[index];
  583. if (!this._skipPredicateFn(item)) {
  584. this.setActiveItem(index);
  585. return;
  586. }
  587. }
  588. }
  589. /**
  590. * Sets the active item properly given the default mode. In other words, it will
  591. * continue to move down the list until it finds an item that is not disabled. If
  592. * it encounters either end of the list, it will stop and not wrap.
  593. */
  594. _setActiveInDefaultMode(delta) {
  595. this._setActiveItemByIndex(this._activeItemIndex + delta, delta);
  596. }
  597. /**
  598. * Sets the active item to the first enabled item starting at the index specified. If the
  599. * item is disabled, it will move in the fallbackDelta direction until it either
  600. * finds an enabled item or encounters the end of the list.
  601. */
  602. _setActiveItemByIndex(index, fallbackDelta) {
  603. const items = this._getItemsArray();
  604. if (!items[index]) {
  605. return;
  606. }
  607. while (this._skipPredicateFn(items[index])) {
  608. index += fallbackDelta;
  609. if (!items[index]) {
  610. return;
  611. }
  612. }
  613. this.setActiveItem(index);
  614. }
  615. /** Returns the items as an array. */
  616. _getItemsArray() {
  617. return this._items instanceof QueryList ? this._items.toArray() : this._items;
  618. }
  619. }
  620. class ActiveDescendantKeyManager extends ListKeyManager {
  621. setActiveItem(index) {
  622. if (this.activeItem) {
  623. this.activeItem.setInactiveStyles();
  624. }
  625. super.setActiveItem(index);
  626. if (this.activeItem) {
  627. this.activeItem.setActiveStyles();
  628. }
  629. }
  630. }
  631. class FocusKeyManager extends ListKeyManager {
  632. constructor() {
  633. super(...arguments);
  634. this._origin = 'program';
  635. }
  636. /**
  637. * Sets the focus origin that will be passed in to the items for any subsequent `focus` calls.
  638. * @param origin Focus origin to be used when focusing items.
  639. */
  640. setFocusOrigin(origin) {
  641. this._origin = origin;
  642. return this;
  643. }
  644. setActiveItem(item) {
  645. super.setActiveItem(item);
  646. if (this.activeItem) {
  647. this.activeItem.focus(this._origin);
  648. }
  649. }
  650. }
  651. /**
  652. * Configuration for the isFocusable method.
  653. */
  654. class IsFocusableConfig {
  655. constructor() {
  656. /**
  657. * Whether to count an element as focusable even if it is not currently visible.
  658. */
  659. this.ignoreVisibility = false;
  660. }
  661. }
  662. // The InteractivityChecker leans heavily on the ally.js accessibility utilities.
  663. // Methods like `isTabbable` are only covering specific edge-cases for the browsers which are
  664. // supported.
  665. /**
  666. * Utility for checking the interactivity of an element, such as whether is is focusable or
  667. * tabbable.
  668. */
  669. class InteractivityChecker {
  670. constructor(_platform) {
  671. this._platform = _platform;
  672. }
  673. /**
  674. * Gets whether an element is disabled.
  675. *
  676. * @param element Element to be checked.
  677. * @returns Whether the element is disabled.
  678. */
  679. isDisabled(element) {
  680. // This does not capture some cases, such as a non-form control with a disabled attribute or
  681. // a form control inside of a disabled form, but should capture the most common cases.
  682. return element.hasAttribute('disabled');
  683. }
  684. /**
  685. * Gets whether an element is visible for the purposes of interactivity.
  686. *
  687. * This will capture states like `display: none` and `visibility: hidden`, but not things like
  688. * being clipped by an `overflow: hidden` parent or being outside the viewport.
  689. *
  690. * @returns Whether the element is visible.
  691. */
  692. isVisible(element) {
  693. return hasGeometry(element) && getComputedStyle(element).visibility === 'visible';
  694. }
  695. /**
  696. * Gets whether an element can be reached via Tab key.
  697. * Assumes that the element has already been checked with isFocusable.
  698. *
  699. * @param element Element to be checked.
  700. * @returns Whether the element is tabbable.
  701. */
  702. isTabbable(element) {
  703. // Nothing is tabbable on the server 😎
  704. if (!this._platform.isBrowser) {
  705. return false;
  706. }
  707. const frameElement = getFrameElement(getWindow(element));
  708. if (frameElement) {
  709. // Frame elements inherit their tabindex onto all child elements.
  710. if (getTabIndexValue(frameElement) === -1) {
  711. return false;
  712. }
  713. // Browsers disable tabbing to an element inside of an invisible frame.
  714. if (!this.isVisible(frameElement)) {
  715. return false;
  716. }
  717. }
  718. let nodeName = element.nodeName.toLowerCase();
  719. let tabIndexValue = getTabIndexValue(element);
  720. if (element.hasAttribute('contenteditable')) {
  721. return tabIndexValue !== -1;
  722. }
  723. if (nodeName === 'iframe' || nodeName === 'object') {
  724. // The frame or object's content may be tabbable depending on the content, but it's
  725. // not possibly to reliably detect the content of the frames. We always consider such
  726. // elements as non-tabbable.
  727. return false;
  728. }
  729. // In iOS, the browser only considers some specific elements as tabbable.
  730. if (this._platform.WEBKIT && this._platform.IOS && !isPotentiallyTabbableIOS(element)) {
  731. return false;
  732. }
  733. if (nodeName === 'audio') {
  734. // Audio elements without controls enabled are never tabbable, regardless
  735. // of the tabindex attribute explicitly being set.
  736. if (!element.hasAttribute('controls')) {
  737. return false;
  738. }
  739. // Audio elements with controls are by default tabbable unless the
  740. // tabindex attribute is set to `-1` explicitly.
  741. return tabIndexValue !== -1;
  742. }
  743. if (nodeName === 'video') {
  744. // For all video elements, if the tabindex attribute is set to `-1`, the video
  745. // is not tabbable. Note: We cannot rely on the default `HTMLElement.tabIndex`
  746. // property as that one is set to `-1` in Chrome, Edge and Safari v13.1. The
  747. // tabindex attribute is the source of truth here.
  748. if (tabIndexValue === -1) {
  749. return false;
  750. }
  751. // If the tabindex is explicitly set, and not `-1` (as per check before), the
  752. // video element is always tabbable (regardless of whether it has controls or not).
  753. if (tabIndexValue !== null) {
  754. return true;
  755. }
  756. // Otherwise (when no explicit tabindex is set), a video is only tabbable if it
  757. // has controls enabled. Firefox is special as videos are always tabbable regardless
  758. // of whether there are controls or not.
  759. return this._platform.FIREFOX || element.hasAttribute('controls');
  760. }
  761. return element.tabIndex >= 0;
  762. }
  763. /**
  764. * Gets whether an element can be focused by the user.
  765. *
  766. * @param element Element to be checked.
  767. * @param config The config object with options to customize this method's behavior
  768. * @returns Whether the element is focusable.
  769. */
  770. isFocusable(element, config) {
  771. // Perform checks in order of left to most expensive.
  772. // Again, naive approach that does not capture many edge cases and browser quirks.
  773. return (isPotentiallyFocusable(element) &&
  774. !this.isDisabled(element) &&
  775. (config?.ignoreVisibility || this.isVisible(element)));
  776. }
  777. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: InteractivityChecker, deps: [{ token: i1.Platform }], target: i0.ɵɵFactoryTarget.Injectable }); }
  778. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: InteractivityChecker, providedIn: 'root' }); }
  779. }
  780. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: InteractivityChecker, decorators: [{
  781. type: Injectable,
  782. args: [{ providedIn: 'root' }]
  783. }], ctorParameters: function () { return [{ type: i1.Platform }]; } });
  784. /**
  785. * Returns the frame element from a window object. Since browsers like MS Edge throw errors if
  786. * the frameElement property is being accessed from a different host address, this property
  787. * should be accessed carefully.
  788. */
  789. function getFrameElement(window) {
  790. try {
  791. return window.frameElement;
  792. }
  793. catch {
  794. return null;
  795. }
  796. }
  797. /** Checks whether the specified element has any geometry / rectangles. */
  798. function hasGeometry(element) {
  799. // Use logic from jQuery to check for an invisible element.
  800. // See https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js#L12
  801. return !!(element.offsetWidth ||
  802. element.offsetHeight ||
  803. (typeof element.getClientRects === 'function' && element.getClientRects().length));
  804. }
  805. /** Gets whether an element's */
  806. function isNativeFormElement(element) {
  807. let nodeName = element.nodeName.toLowerCase();
  808. return (nodeName === 'input' ||
  809. nodeName === 'select' ||
  810. nodeName === 'button' ||
  811. nodeName === 'textarea');
  812. }
  813. /** Gets whether an element is an `<input type="hidden">`. */
  814. function isHiddenInput(element) {
  815. return isInputElement(element) && element.type == 'hidden';
  816. }
  817. /** Gets whether an element is an anchor that has an href attribute. */
  818. function isAnchorWithHref(element) {
  819. return isAnchorElement(element) && element.hasAttribute('href');
  820. }
  821. /** Gets whether an element is an input element. */
  822. function isInputElement(element) {
  823. return element.nodeName.toLowerCase() == 'input';
  824. }
  825. /** Gets whether an element is an anchor element. */
  826. function isAnchorElement(element) {
  827. return element.nodeName.toLowerCase() == 'a';
  828. }
  829. /** Gets whether an element has a valid tabindex. */
  830. function hasValidTabIndex(element) {
  831. if (!element.hasAttribute('tabindex') || element.tabIndex === undefined) {
  832. return false;
  833. }
  834. let tabIndex = element.getAttribute('tabindex');
  835. return !!(tabIndex && !isNaN(parseInt(tabIndex, 10)));
  836. }
  837. /**
  838. * Returns the parsed tabindex from the element attributes instead of returning the
  839. * evaluated tabindex from the browsers defaults.
  840. */
  841. function getTabIndexValue(element) {
  842. if (!hasValidTabIndex(element)) {
  843. return null;
  844. }
  845. // See browser issue in Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054
  846. const tabIndex = parseInt(element.getAttribute('tabindex') || '', 10);
  847. return isNaN(tabIndex) ? -1 : tabIndex;
  848. }
  849. /** Checks whether the specified element is potentially tabbable on iOS */
  850. function isPotentiallyTabbableIOS(element) {
  851. let nodeName = element.nodeName.toLowerCase();
  852. let inputType = nodeName === 'input' && element.type;
  853. return (inputType === 'text' ||
  854. inputType === 'password' ||
  855. nodeName === 'select' ||
  856. nodeName === 'textarea');
  857. }
  858. /**
  859. * Gets whether an element is potentially focusable without taking current visible/disabled state
  860. * into account.
  861. */
  862. function isPotentiallyFocusable(element) {
  863. // Inputs are potentially focusable *unless* they're type="hidden".
  864. if (isHiddenInput(element)) {
  865. return false;
  866. }
  867. return (isNativeFormElement(element) ||
  868. isAnchorWithHref(element) ||
  869. element.hasAttribute('contenteditable') ||
  870. hasValidTabIndex(element));
  871. }
  872. /** Gets the parent window of a DOM node with regards of being inside of an iframe. */
  873. function getWindow(node) {
  874. // ownerDocument is null if `node` itself *is* a document.
  875. return (node.ownerDocument && node.ownerDocument.defaultView) || window;
  876. }
  877. /**
  878. * Class that allows for trapping focus within a DOM element.
  879. *
  880. * This class currently uses a relatively simple approach to focus trapping.
  881. * It assumes that the tab order is the same as DOM order, which is not necessarily true.
  882. * Things like `tabIndex > 0`, flex `order`, and shadow roots can cause the two to be misaligned.
  883. *
  884. * @deprecated Use `ConfigurableFocusTrap` instead.
  885. * @breaking-change 11.0.0
  886. */
  887. class FocusTrap {
  888. /** Whether the focus trap is active. */
  889. get enabled() {
  890. return this._enabled;
  891. }
  892. set enabled(value) {
  893. this._enabled = value;
  894. if (this._startAnchor && this._endAnchor) {
  895. this._toggleAnchorTabIndex(value, this._startAnchor);
  896. this._toggleAnchorTabIndex(value, this._endAnchor);
  897. }
  898. }
  899. constructor(_element, _checker, _ngZone, _document, deferAnchors = false) {
  900. this._element = _element;
  901. this._checker = _checker;
  902. this._ngZone = _ngZone;
  903. this._document = _document;
  904. this._hasAttached = false;
  905. // Event listeners for the anchors. Need to be regular functions so that we can unbind them later.
  906. this.startAnchorListener = () => this.focusLastTabbableElement();
  907. this.endAnchorListener = () => this.focusFirstTabbableElement();
  908. this._enabled = true;
  909. if (!deferAnchors) {
  910. this.attachAnchors();
  911. }
  912. }
  913. /** Destroys the focus trap by cleaning up the anchors. */
  914. destroy() {
  915. const startAnchor = this._startAnchor;
  916. const endAnchor = this._endAnchor;
  917. if (startAnchor) {
  918. startAnchor.removeEventListener('focus', this.startAnchorListener);
  919. startAnchor.remove();
  920. }
  921. if (endAnchor) {
  922. endAnchor.removeEventListener('focus', this.endAnchorListener);
  923. endAnchor.remove();
  924. }
  925. this._startAnchor = this._endAnchor = null;
  926. this._hasAttached = false;
  927. }
  928. /**
  929. * Inserts the anchors into the DOM. This is usually done automatically
  930. * in the constructor, but can be deferred for cases like directives with `*ngIf`.
  931. * @returns Whether the focus trap managed to attach successfully. This may not be the case
  932. * if the target element isn't currently in the DOM.
  933. */
  934. attachAnchors() {
  935. // If we're not on the browser, there can be no focus to trap.
  936. if (this._hasAttached) {
  937. return true;
  938. }
  939. this._ngZone.runOutsideAngular(() => {
  940. if (!this._startAnchor) {
  941. this._startAnchor = this._createAnchor();
  942. this._startAnchor.addEventListener('focus', this.startAnchorListener);
  943. }
  944. if (!this._endAnchor) {
  945. this._endAnchor = this._createAnchor();
  946. this._endAnchor.addEventListener('focus', this.endAnchorListener);
  947. }
  948. });
  949. if (this._element.parentNode) {
  950. this._element.parentNode.insertBefore(this._startAnchor, this._element);
  951. this._element.parentNode.insertBefore(this._endAnchor, this._element.nextSibling);
  952. this._hasAttached = true;
  953. }
  954. return this._hasAttached;
  955. }
  956. /**
  957. * Waits for the zone to stabilize, then focuses the first tabbable element.
  958. * @returns Returns a promise that resolves with a boolean, depending
  959. * on whether focus was moved successfully.
  960. */
  961. focusInitialElementWhenReady(options) {
  962. return new Promise(resolve => {
  963. this._executeOnStable(() => resolve(this.focusInitialElement(options)));
  964. });
  965. }
  966. /**
  967. * Waits for the zone to stabilize, then focuses
  968. * the first tabbable element within the focus trap region.
  969. * @returns Returns a promise that resolves with a boolean, depending
  970. * on whether focus was moved successfully.
  971. */
  972. focusFirstTabbableElementWhenReady(options) {
  973. return new Promise(resolve => {
  974. this._executeOnStable(() => resolve(this.focusFirstTabbableElement(options)));
  975. });
  976. }
  977. /**
  978. * Waits for the zone to stabilize, then focuses
  979. * the last tabbable element within the focus trap region.
  980. * @returns Returns a promise that resolves with a boolean, depending
  981. * on whether focus was moved successfully.
  982. */
  983. focusLastTabbableElementWhenReady(options) {
  984. return new Promise(resolve => {
  985. this._executeOnStable(() => resolve(this.focusLastTabbableElement(options)));
  986. });
  987. }
  988. /**
  989. * Get the specified boundary element of the trapped region.
  990. * @param bound The boundary to get (start or end of trapped region).
  991. * @returns The boundary element.
  992. */
  993. _getRegionBoundary(bound) {
  994. // Contains the deprecated version of selector, for temporary backwards comparability.
  995. const markers = this._element.querySelectorAll(`[cdk-focus-region-${bound}], ` + `[cdkFocusRegion${bound}], ` + `[cdk-focus-${bound}]`);
  996. if (typeof ngDevMode === 'undefined' || ngDevMode) {
  997. for (let i = 0; i < markers.length; i++) {
  998. // @breaking-change 8.0.0
  999. if (markers[i].hasAttribute(`cdk-focus-${bound}`)) {
  1000. console.warn(`Found use of deprecated attribute 'cdk-focus-${bound}', ` +
  1001. `use 'cdkFocusRegion${bound}' instead. The deprecated ` +
  1002. `attribute will be removed in 8.0.0.`, markers[i]);
  1003. }
  1004. else if (markers[i].hasAttribute(`cdk-focus-region-${bound}`)) {
  1005. console.warn(`Found use of deprecated attribute 'cdk-focus-region-${bound}', ` +
  1006. `use 'cdkFocusRegion${bound}' instead. The deprecated attribute ` +
  1007. `will be removed in 8.0.0.`, markers[i]);
  1008. }
  1009. }
  1010. }
  1011. if (bound == 'start') {
  1012. return markers.length ? markers[0] : this._getFirstTabbableElement(this._element);
  1013. }
  1014. return markers.length
  1015. ? markers[markers.length - 1]
  1016. : this._getLastTabbableElement(this._element);
  1017. }
  1018. /**
  1019. * Focuses the element that should be focused when the focus trap is initialized.
  1020. * @returns Whether focus was moved successfully.
  1021. */
  1022. focusInitialElement(options) {
  1023. // Contains the deprecated version of selector, for temporary backwards comparability.
  1024. const redirectToElement = this._element.querySelector(`[cdk-focus-initial], ` + `[cdkFocusInitial]`);
  1025. if (redirectToElement) {
  1026. // @breaking-change 8.0.0
  1027. if ((typeof ngDevMode === 'undefined' || ngDevMode) &&
  1028. redirectToElement.hasAttribute(`cdk-focus-initial`)) {
  1029. console.warn(`Found use of deprecated attribute 'cdk-focus-initial', ` +
  1030. `use 'cdkFocusInitial' instead. The deprecated attribute ` +
  1031. `will be removed in 8.0.0`, redirectToElement);
  1032. }
  1033. // Warn the consumer if the element they've pointed to
  1034. // isn't focusable, when not in production mode.
  1035. if ((typeof ngDevMode === 'undefined' || ngDevMode) &&
  1036. !this._checker.isFocusable(redirectToElement)) {
  1037. console.warn(`Element matching '[cdkFocusInitial]' is not focusable.`, redirectToElement);
  1038. }
  1039. if (!this._checker.isFocusable(redirectToElement)) {
  1040. const focusableChild = this._getFirstTabbableElement(redirectToElement);
  1041. focusableChild?.focus(options);
  1042. return !!focusableChild;
  1043. }
  1044. redirectToElement.focus(options);
  1045. return true;
  1046. }
  1047. return this.focusFirstTabbableElement(options);
  1048. }
  1049. /**
  1050. * Focuses the first tabbable element within the focus trap region.
  1051. * @returns Whether focus was moved successfully.
  1052. */
  1053. focusFirstTabbableElement(options) {
  1054. const redirectToElement = this._getRegionBoundary('start');
  1055. if (redirectToElement) {
  1056. redirectToElement.focus(options);
  1057. }
  1058. return !!redirectToElement;
  1059. }
  1060. /**
  1061. * Focuses the last tabbable element within the focus trap region.
  1062. * @returns Whether focus was moved successfully.
  1063. */
  1064. focusLastTabbableElement(options) {
  1065. const redirectToElement = this._getRegionBoundary('end');
  1066. if (redirectToElement) {
  1067. redirectToElement.focus(options);
  1068. }
  1069. return !!redirectToElement;
  1070. }
  1071. /**
  1072. * Checks whether the focus trap has successfully been attached.
  1073. */
  1074. hasAttached() {
  1075. return this._hasAttached;
  1076. }
  1077. /** Get the first tabbable element from a DOM subtree (inclusive). */
  1078. _getFirstTabbableElement(root) {
  1079. if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
  1080. return root;
  1081. }
  1082. const children = root.children;
  1083. for (let i = 0; i < children.length; i++) {
  1084. const tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE
  1085. ? this._getFirstTabbableElement(children[i])
  1086. : null;
  1087. if (tabbableChild) {
  1088. return tabbableChild;
  1089. }
  1090. }
  1091. return null;
  1092. }
  1093. /** Get the last tabbable element from a DOM subtree (inclusive). */
  1094. _getLastTabbableElement(root) {
  1095. if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
  1096. return root;
  1097. }
  1098. // Iterate in reverse DOM order.
  1099. const children = root.children;
  1100. for (let i = children.length - 1; i >= 0; i--) {
  1101. const tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE
  1102. ? this._getLastTabbableElement(children[i])
  1103. : null;
  1104. if (tabbableChild) {
  1105. return tabbableChild;
  1106. }
  1107. }
  1108. return null;
  1109. }
  1110. /** Creates an anchor element. */
  1111. _createAnchor() {
  1112. const anchor = this._document.createElement('div');
  1113. this._toggleAnchorTabIndex(this._enabled, anchor);
  1114. anchor.classList.add('cdk-visually-hidden');
  1115. anchor.classList.add('cdk-focus-trap-anchor');
  1116. anchor.setAttribute('aria-hidden', 'true');
  1117. return anchor;
  1118. }
  1119. /**
  1120. * Toggles the `tabindex` of an anchor, based on the enabled state of the focus trap.
  1121. * @param isEnabled Whether the focus trap is enabled.
  1122. * @param anchor Anchor on which to toggle the tabindex.
  1123. */
  1124. _toggleAnchorTabIndex(isEnabled, anchor) {
  1125. // Remove the tabindex completely, rather than setting it to -1, because if the
  1126. // element has a tabindex, the user might still hit it when navigating with the arrow keys.
  1127. isEnabled ? anchor.setAttribute('tabindex', '0') : anchor.removeAttribute('tabindex');
  1128. }
  1129. /**
  1130. * Toggles the`tabindex` of both anchors to either trap Tab focus or allow it to escape.
  1131. * @param enabled: Whether the anchors should trap Tab.
  1132. */
  1133. toggleAnchors(enabled) {
  1134. if (this._startAnchor && this._endAnchor) {
  1135. this._toggleAnchorTabIndex(enabled, this._startAnchor);
  1136. this._toggleAnchorTabIndex(enabled, this._endAnchor);
  1137. }
  1138. }
  1139. /** Executes a function when the zone is stable. */
  1140. _executeOnStable(fn) {
  1141. if (this._ngZone.isStable) {
  1142. fn();
  1143. }
  1144. else {
  1145. this._ngZone.onStable.pipe(take(1)).subscribe(fn);
  1146. }
  1147. }
  1148. }
  1149. /**
  1150. * Factory that allows easy instantiation of focus traps.
  1151. * @deprecated Use `ConfigurableFocusTrapFactory` instead.
  1152. * @breaking-change 11.0.0
  1153. */
  1154. class FocusTrapFactory {
  1155. constructor(_checker, _ngZone, _document) {
  1156. this._checker = _checker;
  1157. this._ngZone = _ngZone;
  1158. this._document = _document;
  1159. }
  1160. /**
  1161. * Creates a focus-trapped region around the given element.
  1162. * @param element The element around which focus will be trapped.
  1163. * @param deferCaptureElements Defers the creation of focus-capturing elements to be done
  1164. * manually by the user.
  1165. * @returns The created focus trap instance.
  1166. */
  1167. create(element, deferCaptureElements = false) {
  1168. return new FocusTrap(element, this._checker, this._ngZone, this._document, deferCaptureElements);
  1169. }
  1170. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: FocusTrapFactory, deps: [{ token: InteractivityChecker }, { token: i0.NgZone }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); }
  1171. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: FocusTrapFactory, providedIn: 'root' }); }
  1172. }
  1173. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: FocusTrapFactory, decorators: [{
  1174. type: Injectable,
  1175. args: [{ providedIn: 'root' }]
  1176. }], ctorParameters: function () { return [{ type: InteractivityChecker }, { type: i0.NgZone }, { type: undefined, decorators: [{
  1177. type: Inject,
  1178. args: [DOCUMENT]
  1179. }] }]; } });
  1180. /** Directive for trapping focus within a region. */
  1181. class CdkTrapFocus {
  1182. /** Whether the focus trap is active. */
  1183. get enabled() {
  1184. return this.focusTrap.enabled;
  1185. }
  1186. set enabled(value) {
  1187. this.focusTrap.enabled = coerceBooleanProperty(value);
  1188. }
  1189. /**
  1190. * Whether the directive should automatically move focus into the trapped region upon
  1191. * initialization and return focus to the previous activeElement upon destruction.
  1192. */
  1193. get autoCapture() {
  1194. return this._autoCapture;
  1195. }
  1196. set autoCapture(value) {
  1197. this._autoCapture = coerceBooleanProperty(value);
  1198. }
  1199. constructor(_elementRef, _focusTrapFactory,
  1200. /**
  1201. * @deprecated No longer being used. To be removed.
  1202. * @breaking-change 13.0.0
  1203. */
  1204. _document) {
  1205. this._elementRef = _elementRef;
  1206. this._focusTrapFactory = _focusTrapFactory;
  1207. /** Previously focused element to restore focus to upon destroy when using autoCapture. */
  1208. this._previouslyFocusedElement = null;
  1209. this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
  1210. }
  1211. ngOnDestroy() {
  1212. this.focusTrap.destroy();
  1213. // If we stored a previously focused element when using autoCapture, return focus to that
  1214. // element now that the trapped region is being destroyed.
  1215. if (this._previouslyFocusedElement) {
  1216. this._previouslyFocusedElement.focus();
  1217. this._previouslyFocusedElement = null;
  1218. }
  1219. }
  1220. ngAfterContentInit() {
  1221. this.focusTrap.attachAnchors();
  1222. if (this.autoCapture) {
  1223. this._captureFocus();
  1224. }
  1225. }
  1226. ngDoCheck() {
  1227. if (!this.focusTrap.hasAttached()) {
  1228. this.focusTrap.attachAnchors();
  1229. }
  1230. }
  1231. ngOnChanges(changes) {
  1232. const autoCaptureChange = changes['autoCapture'];
  1233. if (autoCaptureChange &&
  1234. !autoCaptureChange.firstChange &&
  1235. this.autoCapture &&
  1236. this.focusTrap.hasAttached()) {
  1237. this._captureFocus();
  1238. }
  1239. }
  1240. _captureFocus() {
  1241. this._previouslyFocusedElement = _getFocusedElementPierceShadowDom();
  1242. this.focusTrap.focusInitialElementWhenReady();
  1243. }
  1244. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkTrapFocus, deps: [{ token: i0.ElementRef }, { token: FocusTrapFactory }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Directive }); }
  1245. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: { enabled: ["cdkTrapFocus", "enabled"], autoCapture: ["cdkTrapFocusAutoCapture", "autoCapture"] }, exportAs: ["cdkTrapFocus"], usesOnChanges: true, ngImport: i0 }); }
  1246. }
  1247. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkTrapFocus, decorators: [{
  1248. type: Directive,
  1249. args: [{
  1250. selector: '[cdkTrapFocus]',
  1251. exportAs: 'cdkTrapFocus',
  1252. }]
  1253. }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: FocusTrapFactory }, { type: undefined, decorators: [{
  1254. type: Inject,
  1255. args: [DOCUMENT]
  1256. }] }]; }, propDecorators: { enabled: [{
  1257. type: Input,
  1258. args: ['cdkTrapFocus']
  1259. }], autoCapture: [{
  1260. type: Input,
  1261. args: ['cdkTrapFocusAutoCapture']
  1262. }] } });
  1263. /**
  1264. * Class that allows for trapping focus within a DOM element.
  1265. *
  1266. * This class uses a strategy pattern that determines how it traps focus.
  1267. * See FocusTrapInertStrategy.
  1268. */
  1269. class ConfigurableFocusTrap extends FocusTrap {
  1270. /** Whether the FocusTrap is enabled. */
  1271. get enabled() {
  1272. return this._enabled;
  1273. }
  1274. set enabled(value) {
  1275. this._enabled = value;
  1276. if (this._enabled) {
  1277. this._focusTrapManager.register(this);
  1278. }
  1279. else {
  1280. this._focusTrapManager.deregister(this);
  1281. }
  1282. }
  1283. constructor(_element, _checker, _ngZone, _document, _focusTrapManager, _inertStrategy, config) {
  1284. super(_element, _checker, _ngZone, _document, config.defer);
  1285. this._focusTrapManager = _focusTrapManager;
  1286. this._inertStrategy = _inertStrategy;
  1287. this._focusTrapManager.register(this);
  1288. }
  1289. /** Notifies the FocusTrapManager that this FocusTrap will be destroyed. */
  1290. destroy() {
  1291. this._focusTrapManager.deregister(this);
  1292. super.destroy();
  1293. }
  1294. /** @docs-private Implemented as part of ManagedFocusTrap. */
  1295. _enable() {
  1296. this._inertStrategy.preventFocus(this);
  1297. this.toggleAnchors(true);
  1298. }
  1299. /** @docs-private Implemented as part of ManagedFocusTrap. */
  1300. _disable() {
  1301. this._inertStrategy.allowFocus(this);
  1302. this.toggleAnchors(false);
  1303. }
  1304. }
  1305. /** The injection token used to specify the inert strategy. */
  1306. const FOCUS_TRAP_INERT_STRATEGY = new InjectionToken('FOCUS_TRAP_INERT_STRATEGY');
  1307. /**
  1308. * Lightweight FocusTrapInertStrategy that adds a document focus event
  1309. * listener to redirect focus back inside the FocusTrap.
  1310. */
  1311. class EventListenerFocusTrapInertStrategy {
  1312. constructor() {
  1313. /** Focus event handler. */
  1314. this._listener = null;
  1315. }
  1316. /** Adds a document event listener that keeps focus inside the FocusTrap. */
  1317. preventFocus(focusTrap) {
  1318. // Ensure there's only one listener per document
  1319. if (this._listener) {
  1320. focusTrap._document.removeEventListener('focus', this._listener, true);
  1321. }
  1322. this._listener = (e) => this._trapFocus(focusTrap, e);
  1323. focusTrap._ngZone.runOutsideAngular(() => {
  1324. focusTrap._document.addEventListener('focus', this._listener, true);
  1325. });
  1326. }
  1327. /** Removes the event listener added in preventFocus. */
  1328. allowFocus(focusTrap) {
  1329. if (!this._listener) {
  1330. return;
  1331. }
  1332. focusTrap._document.removeEventListener('focus', this._listener, true);
  1333. this._listener = null;
  1334. }
  1335. /**
  1336. * Refocuses the first element in the FocusTrap if the focus event target was outside
  1337. * the FocusTrap.
  1338. *
  1339. * This is an event listener callback. The event listener is added in runOutsideAngular,
  1340. * so all this code runs outside Angular as well.
  1341. */
  1342. _trapFocus(focusTrap, event) {
  1343. const target = event.target;
  1344. const focusTrapRoot = focusTrap._element;
  1345. // Don't refocus if target was in an overlay, because the overlay might be associated
  1346. // with an element inside the FocusTrap, ex. mat-select.
  1347. if (target && !focusTrapRoot.contains(target) && !target.closest?.('div.cdk-overlay-pane')) {
  1348. // Some legacy FocusTrap usages have logic that focuses some element on the page
  1349. // just before FocusTrap is destroyed. For backwards compatibility, wait
  1350. // to be sure FocusTrap is still enabled before refocusing.
  1351. setTimeout(() => {
  1352. // Check whether focus wasn't put back into the focus trap while the timeout was pending.
  1353. if (focusTrap.enabled && !focusTrapRoot.contains(focusTrap._document.activeElement)) {
  1354. focusTrap.focusFirstTabbableElement();
  1355. }
  1356. });
  1357. }
  1358. }
  1359. }
  1360. /** Injectable that ensures only the most recently enabled FocusTrap is active. */
  1361. class FocusTrapManager {
  1362. constructor() {
  1363. // A stack of the FocusTraps on the page. Only the FocusTrap at the
  1364. // top of the stack is active.
  1365. this._focusTrapStack = [];
  1366. }
  1367. /**
  1368. * Disables the FocusTrap at the top of the stack, and then pushes
  1369. * the new FocusTrap onto the stack.
  1370. */
  1371. register(focusTrap) {
  1372. // Dedupe focusTraps that register multiple times.
  1373. this._focusTrapStack = this._focusTrapStack.filter(ft => ft !== focusTrap);
  1374. let stack = this._focusTrapStack;
  1375. if (stack.length) {
  1376. stack[stack.length - 1]._disable();
  1377. }
  1378. stack.push(focusTrap);
  1379. focusTrap._enable();
  1380. }
  1381. /**
  1382. * Removes the FocusTrap from the stack, and activates the
  1383. * FocusTrap that is the new top of the stack.
  1384. */
  1385. deregister(focusTrap) {
  1386. focusTrap._disable();
  1387. const stack = this._focusTrapStack;
  1388. const i = stack.indexOf(focusTrap);
  1389. if (i !== -1) {
  1390. stack.splice(i, 1);
  1391. if (stack.length) {
  1392. stack[stack.length - 1]._enable();
  1393. }
  1394. }
  1395. }
  1396. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: FocusTrapManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
  1397. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: FocusTrapManager, providedIn: 'root' }); }
  1398. }
  1399. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: FocusTrapManager, decorators: [{
  1400. type: Injectable,
  1401. args: [{ providedIn: 'root' }]
  1402. }] });
  1403. /** Factory that allows easy instantiation of configurable focus traps. */
  1404. class ConfigurableFocusTrapFactory {
  1405. constructor(_checker, _ngZone, _focusTrapManager, _document, _inertStrategy) {
  1406. this._checker = _checker;
  1407. this._ngZone = _ngZone;
  1408. this._focusTrapManager = _focusTrapManager;
  1409. this._document = _document;
  1410. // TODO split up the strategies into different modules, similar to DateAdapter.
  1411. this._inertStrategy = _inertStrategy || new EventListenerFocusTrapInertStrategy();
  1412. }
  1413. create(element, config = { defer: false }) {
  1414. let configObject;
  1415. if (typeof config === 'boolean') {
  1416. configObject = { defer: config };
  1417. }
  1418. else {
  1419. configObject = config;
  1420. }
  1421. return new ConfigurableFocusTrap(element, this._checker, this._ngZone, this._document, this._focusTrapManager, this._inertStrategy, configObject);
  1422. }
  1423. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: ConfigurableFocusTrapFactory, deps: [{ token: InteractivityChecker }, { token: i0.NgZone }, { token: FocusTrapManager }, { token: DOCUMENT }, { token: FOCUS_TRAP_INERT_STRATEGY, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
  1424. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: ConfigurableFocusTrapFactory, providedIn: 'root' }); }
  1425. }
  1426. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: ConfigurableFocusTrapFactory, decorators: [{
  1427. type: Injectable,
  1428. args: [{ providedIn: 'root' }]
  1429. }], ctorParameters: function () { return [{ type: InteractivityChecker }, { type: i0.NgZone }, { type: FocusTrapManager }, { type: undefined, decorators: [{
  1430. type: Inject,
  1431. args: [DOCUMENT]
  1432. }] }, { type: undefined, decorators: [{
  1433. type: Optional
  1434. }, {
  1435. type: Inject,
  1436. args: [FOCUS_TRAP_INERT_STRATEGY]
  1437. }] }]; } });
  1438. /** Gets whether an event could be a faked `mousedown` event dispatched by a screen reader. */
  1439. function isFakeMousedownFromScreenReader(event) {
  1440. // Some screen readers will dispatch a fake `mousedown` event when pressing enter or space on
  1441. // a clickable element. We can distinguish these events when both `offsetX` and `offsetY` are
  1442. // zero or `event.buttons` is zero, depending on the browser:
  1443. // - `event.buttons` works on Firefox, but fails on Chrome.
  1444. // - `offsetX` and `offsetY` work on Chrome, but fail on Firefox.
  1445. // Note that there's an edge case where the user could click the 0x0 spot of the
  1446. // screen themselves, but that is unlikely to contain interactive elements.
  1447. return event.buttons === 0 || (event.offsetX === 0 && event.offsetY === 0);
  1448. }
  1449. /** Gets whether an event could be a faked `touchstart` event dispatched by a screen reader. */
  1450. function isFakeTouchstartFromScreenReader(event) {
  1451. const touch = (event.touches && event.touches[0]) || (event.changedTouches && event.changedTouches[0]);
  1452. // A fake `touchstart` can be distinguished from a real one by looking at the `identifier`
  1453. // which is typically >= 0 on a real device versus -1 from a screen reader. Just to be safe,
  1454. // we can also look at `radiusX` and `radiusY`. This behavior was observed against a Windows 10
  1455. // device with a touch screen running NVDA v2020.4 and Firefox 85 or Chrome 88.
  1456. return (!!touch &&
  1457. touch.identifier === -1 &&
  1458. (touch.radiusX == null || touch.radiusX === 1) &&
  1459. (touch.radiusY == null || touch.radiusY === 1));
  1460. }
  1461. /**
  1462. * Injectable options for the InputModalityDetector. These are shallowly merged with the default
  1463. * options.
  1464. */
  1465. const INPUT_MODALITY_DETECTOR_OPTIONS = new InjectionToken('cdk-input-modality-detector-options');
  1466. /**
  1467. * Default options for the InputModalityDetector.
  1468. *
  1469. * Modifier keys are ignored by default (i.e. when pressed won't cause the service to detect
  1470. * keyboard input modality) for two reasons:
  1471. *
  1472. * 1. Modifier keys are commonly used with mouse to perform actions such as 'right click' or 'open
  1473. * in new tab', and are thus less representative of actual keyboard interaction.
  1474. * 2. VoiceOver triggers some keyboard events when linearly navigating with Control + Option (but
  1475. * confusingly not with Caps Lock). Thus, to have parity with other screen readers, we ignore
  1476. * these keys so as to not update the input modality.
  1477. *
  1478. * Note that we do not by default ignore the right Meta key on Safari because it has the same key
  1479. * code as the ContextMenu key on other browsers. When we switch to using event.key, we can
  1480. * distinguish between the two.
  1481. */
  1482. const INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS = {
  1483. ignoreKeys: [ALT, CONTROL, MAC_META, META, SHIFT],
  1484. };
  1485. /**
  1486. * The amount of time needed to pass after a touchstart event in order for a subsequent mousedown
  1487. * event to be attributed as mouse and not touch.
  1488. *
  1489. * This is the value used by AngularJS Material. Through trial and error (on iPhone 6S) they found
  1490. * that a value of around 650ms seems appropriate.
  1491. */
  1492. const TOUCH_BUFFER_MS = 650;
  1493. /**
  1494. * Event listener options that enable capturing and also mark the listener as passive if the browser
  1495. * supports it.
  1496. */
  1497. const modalityEventListenerOptions = normalizePassiveListenerOptions({
  1498. passive: true,
  1499. capture: true,
  1500. });
  1501. /**
  1502. * Service that detects the user's input modality.
  1503. *
  1504. * This service does not update the input modality when a user navigates with a screen reader
  1505. * (e.g. linear navigation with VoiceOver, object navigation / browse mode with NVDA, virtual PC
  1506. * cursor mode with JAWS). This is in part due to technical limitations (i.e. keyboard events do not
  1507. * fire as expected in these modes) but is also arguably the correct behavior. Navigating with a
  1508. * screen reader is akin to visually scanning a page, and should not be interpreted as actual user
  1509. * input interaction.
  1510. *
  1511. * When a user is not navigating but *interacting* with a screen reader, this service attempts to
  1512. * update the input modality to keyboard, but in general this service's behavior is largely
  1513. * undefined.
  1514. */
  1515. class InputModalityDetector {
  1516. /** The most recently detected input modality. */
  1517. get mostRecentModality() {
  1518. return this._modality.value;
  1519. }
  1520. constructor(_platform, ngZone, document, options) {
  1521. this._platform = _platform;
  1522. /**
  1523. * The most recently detected input modality event target. Is null if no input modality has been
  1524. * detected or if the associated event target is null for some unknown reason.
  1525. */
  1526. this._mostRecentTarget = null;
  1527. /** The underlying BehaviorSubject that emits whenever an input modality is detected. */
  1528. this._modality = new BehaviorSubject(null);
  1529. /**
  1530. * The timestamp of the last touch input modality. Used to determine whether mousedown events
  1531. * should be attributed to mouse or touch.
  1532. */
  1533. this._lastTouchMs = 0;
  1534. /**
  1535. * Handles keydown events. Must be an arrow function in order to preserve the context when it gets
  1536. * bound.
  1537. */
  1538. this._onKeydown = (event) => {
  1539. // If this is one of the keys we should ignore, then ignore it and don't update the input
  1540. // modality to keyboard.
  1541. if (this._options?.ignoreKeys?.some(keyCode => keyCode === event.keyCode)) {
  1542. return;
  1543. }
  1544. this._modality.next('keyboard');
  1545. this._mostRecentTarget = _getEventTarget(event);
  1546. };
  1547. /**
  1548. * Handles mousedown events. Must be an arrow function in order to preserve the context when it
  1549. * gets bound.
  1550. */
  1551. this._onMousedown = (event) => {
  1552. // Touches trigger both touch and mouse events, so we need to distinguish between mouse events
  1553. // that were triggered via mouse vs touch. To do so, check if the mouse event occurs closely
  1554. // after the previous touch event.
  1555. if (Date.now() - this._lastTouchMs < TOUCH_BUFFER_MS) {
  1556. return;
  1557. }
  1558. // Fake mousedown events are fired by some screen readers when controls are activated by the
  1559. // screen reader. Attribute them to keyboard input modality.
  1560. this._modality.next(isFakeMousedownFromScreenReader(event) ? 'keyboard' : 'mouse');
  1561. this._mostRecentTarget = _getEventTarget(event);
  1562. };
  1563. /**
  1564. * Handles touchstart events. Must be an arrow function in order to preserve the context when it
  1565. * gets bound.
  1566. */
  1567. this._onTouchstart = (event) => {
  1568. // Same scenario as mentioned in _onMousedown, but on touch screen devices, fake touchstart
  1569. // events are fired. Again, attribute to keyboard input modality.
  1570. if (isFakeTouchstartFromScreenReader(event)) {
  1571. this._modality.next('keyboard');
  1572. return;
  1573. }
  1574. // Store the timestamp of this touch event, as it's used to distinguish between mouse events
  1575. // triggered via mouse vs touch.
  1576. this._lastTouchMs = Date.now();
  1577. this._modality.next('touch');
  1578. this._mostRecentTarget = _getEventTarget(event);
  1579. };
  1580. this._options = {
  1581. ...INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS,
  1582. ...options,
  1583. };
  1584. // Skip the first emission as it's null.
  1585. this.modalityDetected = this._modality.pipe(skip(1));
  1586. this.modalityChanged = this.modalityDetected.pipe(distinctUntilChanged());
  1587. // If we're not in a browser, this service should do nothing, as there's no relevant input
  1588. // modality to detect.
  1589. if (_platform.isBrowser) {
  1590. ngZone.runOutsideAngular(() => {
  1591. document.addEventListener('keydown', this._onKeydown, modalityEventListenerOptions);
  1592. document.addEventListener('mousedown', this._onMousedown, modalityEventListenerOptions);
  1593. document.addEventListener('touchstart', this._onTouchstart, modalityEventListenerOptions);
  1594. });
  1595. }
  1596. }
  1597. ngOnDestroy() {
  1598. this._modality.complete();
  1599. if (this._platform.isBrowser) {
  1600. document.removeEventListener('keydown', this._onKeydown, modalityEventListenerOptions);
  1601. document.removeEventListener('mousedown', this._onMousedown, modalityEventListenerOptions);
  1602. document.removeEventListener('touchstart', this._onTouchstart, modalityEventListenerOptions);
  1603. }
  1604. }
  1605. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: InputModalityDetector, deps: [{ token: i1.Platform }, { token: i0.NgZone }, { token: DOCUMENT }, { token: INPUT_MODALITY_DETECTOR_OPTIONS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
  1606. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: InputModalityDetector, providedIn: 'root' }); }
  1607. }
  1608. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: InputModalityDetector, decorators: [{
  1609. type: Injectable,
  1610. args: [{ providedIn: 'root' }]
  1611. }], ctorParameters: function () { return [{ type: i1.Platform }, { type: i0.NgZone }, { type: Document, decorators: [{
  1612. type: Inject,
  1613. args: [DOCUMENT]
  1614. }] }, { type: undefined, decorators: [{
  1615. type: Optional
  1616. }, {
  1617. type: Inject,
  1618. args: [INPUT_MODALITY_DETECTOR_OPTIONS]
  1619. }] }]; } });
  1620. const LIVE_ANNOUNCER_ELEMENT_TOKEN = new InjectionToken('liveAnnouncerElement', {
  1621. providedIn: 'root',
  1622. factory: LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY,
  1623. });
  1624. /** @docs-private */
  1625. function LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY() {
  1626. return null;
  1627. }
  1628. /** Injection token that can be used to configure the default options for the LiveAnnouncer. */
  1629. const LIVE_ANNOUNCER_DEFAULT_OPTIONS = new InjectionToken('LIVE_ANNOUNCER_DEFAULT_OPTIONS');
  1630. let uniqueIds = 0;
  1631. class LiveAnnouncer {
  1632. constructor(elementToken, _ngZone, _document, _defaultOptions) {
  1633. this._ngZone = _ngZone;
  1634. this._defaultOptions = _defaultOptions;
  1635. // We inject the live element and document as `any` because the constructor signature cannot
  1636. // reference browser globals (HTMLElement, Document) on non-browser environments, since having
  1637. // a class decorator causes TypeScript to preserve the constructor signature types.
  1638. this._document = _document;
  1639. this._liveElement = elementToken || this._createLiveElement();
  1640. }
  1641. announce(message, ...args) {
  1642. const defaultOptions = this._defaultOptions;
  1643. let politeness;
  1644. let duration;
  1645. if (args.length === 1 && typeof args[0] === 'number') {
  1646. duration = args[0];
  1647. }
  1648. else {
  1649. [politeness, duration] = args;
  1650. }
  1651. this.clear();
  1652. clearTimeout(this._previousTimeout);
  1653. if (!politeness) {
  1654. politeness =
  1655. defaultOptions && defaultOptions.politeness ? defaultOptions.politeness : 'polite';
  1656. }
  1657. if (duration == null && defaultOptions) {
  1658. duration = defaultOptions.duration;
  1659. }
  1660. // TODO: ensure changing the politeness works on all environments we support.
  1661. this._liveElement.setAttribute('aria-live', politeness);
  1662. if (this._liveElement.id) {
  1663. this._exposeAnnouncerToModals(this._liveElement.id);
  1664. }
  1665. // This 100ms timeout is necessary for some browser + screen-reader combinations:
  1666. // - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout.
  1667. // - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a
  1668. // second time without clearing and then using a non-zero delay.
  1669. // (using JAWS 17 at time of this writing).
  1670. return this._ngZone.runOutsideAngular(() => {
  1671. if (!this._currentPromise) {
  1672. this._currentPromise = new Promise(resolve => (this._currentResolve = resolve));
  1673. }
  1674. clearTimeout(this._previousTimeout);
  1675. this._previousTimeout = setTimeout(() => {
  1676. this._liveElement.textContent = message;
  1677. if (typeof duration === 'number') {
  1678. this._previousTimeout = setTimeout(() => this.clear(), duration);
  1679. }
  1680. this._currentResolve();
  1681. this._currentPromise = this._currentResolve = undefined;
  1682. }, 100);
  1683. return this._currentPromise;
  1684. });
  1685. }
  1686. /**
  1687. * Clears the current text from the announcer element. Can be used to prevent
  1688. * screen readers from reading the text out again while the user is going
  1689. * through the page landmarks.
  1690. */
  1691. clear() {
  1692. if (this._liveElement) {
  1693. this._liveElement.textContent = '';
  1694. }
  1695. }
  1696. ngOnDestroy() {
  1697. clearTimeout(this._previousTimeout);
  1698. this._liveElement?.remove();
  1699. this._liveElement = null;
  1700. this._currentResolve?.();
  1701. this._currentPromise = this._currentResolve = undefined;
  1702. }
  1703. _createLiveElement() {
  1704. const elementClass = 'cdk-live-announcer-element';
  1705. const previousElements = this._document.getElementsByClassName(elementClass);
  1706. const liveEl = this._document.createElement('div');
  1707. // Remove any old containers. This can happen when coming in from a server-side-rendered page.
  1708. for (let i = 0; i < previousElements.length; i++) {
  1709. previousElements[i].remove();
  1710. }
  1711. liveEl.classList.add(elementClass);
  1712. liveEl.classList.add('cdk-visually-hidden');
  1713. liveEl.setAttribute('aria-atomic', 'true');
  1714. liveEl.setAttribute('aria-live', 'polite');
  1715. liveEl.id = `cdk-live-announcer-${uniqueIds++}`;
  1716. this._document.body.appendChild(liveEl);
  1717. return liveEl;
  1718. }
  1719. /**
  1720. * Some browsers won't expose the accessibility node of the live announcer element if there is an
  1721. * `aria-modal` and the live announcer is outside of it. This method works around the issue by
  1722. * pointing the `aria-owns` of all modals to the live announcer element.
  1723. */
  1724. _exposeAnnouncerToModals(id) {
  1725. // Note that the selector here is limited to CDK overlays at the moment in order to reduce the
  1726. // section of the DOM we need to look through. This should cover all the cases we support, but
  1727. // the selector can be expanded if it turns out to be too narrow.
  1728. const modals = this._document.querySelectorAll('body > .cdk-overlay-container [aria-modal="true"]');
  1729. for (let i = 0; i < modals.length; i++) {
  1730. const modal = modals[i];
  1731. const ariaOwns = modal.getAttribute('aria-owns');
  1732. if (!ariaOwns) {
  1733. modal.setAttribute('aria-owns', id);
  1734. }
  1735. else if (ariaOwns.indexOf(id) === -1) {
  1736. modal.setAttribute('aria-owns', ariaOwns + ' ' + id);
  1737. }
  1738. }
  1739. }
  1740. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: LiveAnnouncer, deps: [{ token: LIVE_ANNOUNCER_ELEMENT_TOKEN, optional: true }, { token: i0.NgZone }, { token: DOCUMENT }, { token: LIVE_ANNOUNCER_DEFAULT_OPTIONS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
  1741. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: LiveAnnouncer, providedIn: 'root' }); }
  1742. }
  1743. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: LiveAnnouncer, decorators: [{
  1744. type: Injectable,
  1745. args: [{ providedIn: 'root' }]
  1746. }], ctorParameters: function () { return [{ type: undefined, decorators: [{
  1747. type: Optional
  1748. }, {
  1749. type: Inject,
  1750. args: [LIVE_ANNOUNCER_ELEMENT_TOKEN]
  1751. }] }, { type: i0.NgZone }, { type: undefined, decorators: [{
  1752. type: Inject,
  1753. args: [DOCUMENT]
  1754. }] }, { type: undefined, decorators: [{
  1755. type: Optional
  1756. }, {
  1757. type: Inject,
  1758. args: [LIVE_ANNOUNCER_DEFAULT_OPTIONS]
  1759. }] }]; } });
  1760. /**
  1761. * A directive that works similarly to aria-live, but uses the LiveAnnouncer to ensure compatibility
  1762. * with a wider range of browsers and screen readers.
  1763. */
  1764. class CdkAriaLive {
  1765. /** The aria-live politeness level to use when announcing messages. */
  1766. get politeness() {
  1767. return this._politeness;
  1768. }
  1769. set politeness(value) {
  1770. this._politeness = value === 'off' || value === 'assertive' ? value : 'polite';
  1771. if (this._politeness === 'off') {
  1772. if (this._subscription) {
  1773. this._subscription.unsubscribe();
  1774. this._subscription = null;
  1775. }
  1776. }
  1777. else if (!this._subscription) {
  1778. this._subscription = this._ngZone.runOutsideAngular(() => {
  1779. return this._contentObserver.observe(this._elementRef).subscribe(() => {
  1780. // Note that we use textContent here, rather than innerText, in order to avoid a reflow.
  1781. const elementText = this._elementRef.nativeElement.textContent;
  1782. // The `MutationObserver` fires also for attribute
  1783. // changes which we don't want to announce.
  1784. if (elementText !== this._previousAnnouncedText) {
  1785. this._liveAnnouncer.announce(elementText, this._politeness, this.duration);
  1786. this._previousAnnouncedText = elementText;
  1787. }
  1788. });
  1789. });
  1790. }
  1791. }
  1792. constructor(_elementRef, _liveAnnouncer, _contentObserver, _ngZone) {
  1793. this._elementRef = _elementRef;
  1794. this._liveAnnouncer = _liveAnnouncer;
  1795. this._contentObserver = _contentObserver;
  1796. this._ngZone = _ngZone;
  1797. this._politeness = 'polite';
  1798. }
  1799. ngOnDestroy() {
  1800. if (this._subscription) {
  1801. this._subscription.unsubscribe();
  1802. }
  1803. }
  1804. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkAriaLive, deps: [{ token: i0.ElementRef }, { token: LiveAnnouncer }, { token: i1$1.ContentObserver }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive }); }
  1805. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkAriaLive, selector: "[cdkAriaLive]", inputs: { politeness: ["cdkAriaLive", "politeness"], duration: ["cdkAriaLiveDuration", "duration"] }, exportAs: ["cdkAriaLive"], ngImport: i0 }); }
  1806. }
  1807. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkAriaLive, decorators: [{
  1808. type: Directive,
  1809. args: [{
  1810. selector: '[cdkAriaLive]',
  1811. exportAs: 'cdkAriaLive',
  1812. }]
  1813. }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: LiveAnnouncer }, { type: i1$1.ContentObserver }, { type: i0.NgZone }]; }, propDecorators: { politeness: [{
  1814. type: Input,
  1815. args: ['cdkAriaLive']
  1816. }], duration: [{
  1817. type: Input,
  1818. args: ['cdkAriaLiveDuration']
  1819. }] } });
  1820. /** InjectionToken for FocusMonitorOptions. */
  1821. const FOCUS_MONITOR_DEFAULT_OPTIONS = new InjectionToken('cdk-focus-monitor-default-options');
  1822. /**
  1823. * Event listener options that enable capturing and also
  1824. * mark the listener as passive if the browser supports it.
  1825. */
  1826. const captureEventListenerOptions = normalizePassiveListenerOptions({
  1827. passive: true,
  1828. capture: true,
  1829. });
  1830. /** Monitors mouse and keyboard events to determine the cause of focus events. */
  1831. class FocusMonitor {
  1832. constructor(_ngZone, _platform, _inputModalityDetector,
  1833. /** @breaking-change 11.0.0 make document required */
  1834. document, options) {
  1835. this._ngZone = _ngZone;
  1836. this._platform = _platform;
  1837. this._inputModalityDetector = _inputModalityDetector;
  1838. /** The focus origin that the next focus event is a result of. */
  1839. this._origin = null;
  1840. /** Whether the window has just been focused. */
  1841. this._windowFocused = false;
  1842. /**
  1843. * Whether the origin was determined via a touch interaction. Necessary as properly attributing
  1844. * focus events to touch interactions requires special logic.
  1845. */
  1846. this._originFromTouchInteraction = false;
  1847. /** Map of elements being monitored to their info. */
  1848. this._elementInfo = new Map();
  1849. /** The number of elements currently being monitored. */
  1850. this._monitoredElementCount = 0;
  1851. /**
  1852. * Keeps track of the root nodes to which we've currently bound a focus/blur handler,
  1853. * as well as the number of monitored elements that they contain. We have to treat focus/blur
  1854. * handlers differently from the rest of the events, because the browser won't emit events
  1855. * to the document when focus moves inside of a shadow root.
  1856. */
  1857. this._rootNodeFocusListenerCount = new Map();
  1858. /**
  1859. * Event listener for `focus` events on the window.
  1860. * Needs to be an arrow function in order to preserve the context when it gets bound.
  1861. */
  1862. this._windowFocusListener = () => {
  1863. // Make a note of when the window regains focus, so we can
  1864. // restore the origin info for the focused element.
  1865. this._windowFocused = true;
  1866. this._windowFocusTimeoutId = window.setTimeout(() => (this._windowFocused = false));
  1867. };
  1868. /** Subject for stopping our InputModalityDetector subscription. */
  1869. this._stopInputModalityDetector = new Subject();
  1870. /**
  1871. * Event listener for `focus` and 'blur' events on the document.
  1872. * Needs to be an arrow function in order to preserve the context when it gets bound.
  1873. */
  1874. this._rootNodeFocusAndBlurListener = (event) => {
  1875. const target = _getEventTarget(event);
  1876. // We need to walk up the ancestor chain in order to support `checkChildren`.
  1877. for (let element = target; element; element = element.parentElement) {
  1878. if (event.type === 'focus') {
  1879. this._onFocus(event, element);
  1880. }
  1881. else {
  1882. this._onBlur(event, element);
  1883. }
  1884. }
  1885. };
  1886. this._document = document;
  1887. this._detectionMode = options?.detectionMode || 0 /* FocusMonitorDetectionMode.IMMEDIATE */;
  1888. }
  1889. monitor(element, checkChildren = false) {
  1890. const nativeElement = coerceElement(element);
  1891. // Do nothing if we're not on the browser platform or the passed in node isn't an element.
  1892. if (!this._platform.isBrowser || nativeElement.nodeType !== 1) {
  1893. return of(null);
  1894. }
  1895. // If the element is inside the shadow DOM, we need to bind our focus/blur listeners to
  1896. // the shadow root, rather than the `document`, because the browser won't emit focus events
  1897. // to the `document`, if focus is moving within the same shadow root.
  1898. const rootNode = _getShadowRoot(nativeElement) || this._getDocument();
  1899. const cachedInfo = this._elementInfo.get(nativeElement);
  1900. // Check if we're already monitoring this element.
  1901. if (cachedInfo) {
  1902. if (checkChildren) {
  1903. // TODO(COMP-318): this can be problematic, because it'll turn all non-checkChildren
  1904. // observers into ones that behave as if `checkChildren` was turned on. We need a more
  1905. // robust solution.
  1906. cachedInfo.checkChildren = true;
  1907. }
  1908. return cachedInfo.subject;
  1909. }
  1910. // Create monitored element info.
  1911. const info = {
  1912. checkChildren: checkChildren,
  1913. subject: new Subject(),
  1914. rootNode,
  1915. };
  1916. this._elementInfo.set(nativeElement, info);
  1917. this._registerGlobalListeners(info);
  1918. return info.subject;
  1919. }
  1920. stopMonitoring(element) {
  1921. const nativeElement = coerceElement(element);
  1922. const elementInfo = this._elementInfo.get(nativeElement);
  1923. if (elementInfo) {
  1924. elementInfo.subject.complete();
  1925. this._setClasses(nativeElement);
  1926. this._elementInfo.delete(nativeElement);
  1927. this._removeGlobalListeners(elementInfo);
  1928. }
  1929. }
  1930. focusVia(element, origin, options) {
  1931. const nativeElement = coerceElement(element);
  1932. const focusedElement = this._getDocument().activeElement;
  1933. // If the element is focused already, calling `focus` again won't trigger the event listener
  1934. // which means that the focus classes won't be updated. If that's the case, update the classes
  1935. // directly without waiting for an event.
  1936. if (nativeElement === focusedElement) {
  1937. this._getClosestElementsInfo(nativeElement).forEach(([currentElement, info]) => this._originChanged(currentElement, origin, info));
  1938. }
  1939. else {
  1940. this._setOrigin(origin);
  1941. // `focus` isn't available on the server
  1942. if (typeof nativeElement.focus === 'function') {
  1943. nativeElement.focus(options);
  1944. }
  1945. }
  1946. }
  1947. ngOnDestroy() {
  1948. this._elementInfo.forEach((_info, element) => this.stopMonitoring(element));
  1949. }
  1950. /** Access injected document if available or fallback to global document reference */
  1951. _getDocument() {
  1952. return this._document || document;
  1953. }
  1954. /** Use defaultView of injected document if available or fallback to global window reference */
  1955. _getWindow() {
  1956. const doc = this._getDocument();
  1957. return doc.defaultView || window;
  1958. }
  1959. _getFocusOrigin(focusEventTarget) {
  1960. if (this._origin) {
  1961. // If the origin was realized via a touch interaction, we need to perform additional checks
  1962. // to determine whether the focus origin should be attributed to touch or program.
  1963. if (this._originFromTouchInteraction) {
  1964. return this._shouldBeAttributedToTouch(focusEventTarget) ? 'touch' : 'program';
  1965. }
  1966. else {
  1967. return this._origin;
  1968. }
  1969. }
  1970. // If the window has just regained focus, we can restore the most recent origin from before the
  1971. // window blurred. Otherwise, we've reached the point where we can't identify the source of the
  1972. // focus. This typically means one of two things happened:
  1973. //
  1974. // 1) The element was programmatically focused, or
  1975. // 2) The element was focused via screen reader navigation (which generally doesn't fire
  1976. // events).
  1977. //
  1978. // Because we can't distinguish between these two cases, we default to setting `program`.
  1979. if (this._windowFocused && this._lastFocusOrigin) {
  1980. return this._lastFocusOrigin;
  1981. }
  1982. // If the interaction is coming from an input label, we consider it a mouse interactions.
  1983. // This is a special case where focus moves on `click`, rather than `mousedown` which breaks
  1984. // our detection, because all our assumptions are for `mousedown`. We need to handle this
  1985. // special case, because it's very common for checkboxes and radio buttons.
  1986. if (focusEventTarget && this._isLastInteractionFromInputLabel(focusEventTarget)) {
  1987. return 'mouse';
  1988. }
  1989. return 'program';
  1990. }
  1991. /**
  1992. * Returns whether the focus event should be attributed to touch. Recall that in IMMEDIATE mode, a
  1993. * touch origin isn't immediately reset at the next tick (see _setOrigin). This means that when we
  1994. * handle a focus event following a touch interaction, we need to determine whether (1) the focus
  1995. * event was directly caused by the touch interaction or (2) the focus event was caused by a
  1996. * subsequent programmatic focus call triggered by the touch interaction.
  1997. * @param focusEventTarget The target of the focus event under examination.
  1998. */
  1999. _shouldBeAttributedToTouch(focusEventTarget) {
  2000. // Please note that this check is not perfect. Consider the following edge case:
  2001. //
  2002. // <div #parent tabindex="0">
  2003. // <div #child tabindex="0" (click)="#parent.focus()"></div>
  2004. // </div>
  2005. //
  2006. // Suppose there is a FocusMonitor in IMMEDIATE mode attached to #parent. When the user touches
  2007. // #child, #parent is programmatically focused. This code will attribute the focus to touch
  2008. // instead of program. This is a relatively minor edge-case that can be worked around by using
  2009. // focusVia(parent, 'program') to focus #parent.
  2010. return (this._detectionMode === 1 /* FocusMonitorDetectionMode.EVENTUAL */ ||
  2011. !!focusEventTarget?.contains(this._inputModalityDetector._mostRecentTarget));
  2012. }
  2013. /**
  2014. * Sets the focus classes on the element based on the given focus origin.
  2015. * @param element The element to update the classes on.
  2016. * @param origin The focus origin.
  2017. */
  2018. _setClasses(element, origin) {
  2019. element.classList.toggle('cdk-focused', !!origin);
  2020. element.classList.toggle('cdk-touch-focused', origin === 'touch');
  2021. element.classList.toggle('cdk-keyboard-focused', origin === 'keyboard');
  2022. element.classList.toggle('cdk-mouse-focused', origin === 'mouse');
  2023. element.classList.toggle('cdk-program-focused', origin === 'program');
  2024. }
  2025. /**
  2026. * Updates the focus origin. If we're using immediate detection mode, we schedule an async
  2027. * function to clear the origin at the end of a timeout. The duration of the timeout depends on
  2028. * the origin being set.
  2029. * @param origin The origin to set.
  2030. * @param isFromInteraction Whether we are setting the origin from an interaction event.
  2031. */
  2032. _setOrigin(origin, isFromInteraction = false) {
  2033. this._ngZone.runOutsideAngular(() => {
  2034. this._origin = origin;
  2035. this._originFromTouchInteraction = origin === 'touch' && isFromInteraction;
  2036. // If we're in IMMEDIATE mode, reset the origin at the next tick (or in `TOUCH_BUFFER_MS` ms
  2037. // for a touch event). We reset the origin at the next tick because Firefox focuses one tick
  2038. // after the interaction event. We wait `TOUCH_BUFFER_MS` ms before resetting the origin for
  2039. // a touch event because when a touch event is fired, the associated focus event isn't yet in
  2040. // the event queue. Before doing so, clear any pending timeouts.
  2041. if (this._detectionMode === 0 /* FocusMonitorDetectionMode.IMMEDIATE */) {
  2042. clearTimeout(this._originTimeoutId);
  2043. const ms = this._originFromTouchInteraction ? TOUCH_BUFFER_MS : 1;
  2044. this._originTimeoutId = setTimeout(() => (this._origin = null), ms);
  2045. }
  2046. });
  2047. }
  2048. /**
  2049. * Handles focus events on a registered element.
  2050. * @param event The focus event.
  2051. * @param element The monitored element.
  2052. */
  2053. _onFocus(event, element) {
  2054. // NOTE(mmalerba): We currently set the classes based on the focus origin of the most recent
  2055. // focus event affecting the monitored element. If we want to use the origin of the first event
  2056. // instead we should check for the cdk-focused class here and return if the element already has
  2057. // it. (This only matters for elements that have includesChildren = true).
  2058. // If we are not counting child-element-focus as focused, make sure that the event target is the
  2059. // monitored element itself.
  2060. const elementInfo = this._elementInfo.get(element);
  2061. const focusEventTarget = _getEventTarget(event);
  2062. if (!elementInfo || (!elementInfo.checkChildren && element !== focusEventTarget)) {
  2063. return;
  2064. }
  2065. this._originChanged(element, this._getFocusOrigin(focusEventTarget), elementInfo);
  2066. }
  2067. /**
  2068. * Handles blur events on a registered element.
  2069. * @param event The blur event.
  2070. * @param element The monitored element.
  2071. */
  2072. _onBlur(event, element) {
  2073. // If we are counting child-element-focus as focused, make sure that we aren't just blurring in
  2074. // order to focus another child of the monitored element.
  2075. const elementInfo = this._elementInfo.get(element);
  2076. if (!elementInfo ||
  2077. (elementInfo.checkChildren &&
  2078. event.relatedTarget instanceof Node &&
  2079. element.contains(event.relatedTarget))) {
  2080. return;
  2081. }
  2082. this._setClasses(element);
  2083. this._emitOrigin(elementInfo, null);
  2084. }
  2085. _emitOrigin(info, origin) {
  2086. if (info.subject.observers.length) {
  2087. this._ngZone.run(() => info.subject.next(origin));
  2088. }
  2089. }
  2090. _registerGlobalListeners(elementInfo) {
  2091. if (!this._platform.isBrowser) {
  2092. return;
  2093. }
  2094. const rootNode = elementInfo.rootNode;
  2095. const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode) || 0;
  2096. if (!rootNodeFocusListeners) {
  2097. this._ngZone.runOutsideAngular(() => {
  2098. rootNode.addEventListener('focus', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
  2099. rootNode.addEventListener('blur', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
  2100. });
  2101. }
  2102. this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners + 1);
  2103. // Register global listeners when first element is monitored.
  2104. if (++this._monitoredElementCount === 1) {
  2105. // Note: we listen to events in the capture phase so we
  2106. // can detect them even if the user stops propagation.
  2107. this._ngZone.runOutsideAngular(() => {
  2108. const window = this._getWindow();
  2109. window.addEventListener('focus', this._windowFocusListener);
  2110. });
  2111. // The InputModalityDetector is also just a collection of global listeners.
  2112. this._inputModalityDetector.modalityDetected
  2113. .pipe(takeUntil(this._stopInputModalityDetector))
  2114. .subscribe(modality => {
  2115. this._setOrigin(modality, true /* isFromInteraction */);
  2116. });
  2117. }
  2118. }
  2119. _removeGlobalListeners(elementInfo) {
  2120. const rootNode = elementInfo.rootNode;
  2121. if (this._rootNodeFocusListenerCount.has(rootNode)) {
  2122. const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode);
  2123. if (rootNodeFocusListeners > 1) {
  2124. this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners - 1);
  2125. }
  2126. else {
  2127. rootNode.removeEventListener('focus', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
  2128. rootNode.removeEventListener('blur', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
  2129. this._rootNodeFocusListenerCount.delete(rootNode);
  2130. }
  2131. }
  2132. // Unregister global listeners when last element is unmonitored.
  2133. if (!--this._monitoredElementCount) {
  2134. const window = this._getWindow();
  2135. window.removeEventListener('focus', this._windowFocusListener);
  2136. // Equivalently, stop our InputModalityDetector subscription.
  2137. this._stopInputModalityDetector.next();
  2138. // Clear timeouts for all potentially pending timeouts to prevent the leaks.
  2139. clearTimeout(this._windowFocusTimeoutId);
  2140. clearTimeout(this._originTimeoutId);
  2141. }
  2142. }
  2143. /** Updates all the state on an element once its focus origin has changed. */
  2144. _originChanged(element, origin, elementInfo) {
  2145. this._setClasses(element, origin);
  2146. this._emitOrigin(elementInfo, origin);
  2147. this._lastFocusOrigin = origin;
  2148. }
  2149. /**
  2150. * Collects the `MonitoredElementInfo` of a particular element and
  2151. * all of its ancestors that have enabled `checkChildren`.
  2152. * @param element Element from which to start the search.
  2153. */
  2154. _getClosestElementsInfo(element) {
  2155. const results = [];
  2156. this._elementInfo.forEach((info, currentElement) => {
  2157. if (currentElement === element || (info.checkChildren && currentElement.contains(element))) {
  2158. results.push([currentElement, info]);
  2159. }
  2160. });
  2161. return results;
  2162. }
  2163. /**
  2164. * Returns whether an interaction is likely to have come from the user clicking the `label` of
  2165. * an `input` or `textarea` in order to focus it.
  2166. * @param focusEventTarget Target currently receiving focus.
  2167. */
  2168. _isLastInteractionFromInputLabel(focusEventTarget) {
  2169. const { _mostRecentTarget: mostRecentTarget, mostRecentModality } = this._inputModalityDetector;
  2170. // If the last interaction used the mouse on an element contained by one of the labels
  2171. // of an `input`/`textarea` that is currently focused, it is very likely that the
  2172. // user redirected focus using the label.
  2173. if (mostRecentModality !== 'mouse' ||
  2174. !mostRecentTarget ||
  2175. mostRecentTarget === focusEventTarget ||
  2176. (focusEventTarget.nodeName !== 'INPUT' && focusEventTarget.nodeName !== 'TEXTAREA') ||
  2177. focusEventTarget.disabled) {
  2178. return false;
  2179. }
  2180. const labels = focusEventTarget.labels;
  2181. if (labels) {
  2182. for (let i = 0; i < labels.length; i++) {
  2183. if (labels[i].contains(mostRecentTarget)) {
  2184. return true;
  2185. }
  2186. }
  2187. }
  2188. return false;
  2189. }
  2190. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: FocusMonitor, deps: [{ token: i0.NgZone }, { token: i1.Platform }, { token: InputModalityDetector }, { token: DOCUMENT, optional: true }, { token: FOCUS_MONITOR_DEFAULT_OPTIONS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
  2191. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: FocusMonitor, providedIn: 'root' }); }
  2192. }
  2193. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: FocusMonitor, decorators: [{
  2194. type: Injectable,
  2195. args: [{ providedIn: 'root' }]
  2196. }], ctorParameters: function () { return [{ type: i0.NgZone }, { type: i1.Platform }, { type: InputModalityDetector }, { type: undefined, decorators: [{
  2197. type: Optional
  2198. }, {
  2199. type: Inject,
  2200. args: [DOCUMENT]
  2201. }] }, { type: undefined, decorators: [{
  2202. type: Optional
  2203. }, {
  2204. type: Inject,
  2205. args: [FOCUS_MONITOR_DEFAULT_OPTIONS]
  2206. }] }]; } });
  2207. /**
  2208. * Directive that determines how a particular element was focused (via keyboard, mouse, touch, or
  2209. * programmatically) and adds corresponding classes to the element.
  2210. *
  2211. * There are two variants of this directive:
  2212. * 1) cdkMonitorElementFocus: does not consider an element to be focused if one of its children is
  2213. * focused.
  2214. * 2) cdkMonitorSubtreeFocus: considers an element focused if it or any of its children are focused.
  2215. */
  2216. class CdkMonitorFocus {
  2217. constructor(_elementRef, _focusMonitor) {
  2218. this._elementRef = _elementRef;
  2219. this._focusMonitor = _focusMonitor;
  2220. this._focusOrigin = null;
  2221. this.cdkFocusChange = new EventEmitter();
  2222. }
  2223. get focusOrigin() {
  2224. return this._focusOrigin;
  2225. }
  2226. ngAfterViewInit() {
  2227. const element = this._elementRef.nativeElement;
  2228. this._monitorSubscription = this._focusMonitor
  2229. .monitor(element, element.nodeType === 1 && element.hasAttribute('cdkMonitorSubtreeFocus'))
  2230. .subscribe(origin => {
  2231. this._focusOrigin = origin;
  2232. this.cdkFocusChange.emit(origin);
  2233. });
  2234. }
  2235. ngOnDestroy() {
  2236. this._focusMonitor.stopMonitoring(this._elementRef);
  2237. if (this._monitorSubscription) {
  2238. this._monitorSubscription.unsubscribe();
  2239. }
  2240. }
  2241. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMonitorFocus, deps: [{ token: i0.ElementRef }, { token: FocusMonitor }], target: i0.ɵɵFactoryTarget.Directive }); }
  2242. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMonitorFocus, selector: "[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]", outputs: { cdkFocusChange: "cdkFocusChange" }, exportAs: ["cdkMonitorFocus"], ngImport: i0 }); }
  2243. }
  2244. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMonitorFocus, decorators: [{
  2245. type: Directive,
  2246. args: [{
  2247. selector: '[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]',
  2248. exportAs: 'cdkMonitorFocus',
  2249. }]
  2250. }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: FocusMonitor }]; }, propDecorators: { cdkFocusChange: [{
  2251. type: Output
  2252. }] } });
  2253. /** CSS class applied to the document body when in black-on-white high-contrast mode. */
  2254. const BLACK_ON_WHITE_CSS_CLASS = 'cdk-high-contrast-black-on-white';
  2255. /** CSS class applied to the document body when in white-on-black high-contrast mode. */
  2256. const WHITE_ON_BLACK_CSS_CLASS = 'cdk-high-contrast-white-on-black';
  2257. /** CSS class applied to the document body when in high-contrast mode. */
  2258. const HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS = 'cdk-high-contrast-active';
  2259. /**
  2260. * Service to determine whether the browser is currently in a high-contrast-mode environment.
  2261. *
  2262. * Microsoft Windows supports an accessibility feature called "High Contrast Mode". This mode
  2263. * changes the appearance of all applications, including web applications, to dramatically increase
  2264. * contrast.
  2265. *
  2266. * IE, Edge, and Firefox currently support this mode. Chrome does not support Windows High Contrast
  2267. * Mode. This service does not detect high-contrast mode as added by the Chrome "High Contrast"
  2268. * browser extension.
  2269. */
  2270. class HighContrastModeDetector {
  2271. constructor(_platform, document) {
  2272. this._platform = _platform;
  2273. this._document = document;
  2274. this._breakpointSubscription = inject(BreakpointObserver)
  2275. .observe('(forced-colors: active)')
  2276. .subscribe(() => {
  2277. if (this._hasCheckedHighContrastMode) {
  2278. this._hasCheckedHighContrastMode = false;
  2279. this._applyBodyHighContrastModeCssClasses();
  2280. }
  2281. });
  2282. }
  2283. /** Gets the current high-contrast-mode for the page. */
  2284. getHighContrastMode() {
  2285. if (!this._platform.isBrowser) {
  2286. return 0 /* HighContrastMode.NONE */;
  2287. }
  2288. // Create a test element with an arbitrary background-color that is neither black nor
  2289. // white; high-contrast mode will coerce the color to either black or white. Also ensure that
  2290. // appending the test element to the DOM does not affect layout by absolutely positioning it
  2291. const testElement = this._document.createElement('div');
  2292. testElement.style.backgroundColor = 'rgb(1,2,3)';
  2293. testElement.style.position = 'absolute';
  2294. this._document.body.appendChild(testElement);
  2295. // Get the computed style for the background color, collapsing spaces to normalize between
  2296. // browsers. Once we get this color, we no longer need the test element. Access the `window`
  2297. // via the document so we can fake it in tests. Note that we have extra null checks, because
  2298. // this logic will likely run during app bootstrap and throwing can break the entire app.
  2299. const documentWindow = this._document.defaultView || window;
  2300. const computedStyle = documentWindow && documentWindow.getComputedStyle
  2301. ? documentWindow.getComputedStyle(testElement)
  2302. : null;
  2303. const computedColor = ((computedStyle && computedStyle.backgroundColor) || '').replace(/ /g, '');
  2304. testElement.remove();
  2305. switch (computedColor) {
  2306. // Pre Windows 11 dark theme.
  2307. case 'rgb(0,0,0)':
  2308. // Windows 11 dark themes.
  2309. case 'rgb(45,50,54)':
  2310. case 'rgb(32,32,32)':
  2311. return 2 /* HighContrastMode.WHITE_ON_BLACK */;
  2312. // Pre Windows 11 light theme.
  2313. case 'rgb(255,255,255)':
  2314. // Windows 11 light theme.
  2315. case 'rgb(255,250,239)':
  2316. return 1 /* HighContrastMode.BLACK_ON_WHITE */;
  2317. }
  2318. return 0 /* HighContrastMode.NONE */;
  2319. }
  2320. ngOnDestroy() {
  2321. this._breakpointSubscription.unsubscribe();
  2322. }
  2323. /** Applies CSS classes indicating high-contrast mode to document body (browser-only). */
  2324. _applyBodyHighContrastModeCssClasses() {
  2325. if (!this._hasCheckedHighContrastMode && this._platform.isBrowser && this._document.body) {
  2326. const bodyClasses = this._document.body.classList;
  2327. bodyClasses.remove(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, BLACK_ON_WHITE_CSS_CLASS, WHITE_ON_BLACK_CSS_CLASS);
  2328. this._hasCheckedHighContrastMode = true;
  2329. const mode = this.getHighContrastMode();
  2330. if (mode === 1 /* HighContrastMode.BLACK_ON_WHITE */) {
  2331. bodyClasses.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, BLACK_ON_WHITE_CSS_CLASS);
  2332. }
  2333. else if (mode === 2 /* HighContrastMode.WHITE_ON_BLACK */) {
  2334. bodyClasses.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, WHITE_ON_BLACK_CSS_CLASS);
  2335. }
  2336. }
  2337. }
  2338. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: HighContrastModeDetector, deps: [{ token: i1.Platform }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); }
  2339. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: HighContrastModeDetector, providedIn: 'root' }); }
  2340. }
  2341. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: HighContrastModeDetector, decorators: [{
  2342. type: Injectable,
  2343. args: [{ providedIn: 'root' }]
  2344. }], ctorParameters: function () { return [{ type: i1.Platform }, { type: undefined, decorators: [{
  2345. type: Inject,
  2346. args: [DOCUMENT]
  2347. }] }]; } });
  2348. class A11yModule {
  2349. constructor(highContrastModeDetector) {
  2350. highContrastModeDetector._applyBodyHighContrastModeCssClasses();
  2351. }
  2352. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: A11yModule, deps: [{ token: HighContrastModeDetector }], target: i0.ɵɵFactoryTarget.NgModule }); }
  2353. static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.0.0", ngImport: i0, type: A11yModule, declarations: [CdkAriaLive, CdkTrapFocus, CdkMonitorFocus], imports: [ObserversModule], exports: [CdkAriaLive, CdkTrapFocus, CdkMonitorFocus] }); }
  2354. static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: A11yModule, imports: [ObserversModule] }); }
  2355. }
  2356. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: A11yModule, decorators: [{
  2357. type: NgModule,
  2358. args: [{
  2359. imports: [ObserversModule],
  2360. declarations: [CdkAriaLive, CdkTrapFocus, CdkMonitorFocus],
  2361. exports: [CdkAriaLive, CdkTrapFocus, CdkMonitorFocus],
  2362. }]
  2363. }], ctorParameters: function () { return [{ type: HighContrastModeDetector }]; } });
  2364. /**
  2365. * Generated bundle index. Do not edit.
  2366. */
  2367. export { A11yModule, ActiveDescendantKeyManager, AriaDescriber, CDK_DESCRIBEDBY_HOST_ATTRIBUTE, CDK_DESCRIBEDBY_ID_PREFIX, CdkAriaLive, CdkMonitorFocus, CdkTrapFocus, ConfigurableFocusTrap, ConfigurableFocusTrapFactory, EventListenerFocusTrapInertStrategy, FOCUS_MONITOR_DEFAULT_OPTIONS, FOCUS_TRAP_INERT_STRATEGY, FocusKeyManager, FocusMonitor, FocusTrap, FocusTrapFactory, HighContrastModeDetector, INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS, INPUT_MODALITY_DETECTOR_OPTIONS, InputModalityDetector, InteractivityChecker, IsFocusableConfig, LIVE_ANNOUNCER_DEFAULT_OPTIONS, LIVE_ANNOUNCER_ELEMENT_TOKEN, LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY, ListKeyManager, LiveAnnouncer, MESSAGES_CONTAINER_ID, isFakeMousedownFromScreenReader, isFakeTouchstartFromScreenReader };
  2368. //# sourceMappingURL=a11y.mjs.map