a11y-module-BYox5gpI.mjs 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. import * as i0 from '@angular/core';
  2. import { inject, Injectable, afterNextRender, NgZone, Injector, ElementRef, booleanAttribute, Directive, Input, InjectionToken, NgModule } from '@angular/core';
  3. import { C as CdkMonitorFocus } from './focus-monitor-e2l_RpN3.mjs';
  4. import { DOCUMENT } from '@angular/common';
  5. import { P as Platform } from './platform-DmdVEw_C.mjs';
  6. import { c as _getFocusedElementPierceShadowDom } from './shadow-dom-B0oHn41l.mjs';
  7. import { _ as _CdkPrivateStyleLoader } from './style-loader-Cu9AvjH9.mjs';
  8. import { _VisuallyHiddenLoader } from './private.mjs';
  9. import { B as BreakpointObserver } from './breakpoints-observer-CljOfYGy.mjs';
  10. import { ContentObserver, ObserversModule } from './observers.mjs';
  11. /**
  12. * Configuration for the isFocusable method.
  13. */
  14. class IsFocusableConfig {
  15. /**
  16. * Whether to count an element as focusable even if it is not currently visible.
  17. */
  18. ignoreVisibility = false;
  19. }
  20. // The InteractivityChecker leans heavily on the ally.js accessibility utilities.
  21. // Methods like `isTabbable` are only covering specific edge-cases for the browsers which are
  22. // supported.
  23. /**
  24. * Utility for checking the interactivity of an element, such as whether it is focusable or
  25. * tabbable.
  26. */
  27. class InteractivityChecker {
  28. _platform = inject(Platform);
  29. constructor() { }
  30. /**
  31. * Gets whether an element is disabled.
  32. *
  33. * @param element Element to be checked.
  34. * @returns Whether the element is disabled.
  35. */
  36. isDisabled(element) {
  37. // This does not capture some cases, such as a non-form control with a disabled attribute or
  38. // a form control inside of a disabled form, but should capture the most common cases.
  39. return element.hasAttribute('disabled');
  40. }
  41. /**
  42. * Gets whether an element is visible for the purposes of interactivity.
  43. *
  44. * This will capture states like `display: none` and `visibility: hidden`, but not things like
  45. * being clipped by an `overflow: hidden` parent or being outside the viewport.
  46. *
  47. * @returns Whether the element is visible.
  48. */
  49. isVisible(element) {
  50. return hasGeometry(element) && getComputedStyle(element).visibility === 'visible';
  51. }
  52. /**
  53. * Gets whether an element can be reached via Tab key.
  54. * Assumes that the element has already been checked with isFocusable.
  55. *
  56. * @param element Element to be checked.
  57. * @returns Whether the element is tabbable.
  58. */
  59. isTabbable(element) {
  60. // Nothing is tabbable on the server 😎
  61. if (!this._platform.isBrowser) {
  62. return false;
  63. }
  64. const frameElement = getFrameElement(getWindow(element));
  65. if (frameElement) {
  66. // Frame elements inherit their tabindex onto all child elements.
  67. if (getTabIndexValue(frameElement) === -1) {
  68. return false;
  69. }
  70. // Browsers disable tabbing to an element inside of an invisible frame.
  71. if (!this.isVisible(frameElement)) {
  72. return false;
  73. }
  74. }
  75. let nodeName = element.nodeName.toLowerCase();
  76. let tabIndexValue = getTabIndexValue(element);
  77. if (element.hasAttribute('contenteditable')) {
  78. return tabIndexValue !== -1;
  79. }
  80. if (nodeName === 'iframe' || nodeName === 'object') {
  81. // The frame or object's content may be tabbable depending on the content, but it's
  82. // not possibly to reliably detect the content of the frames. We always consider such
  83. // elements as non-tabbable.
  84. return false;
  85. }
  86. // In iOS, the browser only considers some specific elements as tabbable.
  87. if (this._platform.WEBKIT && this._platform.IOS && !isPotentiallyTabbableIOS(element)) {
  88. return false;
  89. }
  90. if (nodeName === 'audio') {
  91. // Audio elements without controls enabled are never tabbable, regardless
  92. // of the tabindex attribute explicitly being set.
  93. if (!element.hasAttribute('controls')) {
  94. return false;
  95. }
  96. // Audio elements with controls are by default tabbable unless the
  97. // tabindex attribute is set to `-1` explicitly.
  98. return tabIndexValue !== -1;
  99. }
  100. if (nodeName === 'video') {
  101. // For all video elements, if the tabindex attribute is set to `-1`, the video
  102. // is not tabbable. Note: We cannot rely on the default `HTMLElement.tabIndex`
  103. // property as that one is set to `-1` in Chrome, Edge and Safari v13.1. The
  104. // tabindex attribute is the source of truth here.
  105. if (tabIndexValue === -1) {
  106. return false;
  107. }
  108. // If the tabindex is explicitly set, and not `-1` (as per check before), the
  109. // video element is always tabbable (regardless of whether it has controls or not).
  110. if (tabIndexValue !== null) {
  111. return true;
  112. }
  113. // Otherwise (when no explicit tabindex is set), a video is only tabbable if it
  114. // has controls enabled. Firefox is special as videos are always tabbable regardless
  115. // of whether there are controls or not.
  116. return this._platform.FIREFOX || element.hasAttribute('controls');
  117. }
  118. return element.tabIndex >= 0;
  119. }
  120. /**
  121. * Gets whether an element can be focused by the user.
  122. *
  123. * @param element Element to be checked.
  124. * @param config The config object with options to customize this method's behavior
  125. * @returns Whether the element is focusable.
  126. */
  127. isFocusable(element, config) {
  128. // Perform checks in order of left to most expensive.
  129. // Again, naive approach that does not capture many edge cases and browser quirks.
  130. return (isPotentiallyFocusable(element) &&
  131. !this.isDisabled(element) &&
  132. (config?.ignoreVisibility || this.isVisible(element)));
  133. }
  134. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: InteractivityChecker, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
  135. static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: InteractivityChecker, providedIn: 'root' });
  136. }
  137. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: InteractivityChecker, decorators: [{
  138. type: Injectable,
  139. args: [{ providedIn: 'root' }]
  140. }], ctorParameters: () => [] });
  141. /**
  142. * Returns the frame element from a window object. Since browsers like MS Edge throw errors if
  143. * the frameElement property is being accessed from a different host address, this property
  144. * should be accessed carefully.
  145. */
  146. function getFrameElement(window) {
  147. try {
  148. return window.frameElement;
  149. }
  150. catch {
  151. return null;
  152. }
  153. }
  154. /** Checks whether the specified element has any geometry / rectangles. */
  155. function hasGeometry(element) {
  156. // Use logic from jQuery to check for an invisible element.
  157. // See https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js#L12
  158. return !!(element.offsetWidth ||
  159. element.offsetHeight ||
  160. (typeof element.getClientRects === 'function' && element.getClientRects().length));
  161. }
  162. /** Gets whether an element's */
  163. function isNativeFormElement(element) {
  164. let nodeName = element.nodeName.toLowerCase();
  165. return (nodeName === 'input' ||
  166. nodeName === 'select' ||
  167. nodeName === 'button' ||
  168. nodeName === 'textarea');
  169. }
  170. /** Gets whether an element is an `<input type="hidden">`. */
  171. function isHiddenInput(element) {
  172. return isInputElement(element) && element.type == 'hidden';
  173. }
  174. /** Gets whether an element is an anchor that has an href attribute. */
  175. function isAnchorWithHref(element) {
  176. return isAnchorElement(element) && element.hasAttribute('href');
  177. }
  178. /** Gets whether an element is an input element. */
  179. function isInputElement(element) {
  180. return element.nodeName.toLowerCase() == 'input';
  181. }
  182. /** Gets whether an element is an anchor element. */
  183. function isAnchorElement(element) {
  184. return element.nodeName.toLowerCase() == 'a';
  185. }
  186. /** Gets whether an element has a valid tabindex. */
  187. function hasValidTabIndex(element) {
  188. if (!element.hasAttribute('tabindex') || element.tabIndex === undefined) {
  189. return false;
  190. }
  191. let tabIndex = element.getAttribute('tabindex');
  192. return !!(tabIndex && !isNaN(parseInt(tabIndex, 10)));
  193. }
  194. /**
  195. * Returns the parsed tabindex from the element attributes instead of returning the
  196. * evaluated tabindex from the browsers defaults.
  197. */
  198. function getTabIndexValue(element) {
  199. if (!hasValidTabIndex(element)) {
  200. return null;
  201. }
  202. // See browser issue in Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054
  203. const tabIndex = parseInt(element.getAttribute('tabindex') || '', 10);
  204. return isNaN(tabIndex) ? -1 : tabIndex;
  205. }
  206. /** Checks whether the specified element is potentially tabbable on iOS */
  207. function isPotentiallyTabbableIOS(element) {
  208. let nodeName = element.nodeName.toLowerCase();
  209. let inputType = nodeName === 'input' && element.type;
  210. return (inputType === 'text' ||
  211. inputType === 'password' ||
  212. nodeName === 'select' ||
  213. nodeName === 'textarea');
  214. }
  215. /**
  216. * Gets whether an element is potentially focusable without taking current visible/disabled state
  217. * into account.
  218. */
  219. function isPotentiallyFocusable(element) {
  220. // Inputs are potentially focusable *unless* they're type="hidden".
  221. if (isHiddenInput(element)) {
  222. return false;
  223. }
  224. return (isNativeFormElement(element) ||
  225. isAnchorWithHref(element) ||
  226. element.hasAttribute('contenteditable') ||
  227. hasValidTabIndex(element));
  228. }
  229. /** Gets the parent window of a DOM node with regards of being inside of an iframe. */
  230. function getWindow(node) {
  231. // ownerDocument is null if `node` itself *is* a document.
  232. return (node.ownerDocument && node.ownerDocument.defaultView) || window;
  233. }
  234. /**
  235. * Class that allows for trapping focus within a DOM element.
  236. *
  237. * This class currently uses a relatively simple approach to focus trapping.
  238. * It assumes that the tab order is the same as DOM order, which is not necessarily true.
  239. * Things like `tabIndex > 0`, flex `order`, and shadow roots can cause the two to be misaligned.
  240. */
  241. class FocusTrap {
  242. _element;
  243. _checker;
  244. _ngZone;
  245. _document;
  246. _injector;
  247. _startAnchor;
  248. _endAnchor;
  249. _hasAttached = false;
  250. // Event listeners for the anchors. Need to be regular functions so that we can unbind them later.
  251. startAnchorListener = () => this.focusLastTabbableElement();
  252. endAnchorListener = () => this.focusFirstTabbableElement();
  253. /** Whether the focus trap is active. */
  254. get enabled() {
  255. return this._enabled;
  256. }
  257. set enabled(value) {
  258. this._enabled = value;
  259. if (this._startAnchor && this._endAnchor) {
  260. this._toggleAnchorTabIndex(value, this._startAnchor);
  261. this._toggleAnchorTabIndex(value, this._endAnchor);
  262. }
  263. }
  264. _enabled = true;
  265. constructor(_element, _checker, _ngZone, _document, deferAnchors = false,
  266. /** @breaking-change 20.0.0 param to become required */
  267. _injector) {
  268. this._element = _element;
  269. this._checker = _checker;
  270. this._ngZone = _ngZone;
  271. this._document = _document;
  272. this._injector = _injector;
  273. if (!deferAnchors) {
  274. this.attachAnchors();
  275. }
  276. }
  277. /** Destroys the focus trap by cleaning up the anchors. */
  278. destroy() {
  279. const startAnchor = this._startAnchor;
  280. const endAnchor = this._endAnchor;
  281. if (startAnchor) {
  282. startAnchor.removeEventListener('focus', this.startAnchorListener);
  283. startAnchor.remove();
  284. }
  285. if (endAnchor) {
  286. endAnchor.removeEventListener('focus', this.endAnchorListener);
  287. endAnchor.remove();
  288. }
  289. this._startAnchor = this._endAnchor = null;
  290. this._hasAttached = false;
  291. }
  292. /**
  293. * Inserts the anchors into the DOM. This is usually done automatically
  294. * in the constructor, but can be deferred for cases like directives with `*ngIf`.
  295. * @returns Whether the focus trap managed to attach successfully. This may not be the case
  296. * if the target element isn't currently in the DOM.
  297. */
  298. attachAnchors() {
  299. // If we're not on the browser, there can be no focus to trap.
  300. if (this._hasAttached) {
  301. return true;
  302. }
  303. this._ngZone.runOutsideAngular(() => {
  304. if (!this._startAnchor) {
  305. this._startAnchor = this._createAnchor();
  306. this._startAnchor.addEventListener('focus', this.startAnchorListener);
  307. }
  308. if (!this._endAnchor) {
  309. this._endAnchor = this._createAnchor();
  310. this._endAnchor.addEventListener('focus', this.endAnchorListener);
  311. }
  312. });
  313. if (this._element.parentNode) {
  314. this._element.parentNode.insertBefore(this._startAnchor, this._element);
  315. this._element.parentNode.insertBefore(this._endAnchor, this._element.nextSibling);
  316. this._hasAttached = true;
  317. }
  318. return this._hasAttached;
  319. }
  320. /**
  321. * Waits for the zone to stabilize, then focuses the first tabbable element.
  322. * @returns Returns a promise that resolves with a boolean, depending
  323. * on whether focus was moved successfully.
  324. */
  325. focusInitialElementWhenReady(options) {
  326. return new Promise(resolve => {
  327. this._executeOnStable(() => resolve(this.focusInitialElement(options)));
  328. });
  329. }
  330. /**
  331. * Waits for the zone to stabilize, then focuses
  332. * the first tabbable element within the focus trap region.
  333. * @returns Returns a promise that resolves with a boolean, depending
  334. * on whether focus was moved successfully.
  335. */
  336. focusFirstTabbableElementWhenReady(options) {
  337. return new Promise(resolve => {
  338. this._executeOnStable(() => resolve(this.focusFirstTabbableElement(options)));
  339. });
  340. }
  341. /**
  342. * Waits for the zone to stabilize, then focuses
  343. * the last tabbable element within the focus trap region.
  344. * @returns Returns a promise that resolves with a boolean, depending
  345. * on whether focus was moved successfully.
  346. */
  347. focusLastTabbableElementWhenReady(options) {
  348. return new Promise(resolve => {
  349. this._executeOnStable(() => resolve(this.focusLastTabbableElement(options)));
  350. });
  351. }
  352. /**
  353. * Get the specified boundary element of the trapped region.
  354. * @param bound The boundary to get (start or end of trapped region).
  355. * @returns The boundary element.
  356. */
  357. _getRegionBoundary(bound) {
  358. // Contains the deprecated version of selector, for temporary backwards comparability.
  359. const markers = this._element.querySelectorAll(`[cdk-focus-region-${bound}], ` + `[cdkFocusRegion${bound}], ` + `[cdk-focus-${bound}]`);
  360. if (typeof ngDevMode === 'undefined' || ngDevMode) {
  361. for (let i = 0; i < markers.length; i++) {
  362. // @breaking-change 8.0.0
  363. if (markers[i].hasAttribute(`cdk-focus-${bound}`)) {
  364. console.warn(`Found use of deprecated attribute 'cdk-focus-${bound}', ` +
  365. `use 'cdkFocusRegion${bound}' instead. The deprecated ` +
  366. `attribute will be removed in 8.0.0.`, markers[i]);
  367. }
  368. else if (markers[i].hasAttribute(`cdk-focus-region-${bound}`)) {
  369. console.warn(`Found use of deprecated attribute 'cdk-focus-region-${bound}', ` +
  370. `use 'cdkFocusRegion${bound}' instead. The deprecated attribute ` +
  371. `will be removed in 8.0.0.`, markers[i]);
  372. }
  373. }
  374. }
  375. if (bound == 'start') {
  376. return markers.length ? markers[0] : this._getFirstTabbableElement(this._element);
  377. }
  378. return markers.length
  379. ? markers[markers.length - 1]
  380. : this._getLastTabbableElement(this._element);
  381. }
  382. /**
  383. * Focuses the element that should be focused when the focus trap is initialized.
  384. * @returns Whether focus was moved successfully.
  385. */
  386. focusInitialElement(options) {
  387. // Contains the deprecated version of selector, for temporary backwards comparability.
  388. const redirectToElement = this._element.querySelector(`[cdk-focus-initial], ` + `[cdkFocusInitial]`);
  389. if (redirectToElement) {
  390. // @breaking-change 8.0.0
  391. if ((typeof ngDevMode === 'undefined' || ngDevMode) &&
  392. redirectToElement.hasAttribute(`cdk-focus-initial`)) {
  393. console.warn(`Found use of deprecated attribute 'cdk-focus-initial', ` +
  394. `use 'cdkFocusInitial' instead. The deprecated attribute ` +
  395. `will be removed in 8.0.0`, redirectToElement);
  396. }
  397. // Warn the consumer if the element they've pointed to
  398. // isn't focusable, when not in production mode.
  399. if ((typeof ngDevMode === 'undefined' || ngDevMode) &&
  400. !this._checker.isFocusable(redirectToElement)) {
  401. console.warn(`Element matching '[cdkFocusInitial]' is not focusable.`, redirectToElement);
  402. }
  403. if (!this._checker.isFocusable(redirectToElement)) {
  404. const focusableChild = this._getFirstTabbableElement(redirectToElement);
  405. focusableChild?.focus(options);
  406. return !!focusableChild;
  407. }
  408. redirectToElement.focus(options);
  409. return true;
  410. }
  411. return this.focusFirstTabbableElement(options);
  412. }
  413. /**
  414. * Focuses the first tabbable element within the focus trap region.
  415. * @returns Whether focus was moved successfully.
  416. */
  417. focusFirstTabbableElement(options) {
  418. const redirectToElement = this._getRegionBoundary('start');
  419. if (redirectToElement) {
  420. redirectToElement.focus(options);
  421. }
  422. return !!redirectToElement;
  423. }
  424. /**
  425. * Focuses the last tabbable element within the focus trap region.
  426. * @returns Whether focus was moved successfully.
  427. */
  428. focusLastTabbableElement(options) {
  429. const redirectToElement = this._getRegionBoundary('end');
  430. if (redirectToElement) {
  431. redirectToElement.focus(options);
  432. }
  433. return !!redirectToElement;
  434. }
  435. /**
  436. * Checks whether the focus trap has successfully been attached.
  437. */
  438. hasAttached() {
  439. return this._hasAttached;
  440. }
  441. /** Get the first tabbable element from a DOM subtree (inclusive). */
  442. _getFirstTabbableElement(root) {
  443. if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
  444. return root;
  445. }
  446. const children = root.children;
  447. for (let i = 0; i < children.length; i++) {
  448. const tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE
  449. ? this._getFirstTabbableElement(children[i])
  450. : null;
  451. if (tabbableChild) {
  452. return tabbableChild;
  453. }
  454. }
  455. return null;
  456. }
  457. /** Get the last tabbable element from a DOM subtree (inclusive). */
  458. _getLastTabbableElement(root) {
  459. if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
  460. return root;
  461. }
  462. // Iterate in reverse DOM order.
  463. const children = root.children;
  464. for (let i = children.length - 1; i >= 0; i--) {
  465. const tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE
  466. ? this._getLastTabbableElement(children[i])
  467. : null;
  468. if (tabbableChild) {
  469. return tabbableChild;
  470. }
  471. }
  472. return null;
  473. }
  474. /** Creates an anchor element. */
  475. _createAnchor() {
  476. const anchor = this._document.createElement('div');
  477. this._toggleAnchorTabIndex(this._enabled, anchor);
  478. anchor.classList.add('cdk-visually-hidden');
  479. anchor.classList.add('cdk-focus-trap-anchor');
  480. anchor.setAttribute('aria-hidden', 'true');
  481. return anchor;
  482. }
  483. /**
  484. * Toggles the `tabindex` of an anchor, based on the enabled state of the focus trap.
  485. * @param isEnabled Whether the focus trap is enabled.
  486. * @param anchor Anchor on which to toggle the tabindex.
  487. */
  488. _toggleAnchorTabIndex(isEnabled, anchor) {
  489. // Remove the tabindex completely, rather than setting it to -1, because if the
  490. // element has a tabindex, the user might still hit it when navigating with the arrow keys.
  491. isEnabled ? anchor.setAttribute('tabindex', '0') : anchor.removeAttribute('tabindex');
  492. }
  493. /**
  494. * Toggles the`tabindex` of both anchors to either trap Tab focus or allow it to escape.
  495. * @param enabled: Whether the anchors should trap Tab.
  496. */
  497. toggleAnchors(enabled) {
  498. if (this._startAnchor && this._endAnchor) {
  499. this._toggleAnchorTabIndex(enabled, this._startAnchor);
  500. this._toggleAnchorTabIndex(enabled, this._endAnchor);
  501. }
  502. }
  503. /** Executes a function when the zone is stable. */
  504. _executeOnStable(fn) {
  505. // TODO: remove this conditional when injector is required in the constructor.
  506. if (this._injector) {
  507. afterNextRender(fn, { injector: this._injector });
  508. }
  509. else {
  510. setTimeout(fn);
  511. }
  512. }
  513. }
  514. /**
  515. * Factory that allows easy instantiation of focus traps.
  516. */
  517. class FocusTrapFactory {
  518. _checker = inject(InteractivityChecker);
  519. _ngZone = inject(NgZone);
  520. _document = inject(DOCUMENT);
  521. _injector = inject(Injector);
  522. constructor() {
  523. inject(_CdkPrivateStyleLoader).load(_VisuallyHiddenLoader);
  524. }
  525. /**
  526. * Creates a focus-trapped region around the given element.
  527. * @param element The element around which focus will be trapped.
  528. * @param deferCaptureElements Defers the creation of focus-capturing elements to be done
  529. * manually by the user.
  530. * @returns The created focus trap instance.
  531. */
  532. create(element, deferCaptureElements = false) {
  533. return new FocusTrap(element, this._checker, this._ngZone, this._document, deferCaptureElements, this._injector);
  534. }
  535. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: FocusTrapFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
  536. static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: FocusTrapFactory, providedIn: 'root' });
  537. }
  538. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: FocusTrapFactory, decorators: [{
  539. type: Injectable,
  540. args: [{ providedIn: 'root' }]
  541. }], ctorParameters: () => [] });
  542. /** Directive for trapping focus within a region. */
  543. class CdkTrapFocus {
  544. _elementRef = inject(ElementRef);
  545. _focusTrapFactory = inject(FocusTrapFactory);
  546. /** Underlying FocusTrap instance. */
  547. focusTrap;
  548. /** Previously focused element to restore focus to upon destroy when using autoCapture. */
  549. _previouslyFocusedElement = null;
  550. /** Whether the focus trap is active. */
  551. get enabled() {
  552. return this.focusTrap?.enabled || false;
  553. }
  554. set enabled(value) {
  555. if (this.focusTrap) {
  556. this.focusTrap.enabled = value;
  557. }
  558. }
  559. /**
  560. * Whether the directive should automatically move focus into the trapped region upon
  561. * initialization and return focus to the previous activeElement upon destruction.
  562. */
  563. autoCapture;
  564. constructor() {
  565. const platform = inject(Platform);
  566. if (platform.isBrowser) {
  567. this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
  568. }
  569. }
  570. ngOnDestroy() {
  571. this.focusTrap?.destroy();
  572. // If we stored a previously focused element when using autoCapture, return focus to that
  573. // element now that the trapped region is being destroyed.
  574. if (this._previouslyFocusedElement) {
  575. this._previouslyFocusedElement.focus();
  576. this._previouslyFocusedElement = null;
  577. }
  578. }
  579. ngAfterContentInit() {
  580. this.focusTrap?.attachAnchors();
  581. if (this.autoCapture) {
  582. this._captureFocus();
  583. }
  584. }
  585. ngDoCheck() {
  586. if (this.focusTrap && !this.focusTrap.hasAttached()) {
  587. this.focusTrap.attachAnchors();
  588. }
  589. }
  590. ngOnChanges(changes) {
  591. const autoCaptureChange = changes['autoCapture'];
  592. if (autoCaptureChange &&
  593. !autoCaptureChange.firstChange &&
  594. this.autoCapture &&
  595. this.focusTrap?.hasAttached()) {
  596. this._captureFocus();
  597. }
  598. }
  599. _captureFocus() {
  600. this._previouslyFocusedElement = _getFocusedElementPierceShadowDom();
  601. this.focusTrap?.focusInitialElementWhenReady();
  602. }
  603. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTrapFocus, deps: [], target: i0.ɵɵFactoryTarget.Directive });
  604. static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.6", type: CdkTrapFocus, isStandalone: true, selector: "[cdkTrapFocus]", inputs: { enabled: ["cdkTrapFocus", "enabled", booleanAttribute], autoCapture: ["cdkTrapFocusAutoCapture", "autoCapture", booleanAttribute] }, exportAs: ["cdkTrapFocus"], usesOnChanges: true, ngImport: i0 });
  605. }
  606. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTrapFocus, decorators: [{
  607. type: Directive,
  608. args: [{
  609. selector: '[cdkTrapFocus]',
  610. exportAs: 'cdkTrapFocus',
  611. }]
  612. }], ctorParameters: () => [], propDecorators: { enabled: [{
  613. type: Input,
  614. args: [{ alias: 'cdkTrapFocus', transform: booleanAttribute }]
  615. }], autoCapture: [{
  616. type: Input,
  617. args: [{ alias: 'cdkTrapFocusAutoCapture', transform: booleanAttribute }]
  618. }] } });
  619. const LIVE_ANNOUNCER_ELEMENT_TOKEN = new InjectionToken('liveAnnouncerElement', {
  620. providedIn: 'root',
  621. factory: LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY,
  622. });
  623. /**
  624. * @docs-private
  625. * @deprecated No longer used, will be removed.
  626. * @breaking-change 21.0.0
  627. */
  628. function LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY() {
  629. return null;
  630. }
  631. /** Injection token that can be used to configure the default options for the LiveAnnouncer. */
  632. const LIVE_ANNOUNCER_DEFAULT_OPTIONS = new InjectionToken('LIVE_ANNOUNCER_DEFAULT_OPTIONS');
  633. let uniqueIds = 0;
  634. class LiveAnnouncer {
  635. _ngZone = inject(NgZone);
  636. _defaultOptions = inject(LIVE_ANNOUNCER_DEFAULT_OPTIONS, {
  637. optional: true,
  638. });
  639. _liveElement;
  640. _document = inject(DOCUMENT);
  641. _previousTimeout;
  642. _currentPromise;
  643. _currentResolve;
  644. constructor() {
  645. const elementToken = inject(LIVE_ANNOUNCER_ELEMENT_TOKEN, { optional: true });
  646. this._liveElement = elementToken || this._createLiveElement();
  647. }
  648. announce(message, ...args) {
  649. const defaultOptions = this._defaultOptions;
  650. let politeness;
  651. let duration;
  652. if (args.length === 1 && typeof args[0] === 'number') {
  653. duration = args[0];
  654. }
  655. else {
  656. [politeness, duration] = args;
  657. }
  658. this.clear();
  659. clearTimeout(this._previousTimeout);
  660. if (!politeness) {
  661. politeness =
  662. defaultOptions && defaultOptions.politeness ? defaultOptions.politeness : 'polite';
  663. }
  664. if (duration == null && defaultOptions) {
  665. duration = defaultOptions.duration;
  666. }
  667. // TODO: ensure changing the politeness works on all environments we support.
  668. this._liveElement.setAttribute('aria-live', politeness);
  669. if (this._liveElement.id) {
  670. this._exposeAnnouncerToModals(this._liveElement.id);
  671. }
  672. // This 100ms timeout is necessary for some browser + screen-reader combinations:
  673. // - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout.
  674. // - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a
  675. // second time without clearing and then using a non-zero delay.
  676. // (using JAWS 17 at time of this writing).
  677. return this._ngZone.runOutsideAngular(() => {
  678. if (!this._currentPromise) {
  679. this._currentPromise = new Promise(resolve => (this._currentResolve = resolve));
  680. }
  681. clearTimeout(this._previousTimeout);
  682. this._previousTimeout = setTimeout(() => {
  683. this._liveElement.textContent = message;
  684. if (typeof duration === 'number') {
  685. this._previousTimeout = setTimeout(() => this.clear(), duration);
  686. }
  687. // For some reason in tests this can be undefined
  688. // Probably related to ZoneJS and every other thing that patches browser APIs in tests
  689. this._currentResolve?.();
  690. this._currentPromise = this._currentResolve = undefined;
  691. }, 100);
  692. return this._currentPromise;
  693. });
  694. }
  695. /**
  696. * Clears the current text from the announcer element. Can be used to prevent
  697. * screen readers from reading the text out again while the user is going
  698. * through the page landmarks.
  699. */
  700. clear() {
  701. if (this._liveElement) {
  702. this._liveElement.textContent = '';
  703. }
  704. }
  705. ngOnDestroy() {
  706. clearTimeout(this._previousTimeout);
  707. this._liveElement?.remove();
  708. this._liveElement = null;
  709. this._currentResolve?.();
  710. this._currentPromise = this._currentResolve = undefined;
  711. }
  712. _createLiveElement() {
  713. const elementClass = 'cdk-live-announcer-element';
  714. const previousElements = this._document.getElementsByClassName(elementClass);
  715. const liveEl = this._document.createElement('div');
  716. // Remove any old containers. This can happen when coming in from a server-side-rendered page.
  717. for (let i = 0; i < previousElements.length; i++) {
  718. previousElements[i].remove();
  719. }
  720. liveEl.classList.add(elementClass);
  721. liveEl.classList.add('cdk-visually-hidden');
  722. liveEl.setAttribute('aria-atomic', 'true');
  723. liveEl.setAttribute('aria-live', 'polite');
  724. liveEl.id = `cdk-live-announcer-${uniqueIds++}`;
  725. this._document.body.appendChild(liveEl);
  726. return liveEl;
  727. }
  728. /**
  729. * Some browsers won't expose the accessibility node of the live announcer element if there is an
  730. * `aria-modal` and the live announcer is outside of it. This method works around the issue by
  731. * pointing the `aria-owns` of all modals to the live announcer element.
  732. */
  733. _exposeAnnouncerToModals(id) {
  734. // TODO(http://github.com/angular/components/issues/26853): consider de-duplicating this with
  735. // the `SnakBarContainer` and other usages.
  736. //
  737. // Note that the selector here is limited to CDK overlays at the moment in order to reduce the
  738. // section of the DOM we need to look through. This should cover all the cases we support, but
  739. // the selector can be expanded if it turns out to be too narrow.
  740. const modals = this._document.querySelectorAll('body > .cdk-overlay-container [aria-modal="true"]');
  741. for (let i = 0; i < modals.length; i++) {
  742. const modal = modals[i];
  743. const ariaOwns = modal.getAttribute('aria-owns');
  744. if (!ariaOwns) {
  745. modal.setAttribute('aria-owns', id);
  746. }
  747. else if (ariaOwns.indexOf(id) === -1) {
  748. modal.setAttribute('aria-owns', ariaOwns + ' ' + id);
  749. }
  750. }
  751. }
  752. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: LiveAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
  753. static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: LiveAnnouncer, providedIn: 'root' });
  754. }
  755. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: LiveAnnouncer, decorators: [{
  756. type: Injectable,
  757. args: [{ providedIn: 'root' }]
  758. }], ctorParameters: () => [] });
  759. /**
  760. * A directive that works similarly to aria-live, but uses the LiveAnnouncer to ensure compatibility
  761. * with a wider range of browsers and screen readers.
  762. */
  763. class CdkAriaLive {
  764. _elementRef = inject(ElementRef);
  765. _liveAnnouncer = inject(LiveAnnouncer);
  766. _contentObserver = inject(ContentObserver);
  767. _ngZone = inject(NgZone);
  768. /** The aria-live politeness level to use when announcing messages. */
  769. get politeness() {
  770. return this._politeness;
  771. }
  772. set politeness(value) {
  773. this._politeness = value === 'off' || value === 'assertive' ? value : 'polite';
  774. if (this._politeness === 'off') {
  775. if (this._subscription) {
  776. this._subscription.unsubscribe();
  777. this._subscription = null;
  778. }
  779. }
  780. else if (!this._subscription) {
  781. this._subscription = this._ngZone.runOutsideAngular(() => {
  782. return this._contentObserver.observe(this._elementRef).subscribe(() => {
  783. // Note that we use textContent here, rather than innerText, in order to avoid a reflow.
  784. const elementText = this._elementRef.nativeElement.textContent;
  785. // The `MutationObserver` fires also for attribute
  786. // changes which we don't want to announce.
  787. if (elementText !== this._previousAnnouncedText) {
  788. this._liveAnnouncer.announce(elementText, this._politeness, this.duration);
  789. this._previousAnnouncedText = elementText;
  790. }
  791. });
  792. });
  793. }
  794. }
  795. _politeness = 'polite';
  796. /** Time in milliseconds after which to clear out the announcer element. */
  797. duration;
  798. _previousAnnouncedText;
  799. _subscription;
  800. constructor() {
  801. inject(_CdkPrivateStyleLoader).load(_VisuallyHiddenLoader);
  802. }
  803. ngOnDestroy() {
  804. if (this._subscription) {
  805. this._subscription.unsubscribe();
  806. }
  807. }
  808. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkAriaLive, deps: [], target: i0.ɵɵFactoryTarget.Directive });
  809. static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: CdkAriaLive, isStandalone: true, selector: "[cdkAriaLive]", inputs: { politeness: ["cdkAriaLive", "politeness"], duration: ["cdkAriaLiveDuration", "duration"] }, exportAs: ["cdkAriaLive"], ngImport: i0 });
  810. }
  811. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkAriaLive, decorators: [{
  812. type: Directive,
  813. args: [{
  814. selector: '[cdkAriaLive]',
  815. exportAs: 'cdkAriaLive',
  816. }]
  817. }], ctorParameters: () => [], propDecorators: { politeness: [{
  818. type: Input,
  819. args: ['cdkAriaLive']
  820. }], duration: [{
  821. type: Input,
  822. args: ['cdkAriaLiveDuration']
  823. }] } });
  824. /** Set of possible high-contrast mode backgrounds. */
  825. var HighContrastMode;
  826. (function (HighContrastMode) {
  827. HighContrastMode[HighContrastMode["NONE"] = 0] = "NONE";
  828. HighContrastMode[HighContrastMode["BLACK_ON_WHITE"] = 1] = "BLACK_ON_WHITE";
  829. HighContrastMode[HighContrastMode["WHITE_ON_BLACK"] = 2] = "WHITE_ON_BLACK";
  830. })(HighContrastMode || (HighContrastMode = {}));
  831. /** CSS class applied to the document body when in black-on-white high-contrast mode. */
  832. const BLACK_ON_WHITE_CSS_CLASS = 'cdk-high-contrast-black-on-white';
  833. /** CSS class applied to the document body when in white-on-black high-contrast mode. */
  834. const WHITE_ON_BLACK_CSS_CLASS = 'cdk-high-contrast-white-on-black';
  835. /** CSS class applied to the document body when in high-contrast mode. */
  836. const HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS = 'cdk-high-contrast-active';
  837. /**
  838. * Service to determine whether the browser is currently in a high-contrast-mode environment.
  839. *
  840. * Microsoft Windows supports an accessibility feature called "High Contrast Mode". This mode
  841. * changes the appearance of all applications, including web applications, to dramatically increase
  842. * contrast.
  843. *
  844. * IE, Edge, and Firefox currently support this mode. Chrome does not support Windows High Contrast
  845. * Mode. This service does not detect high-contrast mode as added by the Chrome "High Contrast"
  846. * browser extension.
  847. */
  848. class HighContrastModeDetector {
  849. _platform = inject(Platform);
  850. /**
  851. * Figuring out the high contrast mode and adding the body classes can cause
  852. * some expensive layouts. This flag is used to ensure that we only do it once.
  853. */
  854. _hasCheckedHighContrastMode;
  855. _document = inject(DOCUMENT);
  856. _breakpointSubscription;
  857. constructor() {
  858. this._breakpointSubscription = inject(BreakpointObserver)
  859. .observe('(forced-colors: active)')
  860. .subscribe(() => {
  861. if (this._hasCheckedHighContrastMode) {
  862. this._hasCheckedHighContrastMode = false;
  863. this._applyBodyHighContrastModeCssClasses();
  864. }
  865. });
  866. }
  867. /** Gets the current high-contrast-mode for the page. */
  868. getHighContrastMode() {
  869. if (!this._platform.isBrowser) {
  870. return HighContrastMode.NONE;
  871. }
  872. // Create a test element with an arbitrary background-color that is neither black nor
  873. // white; high-contrast mode will coerce the color to either black or white. Also ensure that
  874. // appending the test element to the DOM does not affect layout by absolutely positioning it
  875. const testElement = this._document.createElement('div');
  876. testElement.style.backgroundColor = 'rgb(1,2,3)';
  877. testElement.style.position = 'absolute';
  878. this._document.body.appendChild(testElement);
  879. // Get the computed style for the background color, collapsing spaces to normalize between
  880. // browsers. Once we get this color, we no longer need the test element. Access the `window`
  881. // via the document so we can fake it in tests. Note that we have extra null checks, because
  882. // this logic will likely run during app bootstrap and throwing can break the entire app.
  883. const documentWindow = this._document.defaultView || window;
  884. const computedStyle = documentWindow && documentWindow.getComputedStyle
  885. ? documentWindow.getComputedStyle(testElement)
  886. : null;
  887. const computedColor = ((computedStyle && computedStyle.backgroundColor) || '').replace(/ /g, '');
  888. testElement.remove();
  889. switch (computedColor) {
  890. // Pre Windows 11 dark theme.
  891. case 'rgb(0,0,0)':
  892. // Windows 11 dark themes.
  893. case 'rgb(45,50,54)':
  894. case 'rgb(32,32,32)':
  895. return HighContrastMode.WHITE_ON_BLACK;
  896. // Pre Windows 11 light theme.
  897. case 'rgb(255,255,255)':
  898. // Windows 11 light theme.
  899. case 'rgb(255,250,239)':
  900. return HighContrastMode.BLACK_ON_WHITE;
  901. }
  902. return HighContrastMode.NONE;
  903. }
  904. ngOnDestroy() {
  905. this._breakpointSubscription.unsubscribe();
  906. }
  907. /** Applies CSS classes indicating high-contrast mode to document body (browser-only). */
  908. _applyBodyHighContrastModeCssClasses() {
  909. if (!this._hasCheckedHighContrastMode && this._platform.isBrowser && this._document.body) {
  910. const bodyClasses = this._document.body.classList;
  911. bodyClasses.remove(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, BLACK_ON_WHITE_CSS_CLASS, WHITE_ON_BLACK_CSS_CLASS);
  912. this._hasCheckedHighContrastMode = true;
  913. const mode = this.getHighContrastMode();
  914. if (mode === HighContrastMode.BLACK_ON_WHITE) {
  915. bodyClasses.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, BLACK_ON_WHITE_CSS_CLASS);
  916. }
  917. else if (mode === HighContrastMode.WHITE_ON_BLACK) {
  918. bodyClasses.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, WHITE_ON_BLACK_CSS_CLASS);
  919. }
  920. }
  921. }
  922. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: HighContrastModeDetector, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
  923. static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: HighContrastModeDetector, providedIn: 'root' });
  924. }
  925. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: HighContrastModeDetector, decorators: [{
  926. type: Injectable,
  927. args: [{ providedIn: 'root' }]
  928. }], ctorParameters: () => [] });
  929. class A11yModule {
  930. constructor() {
  931. inject(HighContrastModeDetector)._applyBodyHighContrastModeCssClasses();
  932. }
  933. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: A11yModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
  934. static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.6", ngImport: i0, type: A11yModule, imports: [ObserversModule, CdkAriaLive, CdkTrapFocus, CdkMonitorFocus], exports: [CdkAriaLive, CdkTrapFocus, CdkMonitorFocus] });
  935. static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: A11yModule, imports: [ObserversModule] });
  936. }
  937. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: A11yModule, decorators: [{
  938. type: NgModule,
  939. args: [{
  940. imports: [ObserversModule, CdkAriaLive, CdkTrapFocus, CdkMonitorFocus],
  941. exports: [CdkAriaLive, CdkTrapFocus, CdkMonitorFocus],
  942. }]
  943. }], ctorParameters: () => [] });
  944. export { A11yModule as A, CdkTrapFocus as C, FocusTrapFactory as F, HighContrastModeDetector as H, InteractivityChecker as I, LiveAnnouncer as L, FocusTrap as a, HighContrastMode as b, IsFocusableConfig as c, CdkAriaLive as d, LIVE_ANNOUNCER_ELEMENT_TOKEN as e, LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY as f, LIVE_ANNOUNCER_DEFAULT_OPTIONS as g };
  945. //# sourceMappingURL=a11y-module-BYox5gpI.mjs.map