Files
pyrofetes-frontend/node_modules/@angular/cdk/fesm2022/a11y-module.mjs
CHEVALLIER Abel cb235644dc init
2025-11-13 16:23:22 +01:00

952 lines
42 KiB
JavaScript
Executable File

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 `<input type="hidden">`. */
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