import * as i0 from '@angular/core'; import { inject, Injectable, afterNextRender, NgZone, DOCUMENT, Injector, ElementRef, booleanAttribute, Directive, Input, InjectionToken, NgModule } from '@angular/core'; import { CdkMonitorFocus } from './focus-monitor.mjs'; import { Platform } from './platform2.mjs'; import { _getFocusedElementPierceShadowDom } from './shadow-dom.mjs'; import { _CdkPrivateStyleLoader } from './style-loader.mjs'; import { _VisuallyHiddenLoader } from './private.mjs'; import { BreakpointObserver } from './breakpoints-observer.mjs'; import { ContentObserver, ObserversModule } from './observers.mjs'; /** * Configuration for the isFocusable method. */ class IsFocusableConfig { /** * Whether to count an element as focusable even if it is not currently visible. */ ignoreVisibility = false; } // The InteractivityChecker leans heavily on the ally.js accessibility utilities. // Methods like `isTabbable` are only covering specific edge-cases for the browsers which are // supported. /** * Utility for checking the interactivity of an element, such as whether it is focusable or * tabbable. */ class InteractivityChecker { _platform = inject(Platform); constructor() { } /** * Gets whether an element is disabled. * * @param element Element to be checked. * @returns Whether the element is disabled. */ isDisabled(element) { // This does not capture some cases, such as a non-form control with a disabled attribute or // a form control inside of a disabled form, but should capture the most common cases. return element.hasAttribute('disabled'); } /** * Gets whether an element is visible for the purposes of interactivity. * * This will capture states like `display: none` and `visibility: hidden`, but not things like * being clipped by an `overflow: hidden` parent or being outside the viewport. * * @returns Whether the element is visible. */ isVisible(element) { return hasGeometry(element) && getComputedStyle(element).visibility === 'visible'; } /** * Gets whether an element can be reached via Tab key. * Assumes that the element has already been checked with isFocusable. * * @param element Element to be checked. * @returns Whether the element is tabbable. */ isTabbable(element) { // Nothing is tabbable on the server 😎 if (!this._platform.isBrowser) { return false; } const frameElement = getFrameElement(getWindow(element)); if (frameElement) { // Frame elements inherit their tabindex onto all child elements. if (getTabIndexValue(frameElement) === -1) { return false; } // Browsers disable tabbing to an element inside of an invisible frame. if (!this.isVisible(frameElement)) { return false; } } let nodeName = element.nodeName.toLowerCase(); let tabIndexValue = getTabIndexValue(element); if (element.hasAttribute('contenteditable')) { return tabIndexValue !== -1; } if (nodeName === 'iframe' || nodeName === 'object') { // The frame or object's content may be tabbable depending on the content, but it's // not possibly to reliably detect the content of the frames. We always consider such // elements as non-tabbable. return false; } // In iOS, the browser only considers some specific elements as tabbable. if (this._platform.WEBKIT && this._platform.IOS && !isPotentiallyTabbableIOS(element)) { return false; } if (nodeName === 'audio') { // Audio elements without controls enabled are never tabbable, regardless // of the tabindex attribute explicitly being set. if (!element.hasAttribute('controls')) { return false; } // Audio elements with controls are by default tabbable unless the // tabindex attribute is set to `-1` explicitly. return tabIndexValue !== -1; } if (nodeName === 'video') { // For all video elements, if the tabindex attribute is set to `-1`, the video // is not tabbable. Note: We cannot rely on the default `HTMLElement.tabIndex` // property as that one is set to `-1` in Chrome, Edge and Safari v13.1. The // tabindex attribute is the source of truth here. if (tabIndexValue === -1) { return false; } // If the tabindex is explicitly set, and not `-1` (as per check before), the // video element is always tabbable (regardless of whether it has controls or not). if (tabIndexValue !== null) { return true; } // Otherwise (when no explicit tabindex is set), a video is only tabbable if it // has controls enabled. Firefox is special as videos are always tabbable regardless // of whether there are controls or not. return this._platform.FIREFOX || element.hasAttribute('controls'); } return element.tabIndex >= 0; } /** * Gets whether an element can be focused by the user. * * @param element Element to be checked. * @param config The config object with options to customize this method's behavior * @returns Whether the element is focusable. */ isFocusable(element, config) { // Perform checks in order of left to most expensive. // Again, naive approach that does not capture many edge cases and browser quirks. return (isPotentiallyFocusable(element) && !this.isDisabled(element) && (config?.ignoreVisibility || this.isVisible(element))); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: InteractivityChecker, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: InteractivityChecker, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: InteractivityChecker, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** * Returns the frame element from a window object. Since browsers like MS Edge throw errors if * the frameElement property is being accessed from a different host address, this property * should be accessed carefully. */ function getFrameElement(window) { try { return window.frameElement; } catch { return null; } } /** Checks whether the specified element has any geometry / rectangles. */ function hasGeometry(element) { // Use logic from jQuery to check for an invisible element. // See https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js#L12 return !!(element.offsetWidth || element.offsetHeight || (typeof element.getClientRects === 'function' && element.getClientRects().length)); } /** Gets whether an element's */ function isNativeFormElement(element) { let nodeName = element.nodeName.toLowerCase(); return (nodeName === 'input' || nodeName === 'select' || nodeName === 'button' || nodeName === 'textarea'); } /** Gets whether an element is an ``. */ function isHiddenInput(element) { return isInputElement(element) && element.type == 'hidden'; } /** Gets whether an element is an anchor that has an href attribute. */ function isAnchorWithHref(element) { return isAnchorElement(element) && element.hasAttribute('href'); } /** Gets whether an element is an input element. */ function isInputElement(element) { return element.nodeName.toLowerCase() == 'input'; } /** Gets whether an element is an anchor element. */ function isAnchorElement(element) { return element.nodeName.toLowerCase() == 'a'; } /** Gets whether an element has a valid tabindex. */ function hasValidTabIndex(element) { if (!element.hasAttribute('tabindex') || element.tabIndex === undefined) { return false; } let tabIndex = element.getAttribute('tabindex'); return !!(tabIndex && !isNaN(parseInt(tabIndex, 10))); } /** * Returns the parsed tabindex from the element attributes instead of returning the * evaluated tabindex from the browsers defaults. */ function getTabIndexValue(element) { if (!hasValidTabIndex(element)) { return null; } // See browser issue in Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054 const tabIndex = parseInt(element.getAttribute('tabindex') || '', 10); return isNaN(tabIndex) ? -1 : tabIndex; } /** Checks whether the specified element is potentially tabbable on iOS */ function isPotentiallyTabbableIOS(element) { let nodeName = element.nodeName.toLowerCase(); let inputType = nodeName === 'input' && element.type; return (inputType === 'text' || inputType === 'password' || nodeName === 'select' || nodeName === 'textarea'); } /** * Gets whether an element is potentially focusable without taking current visible/disabled state * into account. */ function isPotentiallyFocusable(element) { // Inputs are potentially focusable *unless* they're type="hidden". if (isHiddenInput(element)) { return false; } return (isNativeFormElement(element) || isAnchorWithHref(element) || element.hasAttribute('contenteditable') || hasValidTabIndex(element)); } /** Gets the parent window of a DOM node with regards of being inside of an iframe. */ function getWindow(node) { // ownerDocument is null if `node` itself *is* a document. return (node.ownerDocument && node.ownerDocument.defaultView) || window; } /** * Class that allows for trapping focus within a DOM element. * * This class currently uses a relatively simple approach to focus trapping. * It assumes that the tab order is the same as DOM order, which is not necessarily true. * Things like `tabIndex > 0`, flex `order`, and shadow roots can cause the two to be misaligned. */ class FocusTrap { _element; _checker; _ngZone; _document; _injector; _startAnchor; _endAnchor; _hasAttached = false; // Event listeners for the anchors. Need to be regular functions so that we can unbind them later. startAnchorListener = () => this.focusLastTabbableElement(); endAnchorListener = () => this.focusFirstTabbableElement(); /** Whether the focus trap is active. */ get enabled() { return this._enabled; } set enabled(value) { this._enabled = value; if (this._startAnchor && this._endAnchor) { this._toggleAnchorTabIndex(value, this._startAnchor); this._toggleAnchorTabIndex(value, this._endAnchor); } } _enabled = true; constructor(_element, _checker, _ngZone, _document, deferAnchors = false, /** @breaking-change 20.0.0 param to become required */ _injector) { this._element = _element; this._checker = _checker; this._ngZone = _ngZone; this._document = _document; this._injector = _injector; if (!deferAnchors) { this.attachAnchors(); } } /** Destroys the focus trap by cleaning up the anchors. */ destroy() { const startAnchor = this._startAnchor; const endAnchor = this._endAnchor; if (startAnchor) { startAnchor.removeEventListener('focus', this.startAnchorListener); startAnchor.remove(); } if (endAnchor) { endAnchor.removeEventListener('focus', this.endAnchorListener); endAnchor.remove(); } this._startAnchor = this._endAnchor = null; this._hasAttached = false; } /** * Inserts the anchors into the DOM. This is usually done automatically * in the constructor, but can be deferred for cases like directives with `*ngIf`. * @returns Whether the focus trap managed to attach successfully. This may not be the case * if the target element isn't currently in the DOM. */ attachAnchors() { // If we're not on the browser, there can be no focus to trap. if (this._hasAttached) { return true; } this._ngZone.runOutsideAngular(() => { if (!this._startAnchor) { this._startAnchor = this._createAnchor(); this._startAnchor.addEventListener('focus', this.startAnchorListener); } if (!this._endAnchor) { this._endAnchor = this._createAnchor(); this._endAnchor.addEventListener('focus', this.endAnchorListener); } }); if (this._element.parentNode) { this._element.parentNode.insertBefore(this._startAnchor, this._element); this._element.parentNode.insertBefore(this._endAnchor, this._element.nextSibling); this._hasAttached = true; } return this._hasAttached; } /** * Waits for the zone to stabilize, then focuses the first tabbable element. * @returns Returns a promise that resolves with a boolean, depending * on whether focus was moved successfully. */ focusInitialElementWhenReady(options) { return new Promise(resolve => { this._executeOnStable(() => resolve(this.focusInitialElement(options))); }); } /** * Waits for the zone to stabilize, then focuses * the first tabbable element within the focus trap region. * @returns Returns a promise that resolves with a boolean, depending * on whether focus was moved successfully. */ focusFirstTabbableElementWhenReady(options) { return new Promise(resolve => { this._executeOnStable(() => resolve(this.focusFirstTabbableElement(options))); }); } /** * Waits for the zone to stabilize, then focuses * the last tabbable element within the focus trap region. * @returns Returns a promise that resolves with a boolean, depending * on whether focus was moved successfully. */ focusLastTabbableElementWhenReady(options) { return new Promise(resolve => { this._executeOnStable(() => resolve(this.focusLastTabbableElement(options))); }); } /** * Get the specified boundary element of the trapped region. * @param bound The boundary to get (start or end of trapped region). * @returns The boundary element. */ _getRegionBoundary(bound) { // Contains the deprecated version of selector, for temporary backwards comparability. const markers = this._element.querySelectorAll(`[cdk-focus-region-${bound}], ` + `[cdkFocusRegion${bound}], ` + `[cdk-focus-${bound}]`); if (typeof ngDevMode === 'undefined' || ngDevMode) { for (let i = 0; i < markers.length; i++) { // @breaking-change 8.0.0 if (markers[i].hasAttribute(`cdk-focus-${bound}`)) { console.warn(`Found use of deprecated attribute 'cdk-focus-${bound}', ` + `use 'cdkFocusRegion${bound}' instead. The deprecated ` + `attribute will be removed in 8.0.0.`, markers[i]); } else if (markers[i].hasAttribute(`cdk-focus-region-${bound}`)) { console.warn(`Found use of deprecated attribute 'cdk-focus-region-${bound}', ` + `use 'cdkFocusRegion${bound}' instead. The deprecated attribute ` + `will be removed in 8.0.0.`, markers[i]); } } } if (bound == 'start') { return markers.length ? markers[0] : this._getFirstTabbableElement(this._element); } return markers.length ? markers[markers.length - 1] : this._getLastTabbableElement(this._element); } /** * Focuses the element that should be focused when the focus trap is initialized. * @returns Whether focus was moved successfully. */ focusInitialElement(options) { // Contains the deprecated version of selector, for temporary backwards comparability. const redirectToElement = this._element.querySelector(`[cdk-focus-initial], ` + `[cdkFocusInitial]`); if (redirectToElement) { // @breaking-change 8.0.0 if ((typeof ngDevMode === 'undefined' || ngDevMode) && redirectToElement.hasAttribute(`cdk-focus-initial`)) { console.warn(`Found use of deprecated attribute 'cdk-focus-initial', ` + `use 'cdkFocusInitial' instead. The deprecated attribute ` + `will be removed in 8.0.0`, redirectToElement); } // Warn the consumer if the element they've pointed to // isn't focusable, when not in production mode. if ((typeof ngDevMode === 'undefined' || ngDevMode) && !this._checker.isFocusable(redirectToElement)) { console.warn(`Element matching '[cdkFocusInitial]' is not focusable.`, redirectToElement); } if (!this._checker.isFocusable(redirectToElement)) { const focusableChild = this._getFirstTabbableElement(redirectToElement); focusableChild?.focus(options); return !!focusableChild; } redirectToElement.focus(options); return true; } return this.focusFirstTabbableElement(options); } /** * Focuses the first tabbable element within the focus trap region. * @returns Whether focus was moved successfully. */ focusFirstTabbableElement(options) { const redirectToElement = this._getRegionBoundary('start'); if (redirectToElement) { redirectToElement.focus(options); } return !!redirectToElement; } /** * Focuses the last tabbable element within the focus trap region. * @returns Whether focus was moved successfully. */ focusLastTabbableElement(options) { const redirectToElement = this._getRegionBoundary('end'); if (redirectToElement) { redirectToElement.focus(options); } return !!redirectToElement; } /** * Checks whether the focus trap has successfully been attached. */ hasAttached() { return this._hasAttached; } /** Get the first tabbable element from a DOM subtree (inclusive). */ _getFirstTabbableElement(root) { if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) { return root; } const children = root.children; for (let i = 0; i < children.length; i++) { const tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE ? this._getFirstTabbableElement(children[i]) : null; if (tabbableChild) { return tabbableChild; } } return null; } /** Get the last tabbable element from a DOM subtree (inclusive). */ _getLastTabbableElement(root) { if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) { return root; } // Iterate in reverse DOM order. const children = root.children; for (let i = children.length - 1; i >= 0; i--) { const tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE ? this._getLastTabbableElement(children[i]) : null; if (tabbableChild) { return tabbableChild; } } return null; } /** Creates an anchor element. */ _createAnchor() { const anchor = this._document.createElement('div'); this._toggleAnchorTabIndex(this._enabled, anchor); anchor.classList.add('cdk-visually-hidden'); anchor.classList.add('cdk-focus-trap-anchor'); anchor.setAttribute('aria-hidden', 'true'); return anchor; } /** * Toggles the `tabindex` of an anchor, based on the enabled state of the focus trap. * @param isEnabled Whether the focus trap is enabled. * @param anchor Anchor on which to toggle the tabindex. */ _toggleAnchorTabIndex(isEnabled, anchor) { // Remove the tabindex completely, rather than setting it to -1, because if the // element has a tabindex, the user might still hit it when navigating with the arrow keys. isEnabled ? anchor.setAttribute('tabindex', '0') : anchor.removeAttribute('tabindex'); } /** * Toggles the`tabindex` of both anchors to either trap Tab focus or allow it to escape. * @param enabled: Whether the anchors should trap Tab. */ toggleAnchors(enabled) { if (this._startAnchor && this._endAnchor) { this._toggleAnchorTabIndex(enabled, this._startAnchor); this._toggleAnchorTabIndex(enabled, this._endAnchor); } } /** Executes a function when the zone is stable. */ _executeOnStable(fn) { // TODO: remove this conditional when injector is required in the constructor. if (this._injector) { afterNextRender(fn, { injector: this._injector }); } else { setTimeout(fn); } } } /** * Factory that allows easy instantiation of focus traps. */ class FocusTrapFactory { _checker = inject(InteractivityChecker); _ngZone = inject(NgZone); _document = inject(DOCUMENT); _injector = inject(Injector); constructor() { inject(_CdkPrivateStyleLoader).load(_VisuallyHiddenLoader); } /** * Creates a focus-trapped region around the given element. * @param element The element around which focus will be trapped. * @param deferCaptureElements Defers the creation of focus-capturing elements to be done * manually by the user. * @returns The created focus trap instance. */ create(element, deferCaptureElements = false) { return new FocusTrap(element, this._checker, this._ngZone, this._document, deferCaptureElements, this._injector); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: FocusTrapFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: FocusTrapFactory, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: FocusTrapFactory, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** Directive for trapping focus within a region. */ class CdkTrapFocus { _elementRef = inject(ElementRef); _focusTrapFactory = inject(FocusTrapFactory); /** Underlying FocusTrap instance. */ focusTrap; /** Previously focused element to restore focus to upon destroy when using autoCapture. */ _previouslyFocusedElement = null; /** Whether the focus trap is active. */ get enabled() { return this.focusTrap?.enabled || false; } set enabled(value) { if (this.focusTrap) { this.focusTrap.enabled = value; } } /** * Whether the directive should automatically move focus into the trapped region upon * initialization and return focus to the previous activeElement upon destruction. */ autoCapture; constructor() { const platform = inject(Platform); if (platform.isBrowser) { this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true); } } ngOnDestroy() { this.focusTrap?.destroy(); // If we stored a previously focused element when using autoCapture, return focus to that // element now that the trapped region is being destroyed. if (this._previouslyFocusedElement) { this._previouslyFocusedElement.focus(); this._previouslyFocusedElement = null; } } ngAfterContentInit() { this.focusTrap?.attachAnchors(); if (this.autoCapture) { this._captureFocus(); } } ngDoCheck() { if (this.focusTrap && !this.focusTrap.hasAttached()) { this.focusTrap.attachAnchors(); } } ngOnChanges(changes) { const autoCaptureChange = changes['autoCapture']; if (autoCaptureChange && !autoCaptureChange.firstChange && this.autoCapture && this.focusTrap?.hasAttached()) { this._captureFocus(); } } _captureFocus() { this._previouslyFocusedElement = _getFocusedElementPierceShadowDom(); this.focusTrap?.focusInitialElementWhenReady(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: CdkTrapFocus, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "20.2.0-next.2", type: CdkTrapFocus, isStandalone: true, selector: "[cdkTrapFocus]", inputs: { enabled: ["cdkTrapFocus", "enabled", booleanAttribute], autoCapture: ["cdkTrapFocusAutoCapture", "autoCapture", booleanAttribute] }, exportAs: ["cdkTrapFocus"], usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: CdkTrapFocus, decorators: [{ type: Directive, args: [{ selector: '[cdkTrapFocus]', exportAs: 'cdkTrapFocus', }] }], ctorParameters: () => [], propDecorators: { enabled: [{ type: Input, args: [{ alias: 'cdkTrapFocus', transform: booleanAttribute }] }], autoCapture: [{ type: Input, args: [{ alias: 'cdkTrapFocusAutoCapture', transform: booleanAttribute }] }] } }); const LIVE_ANNOUNCER_ELEMENT_TOKEN = new InjectionToken('liveAnnouncerElement', { providedIn: 'root', factory: LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY, }); /** * @docs-private * @deprecated No longer used, will be removed. * @breaking-change 21.0.0 */ function LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY() { return null; } /** Injection token that can be used to configure the default options for the LiveAnnouncer. */ const LIVE_ANNOUNCER_DEFAULT_OPTIONS = new InjectionToken('LIVE_ANNOUNCER_DEFAULT_OPTIONS'); let uniqueIds = 0; class LiveAnnouncer { _ngZone = inject(NgZone); _defaultOptions = inject(LIVE_ANNOUNCER_DEFAULT_OPTIONS, { optional: true, }); _liveElement; _document = inject(DOCUMENT); _previousTimeout; _currentPromise; _currentResolve; constructor() { const elementToken = inject(LIVE_ANNOUNCER_ELEMENT_TOKEN, { optional: true }); this._liveElement = elementToken || this._createLiveElement(); } announce(message, ...args) { const defaultOptions = this._defaultOptions; let politeness; let duration; if (args.length === 1 && typeof args[0] === 'number') { duration = args[0]; } else { [politeness, duration] = args; } this.clear(); clearTimeout(this._previousTimeout); if (!politeness) { politeness = defaultOptions && defaultOptions.politeness ? defaultOptions.politeness : 'polite'; } if (duration == null && defaultOptions) { duration = defaultOptions.duration; } // TODO: ensure changing the politeness works on all environments we support. this._liveElement.setAttribute('aria-live', politeness); if (this._liveElement.id) { this._exposeAnnouncerToModals(this._liveElement.id); } // This 100ms timeout is necessary for some browser + screen-reader combinations: // - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout. // - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a // second time without clearing and then using a non-zero delay. // (using JAWS 17 at time of this writing). return this._ngZone.runOutsideAngular(() => { if (!this._currentPromise) { this._currentPromise = new Promise(resolve => (this._currentResolve = resolve)); } clearTimeout(this._previousTimeout); this._previousTimeout = setTimeout(() => { this._liveElement.textContent = message; if (typeof duration === 'number') { this._previousTimeout = setTimeout(() => this.clear(), duration); } // For some reason in tests this can be undefined // Probably related to ZoneJS and every other thing that patches browser APIs in tests this._currentResolve?.(); this._currentPromise = this._currentResolve = undefined; }, 100); return this._currentPromise; }); } /** * Clears the current text from the announcer element. Can be used to prevent * screen readers from reading the text out again while the user is going * through the page landmarks. */ clear() { if (this._liveElement) { this._liveElement.textContent = ''; } } ngOnDestroy() { clearTimeout(this._previousTimeout); this._liveElement?.remove(); this._liveElement = null; this._currentResolve?.(); this._currentPromise = this._currentResolve = undefined; } _createLiveElement() { const elementClass = 'cdk-live-announcer-element'; const previousElements = this._document.getElementsByClassName(elementClass); const liveEl = this._document.createElement('div'); // Remove any old containers. This can happen when coming in from a server-side-rendered page. for (let i = 0; i < previousElements.length; i++) { previousElements[i].remove(); } liveEl.classList.add(elementClass); liveEl.classList.add('cdk-visually-hidden'); liveEl.setAttribute('aria-atomic', 'true'); liveEl.setAttribute('aria-live', 'polite'); liveEl.id = `cdk-live-announcer-${uniqueIds++}`; this._document.body.appendChild(liveEl); return liveEl; } /** * Some browsers won't expose the accessibility node of the live announcer element if there is an * `aria-modal` and the live announcer is outside of it. This method works around the issue by * pointing the `aria-owns` of all modals to the live announcer element. */ _exposeAnnouncerToModals(id) { // TODO(http://github.com/angular/components/issues/26853): consider de-duplicating this with // the `SnakBarContainer` and other usages. // // Note that the selector here is limited to CDK overlays at the moment in order to reduce the // section of the DOM we need to look through. This should cover all the cases we support, but // the selector can be expanded if it turns out to be too narrow. const modals = this._document.querySelectorAll('body > .cdk-overlay-container [aria-modal="true"]'); for (let i = 0; i < modals.length; i++) { const modal = modals[i]; const ariaOwns = modal.getAttribute('aria-owns'); if (!ariaOwns) { modal.setAttribute('aria-owns', id); } else if (ariaOwns.indexOf(id) === -1) { modal.setAttribute('aria-owns', ariaOwns + ' ' + id); } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: LiveAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: LiveAnnouncer, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: LiveAnnouncer, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** * A directive that works similarly to aria-live, but uses the LiveAnnouncer to ensure compatibility * with a wider range of browsers and screen readers. */ class CdkAriaLive { _elementRef = inject(ElementRef); _liveAnnouncer = inject(LiveAnnouncer); _contentObserver = inject(ContentObserver); _ngZone = inject(NgZone); /** The aria-live politeness level to use when announcing messages. */ get politeness() { return this._politeness; } set politeness(value) { this._politeness = value === 'off' || value === 'assertive' ? value : 'polite'; if (this._politeness === 'off') { if (this._subscription) { this._subscription.unsubscribe(); this._subscription = null; } } else if (!this._subscription) { this._subscription = this._ngZone.runOutsideAngular(() => { return this._contentObserver.observe(this._elementRef).subscribe(() => { // Note that we use textContent here, rather than innerText, in order to avoid a reflow. const elementText = this._elementRef.nativeElement.textContent; // The `MutationObserver` fires also for attribute // changes which we don't want to announce. if (elementText !== this._previousAnnouncedText) { this._liveAnnouncer.announce(elementText, this._politeness, this.duration); this._previousAnnouncedText = elementText; } }); }); } } _politeness = 'polite'; /** Time in milliseconds after which to clear out the announcer element. */ duration; _previousAnnouncedText; _subscription; constructor() { inject(_CdkPrivateStyleLoader).load(_VisuallyHiddenLoader); } ngOnDestroy() { if (this._subscription) { this._subscription.unsubscribe(); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: CdkAriaLive, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.0-next.2", type: CdkAriaLive, isStandalone: true, selector: "[cdkAriaLive]", inputs: { politeness: ["cdkAriaLive", "politeness"], duration: ["cdkAriaLiveDuration", "duration"] }, exportAs: ["cdkAriaLive"], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: CdkAriaLive, decorators: [{ type: Directive, args: [{ selector: '[cdkAriaLive]', exportAs: 'cdkAriaLive', }] }], ctorParameters: () => [], propDecorators: { politeness: [{ type: Input, args: ['cdkAriaLive'] }], duration: [{ type: Input, args: ['cdkAriaLiveDuration'] }] } }); /** Set of possible high-contrast mode backgrounds. */ var HighContrastMode; (function (HighContrastMode) { HighContrastMode[HighContrastMode["NONE"] = 0] = "NONE"; HighContrastMode[HighContrastMode["BLACK_ON_WHITE"] = 1] = "BLACK_ON_WHITE"; HighContrastMode[HighContrastMode["WHITE_ON_BLACK"] = 2] = "WHITE_ON_BLACK"; })(HighContrastMode || (HighContrastMode = {})); /** CSS class applied to the document body when in black-on-white high-contrast mode. */ const BLACK_ON_WHITE_CSS_CLASS = 'cdk-high-contrast-black-on-white'; /** CSS class applied to the document body when in white-on-black high-contrast mode. */ const WHITE_ON_BLACK_CSS_CLASS = 'cdk-high-contrast-white-on-black'; /** CSS class applied to the document body when in high-contrast mode. */ const HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS = 'cdk-high-contrast-active'; /** * Service to determine whether the browser is currently in a high-contrast-mode environment. * * Microsoft Windows supports an accessibility feature called "High Contrast Mode". This mode * changes the appearance of all applications, including web applications, to dramatically increase * contrast. * * IE, Edge, and Firefox currently support this mode. Chrome does not support Windows High Contrast * Mode. This service does not detect high-contrast mode as added by the Chrome "High Contrast" * browser extension. */ class HighContrastModeDetector { _platform = inject(Platform); /** * Figuring out the high contrast mode and adding the body classes can cause * some expensive layouts. This flag is used to ensure that we only do it once. */ _hasCheckedHighContrastMode; _document = inject(DOCUMENT); _breakpointSubscription; constructor() { this._breakpointSubscription = inject(BreakpointObserver) .observe('(forced-colors: active)') .subscribe(() => { if (this._hasCheckedHighContrastMode) { this._hasCheckedHighContrastMode = false; this._applyBodyHighContrastModeCssClasses(); } }); } /** Gets the current high-contrast-mode for the page. */ getHighContrastMode() { if (!this._platform.isBrowser) { return HighContrastMode.NONE; } // Create a test element with an arbitrary background-color that is neither black nor // white; high-contrast mode will coerce the color to either black or white. Also ensure that // appending the test element to the DOM does not affect layout by absolutely positioning it const testElement = this._document.createElement('div'); testElement.style.backgroundColor = 'rgb(1,2,3)'; testElement.style.position = 'absolute'; this._document.body.appendChild(testElement); // Get the computed style for the background color, collapsing spaces to normalize between // browsers. Once we get this color, we no longer need the test element. Access the `window` // via the document so we can fake it in tests. Note that we have extra null checks, because // this logic will likely run during app bootstrap and throwing can break the entire app. const documentWindow = this._document.defaultView || window; const computedStyle = documentWindow && documentWindow.getComputedStyle ? documentWindow.getComputedStyle(testElement) : null; const computedColor = ((computedStyle && computedStyle.backgroundColor) || '').replace(/ /g, ''); testElement.remove(); switch (computedColor) { // Pre Windows 11 dark theme. case 'rgb(0,0,0)': // Windows 11 dark themes. case 'rgb(45,50,54)': case 'rgb(32,32,32)': return HighContrastMode.WHITE_ON_BLACK; // Pre Windows 11 light theme. case 'rgb(255,255,255)': // Windows 11 light theme. case 'rgb(255,250,239)': return HighContrastMode.BLACK_ON_WHITE; } return HighContrastMode.NONE; } ngOnDestroy() { this._breakpointSubscription.unsubscribe(); } /** Applies CSS classes indicating high-contrast mode to document body (browser-only). */ _applyBodyHighContrastModeCssClasses() { if (!this._hasCheckedHighContrastMode && this._platform.isBrowser && this._document.body) { const bodyClasses = this._document.body.classList; bodyClasses.remove(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, BLACK_ON_WHITE_CSS_CLASS, WHITE_ON_BLACK_CSS_CLASS); this._hasCheckedHighContrastMode = true; const mode = this.getHighContrastMode(); if (mode === HighContrastMode.BLACK_ON_WHITE) { bodyClasses.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, BLACK_ON_WHITE_CSS_CLASS); } else if (mode === HighContrastMode.WHITE_ON_BLACK) { bodyClasses.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, WHITE_ON_BLACK_CSS_CLASS); } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: HighContrastModeDetector, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: HighContrastModeDetector, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: HighContrastModeDetector, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class A11yModule { constructor() { inject(HighContrastModeDetector)._applyBodyHighContrastModeCssClasses(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: A11yModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.2.0-next.2", ngImport: i0, type: A11yModule, imports: [ObserversModule, CdkAriaLive, CdkTrapFocus, CdkMonitorFocus], exports: [CdkAriaLive, CdkTrapFocus, CdkMonitorFocus] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: A11yModule, imports: [ObserversModule] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: A11yModule, decorators: [{ type: NgModule, args: [{ imports: [ObserversModule, CdkAriaLive, CdkTrapFocus, CdkMonitorFocus], exports: [CdkAriaLive, CdkTrapFocus, CdkMonitorFocus], }] }], ctorParameters: () => [] }); export { A11yModule, CdkAriaLive, CdkTrapFocus, FocusTrap, FocusTrapFactory, HighContrastMode, HighContrastModeDetector, InteractivityChecker, IsFocusableConfig, LIVE_ANNOUNCER_DEFAULT_OPTIONS, LIVE_ANNOUNCER_ELEMENT_TOKEN, LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY, LiveAnnouncer }; //# sourceMappingURL=a11y-module.mjs.map