@angular/cdk
Version:
Angular Material Component Development Kit
540 lines (530 loc) • 24.4 kB
JavaScript
export { C as CdkMonitorFocus, c as FOCUS_MONITOR_DEFAULT_OPTIONS, d as FocusMonitor, F as FocusMonitorDetectionMode, a as INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS, b as INPUT_MODALITY_DETECTOR_OPTIONS, I as InputModalityDetector } from './focus-monitor-28b6c826.mjs';
import { F as FocusTrap, I as InteractivityChecker } from './a11y-module-e500b0f2.mjs';
export { A as A11yModule, d as CdkAriaLive, C as CdkTrapFocus, F as FocusTrap, b as FocusTrapFactory, a as HighContrastMode, H as HighContrastModeDetector, I as InteractivityChecker, c as IsFocusableConfig, g as LIVE_ANNOUNCER_DEFAULT_OPTIONS, e as LIVE_ANNOUNCER_ELEMENT_TOKEN, f as LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY, L as LiveAnnouncer } from './a11y-module-e500b0f2.mjs';
export { _ as _IdGenerator } from './id-generator-0b91c6f7.mjs';
import { DOCUMENT } from '@angular/common';
import * as i0 from '@angular/core';
import { inject, APP_ID, Injectable, InjectionToken, NgZone, Injector } from '@angular/core';
import { P as Platform } from './platform-20fc4de8.mjs';
import { _ as _CdkPrivateStyleLoader } from './style-loader-09eecacc.mjs';
import { _ as _VisuallyHiddenLoader } from './visually-hidden-9a93b8bb.mjs';
export { A as ActiveDescendantKeyManager } from './activedescendant-key-manager-f0c079ca.mjs';
export { F as FocusKeyManager } from './focus-key-manager-ff488563.mjs';
export { L as ListKeyManager } from './list-key-manager-f9c3e90c.mjs';
import { Subject } from 'rxjs';
import { T as TREE_KEY_MANAGER } from './tree-key-manager-1212bcbe.mjs';
export { T as TREE_KEY_MANAGER, b as TREE_KEY_MANAGER_FACTORY, c as TREE_KEY_MANAGER_FACTORY_PROVIDER, a as TreeKeyManager } from './tree-key-manager-1212bcbe.mjs';
export { i as isFakeMousedownFromScreenReader, a as isFakeTouchstartFromScreenReader } from './fake-event-detection-84590b88.mjs';
import 'rxjs/operators';
import './keycodes-0e4398c6.mjs';
import './shadow-dom-318658ae.mjs';
import './backwards-compatibility-08253a84.mjs';
import './passive-listeners-93cf8be8.mjs';
import './element-15999318.mjs';
import './breakpoints-observer-8e7409df.mjs';
import './array-6239d2f8.mjs';
import './observe-content-937cdfbe.mjs';
import './typeahead-0113d27c.mjs';
import './modifiers-3e8908bb.mjs';
import './observable-3cba8a1c.mjs';
/** IDs are delimited by an empty space, as per the spec. */
const ID_DELIMITER = ' ';
/**
* Adds the given ID to the specified ARIA attribute on an element.
* Used for attributes such as aria-labelledby, aria-owns, etc.
*/
function addAriaReferencedId(el, attr, id) {
const ids = getAriaReferenceIds(el, attr);
id = id.trim();
if (ids.some(existingId => existingId.trim() === id)) {
return;
}
ids.push(id);
el.setAttribute(attr, ids.join(ID_DELIMITER));
}
/**
* Removes the given ID from the specified ARIA attribute on an element.
* Used for attributes such as aria-labelledby, aria-owns, etc.
*/
function removeAriaReferencedId(el, attr, id) {
const ids = getAriaReferenceIds(el, attr);
id = id.trim();
const filteredIds = ids.filter(val => val !== id);
if (filteredIds.length) {
el.setAttribute(attr, filteredIds.join(ID_DELIMITER));
}
else {
el.removeAttribute(attr);
}
}
/**
* Gets the list of IDs referenced by the given ARIA attribute on an element.
* Used for attributes such as aria-labelledby, aria-owns, etc.
*/
function getAriaReferenceIds(el, attr) {
// Get string array of all individual ids (whitespace delimited) in the attribute value
const attrValue = el.getAttribute(attr);
return attrValue?.match(/\S+/g) ?? [];
}
/**
* ID used for the body container where all messages are appended.
* @deprecated No longer being used. To be removed.
* @breaking-change 14.0.0
*/
const MESSAGES_CONTAINER_ID = 'cdk-describedby-message-container';
/**
* ID prefix used for each created message element.
* @deprecated To be turned into a private variable.
* @breaking-change 14.0.0
*/
const CDK_DESCRIBEDBY_ID_PREFIX = 'cdk-describedby-message';
/**
* Attribute given to each host element that is described by a message element.
* @deprecated To be turned into a private variable.
* @breaking-change 14.0.0
*/
const CDK_DESCRIBEDBY_HOST_ATTRIBUTE = 'cdk-describedby-host';
/** Global incremental identifier for each registered message element. */
let nextId = 0;
/**
* Utility that creates visually hidden elements with a message content. Useful for elements that
* want to use aria-describedby to further describe themselves without adding additional visual
* content.
*/
class AriaDescriber {
_platform = inject(Platform);
_document = inject(DOCUMENT);
/** Map of all registered message elements that have been placed into the document. */
_messageRegistry = new Map();
/** Container for all registered messages. */
_messagesContainer = null;
/** Unique ID for the service. */
_id = `${nextId++}`;
constructor() {
inject(_CdkPrivateStyleLoader).load(_VisuallyHiddenLoader);
this._id = inject(APP_ID) + '-' + nextId++;
}
describe(hostElement, message, role) {
if (!this._canBeDescribed(hostElement, message)) {
return;
}
const key = getKey(message, role);
if (typeof message !== 'string') {
// We need to ensure that the element has an ID.
setMessageId(message, this._id);
this._messageRegistry.set(key, { messageElement: message, referenceCount: 0 });
}
else if (!this._messageRegistry.has(key)) {
this._createMessageElement(message, role);
}
if (!this._isElementDescribedByMessage(hostElement, key)) {
this._addMessageReference(hostElement, key);
}
}
removeDescription(hostElement, message, role) {
if (!message || !this._isElementNode(hostElement)) {
return;
}
const key = getKey(message, role);
if (this._isElementDescribedByMessage(hostElement, key)) {
this._removeMessageReference(hostElement, key);
}
// If the message is a string, it means that it's one that we created for the
// consumer so we can remove it safely, otherwise we should leave it in place.
if (typeof message === 'string') {
const registeredMessage = this._messageRegistry.get(key);
if (registeredMessage && registeredMessage.referenceCount === 0) {
this._deleteMessageElement(key);
}
}
if (this._messagesContainer?.childNodes.length === 0) {
this._messagesContainer.remove();
this._messagesContainer = null;
}
}
/** Unregisters all created message elements and removes the message container. */
ngOnDestroy() {
const describedElements = this._document.querySelectorAll(`[${CDK_DESCRIBEDBY_HOST_ATTRIBUTE}="${this._id}"]`);
for (let i = 0; i < describedElements.length; i++) {
this._removeCdkDescribedByReferenceIds(describedElements[i]);
describedElements[i].removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
}
this._messagesContainer?.remove();
this._messagesContainer = null;
this._messageRegistry.clear();
}
/**
* Creates a new element in the visually hidden message container element with the message
* as its content and adds it to the message registry.
*/
_createMessageElement(message, role) {
const messageElement = this._document.createElement('div');
setMessageId(messageElement, this._id);
messageElement.textContent = message;
if (role) {
messageElement.setAttribute('role', role);
}
this._createMessagesContainer();
this._messagesContainer.appendChild(messageElement);
this._messageRegistry.set(getKey(message, role), { messageElement, referenceCount: 0 });
}
/** Deletes the message element from the global messages container. */
_deleteMessageElement(key) {
this._messageRegistry.get(key)?.messageElement?.remove();
this._messageRegistry.delete(key);
}
/** Creates the global container for all aria-describedby messages. */
_createMessagesContainer() {
if (this._messagesContainer) {
return;
}
const containerClassName = 'cdk-describedby-message-container';
const serverContainers = this._document.querySelectorAll(`.${containerClassName}[platform="server"]`);
for (let i = 0; i < serverContainers.length; i++) {
// When going from the server to the client, we may end up in a situation where there's
// already a container on the page, but we don't have a reference to it. Clear the
// old container so we don't get duplicates. Doing this, instead of emptying the previous
// container, should be slightly faster.
serverContainers[i].remove();
}
const messagesContainer = this._document.createElement('div');
// We add `visibility: hidden` in order to prevent text in this container from
// being searchable by the browser's Ctrl + F functionality.
// Screen-readers will still read the description for elements with aria-describedby even
// when the description element is not visible.
messagesContainer.style.visibility = 'hidden';
// Even though we use `visibility: hidden`, we still apply `cdk-visually-hidden` so that
// the description element doesn't impact page layout.
messagesContainer.classList.add(containerClassName);
messagesContainer.classList.add('cdk-visually-hidden');
if (!this._platform.isBrowser) {
messagesContainer.setAttribute('platform', 'server');
}
this._document.body.appendChild(messagesContainer);
this._messagesContainer = messagesContainer;
}
/** Removes all cdk-describedby messages that are hosted through the element. */
_removeCdkDescribedByReferenceIds(element) {
// Remove all aria-describedby reference IDs that are prefixed by CDK_DESCRIBEDBY_ID_PREFIX
const originalReferenceIds = getAriaReferenceIds(element, 'aria-describedby').filter(id => id.indexOf(CDK_DESCRIBEDBY_ID_PREFIX) != 0);
element.setAttribute('aria-describedby', originalReferenceIds.join(' '));
}
/**
* Adds a message reference to the element using aria-describedby and increments the registered
* message's reference count.
*/
_addMessageReference(element, key) {
const registeredMessage = this._messageRegistry.get(key);
// Add the aria-describedby reference and set the
// describedby_host attribute to mark the element.
addAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
element.setAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE, this._id);
registeredMessage.referenceCount++;
}
/**
* Removes a message reference from the element using aria-describedby
* and decrements the registered message's reference count.
*/
_removeMessageReference(element, key) {
const registeredMessage = this._messageRegistry.get(key);
registeredMessage.referenceCount--;
removeAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
element.removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
}
/** Returns true if the element has been described by the provided message ID. */
_isElementDescribedByMessage(element, key) {
const referenceIds = getAriaReferenceIds(element, 'aria-describedby');
const registeredMessage = this._messageRegistry.get(key);
const messageId = registeredMessage && registeredMessage.messageElement.id;
return !!messageId && referenceIds.indexOf(messageId) != -1;
}
/** Determines whether a message can be described on a particular element. */
_canBeDescribed(element, message) {
if (!this._isElementNode(element)) {
return false;
}
if (message && typeof message === 'object') {
// We'd have to make some assumptions about the description element's text, if the consumer
// passed in an element. Assume that if an element is passed in, the consumer has verified
// that it can be used as a description.
return true;
}
const trimmedMessage = message == null ? '' : `${message}`.trim();
const ariaLabel = element.getAttribute('aria-label');
// We shouldn't set descriptions if they're exactly the same as the `aria-label` of the
// element, because screen readers will end up reading out the same text twice in a row.
return trimmedMessage ? !ariaLabel || ariaLabel.trim() !== trimmedMessage : false;
}
/** Checks whether a node is an Element node. */
_isElementNode(element) {
return element.nodeType === this._document.ELEMENT_NODE;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AriaDescriber, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AriaDescriber, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AriaDescriber, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
/** Gets a key that can be used to look messages up in the registry. */
function getKey(message, role) {
return typeof message === 'string' ? `${role || ''}/${message}` : message;
}
/** Assigns a unique ID to an element, if it doesn't have one already. */
function setMessageId(element, serviceId) {
if (!element.id) {
element.id = `${CDK_DESCRIBEDBY_ID_PREFIX}-${serviceId}-${nextId++}`;
}
}
// NoopTreeKeyManager is a "noop" implementation of TreeKeyMangerStrategy. Methods are noops. Does
// not emit to streams.
//
// Used for applications built before TreeKeyManager to opt-out of TreeKeyManager and revert to
// legacy behavior.
/**
* @docs-private
*
* Opt-out of Tree of key manager behavior.
*
* When provided, Tree has same focus management behavior as before TreeKeyManager was introduced.
* - Tree does not respond to keyboard interaction
* - Tree node allows tabindex to be set by Input binding
* - Tree node allows tabindex to be set by attribute binding
*
* @deprecated NoopTreeKeyManager deprecated. Use TreeKeyManager or inject a
* TreeKeyManagerStrategy instead. To be removed in a future version.
*
* @breaking-change 21.0.0
*/
class NoopTreeKeyManager {
_isNoopTreeKeyManager = true;
// Provide change as required by TreeKeyManagerStrategy. NoopTreeKeyManager is a "noop"
// implementation that does not emit to streams.
change = new Subject();
destroy() {
this.change.complete();
}
onKeydown() {
// noop
}
getActiveItemIndex() {
// Always return null. NoopTreeKeyManager is a "noop" implementation that does not maintain
// the active item.
return null;
}
getActiveItem() {
// Always return null. NoopTreeKeyManager is a "noop" implementation that does not maintain
// the active item.
return null;
}
focusItem() {
// noop
}
}
/**
* @docs-private
*
* Opt-out of Tree of key manager behavior.
*
* When provided, Tree has same focus management behavior as before TreeKeyManager was introduced.
* - Tree does not respond to keyboard interaction
* - Tree node allows tabindex to be set by Input binding
* - Tree node allows tabindex to be set by attribute binding
*
* @deprecated NoopTreeKeyManager deprecated. Use TreeKeyManager or inject a
* TreeKeyManagerStrategy instead. To be removed in a future version.
*
* @breaking-change 21.0.0
*/
function NOOP_TREE_KEY_MANAGER_FACTORY() {
return () => new NoopTreeKeyManager();
}
/**
* @docs-private
*
* Opt-out of Tree of key manager behavior.
*
* When provided, Tree has same focus management behavior as before TreeKeyManager was introduced.
* - Tree does not respond to keyboard interaction
* - Tree node allows tabindex to be set by Input binding
* - Tree node allows tabindex to be set by attribute binding
*
* @deprecated NoopTreeKeyManager deprecated. Use TreeKeyManager or inject a
* TreeKeyManagerStrategy instead. To be removed in a future version.
*
* @breaking-change 21.0.0
*/
const NOOP_TREE_KEY_MANAGER_FACTORY_PROVIDER = {
provide: TREE_KEY_MANAGER,
useFactory: NOOP_TREE_KEY_MANAGER_FACTORY,
};
/**
* Class that allows for trapping focus within a DOM element.
*
* This class uses a strategy pattern that determines how it traps focus.
* See FocusTrapInertStrategy.
*/
class ConfigurableFocusTrap extends FocusTrap {
_focusTrapManager;
_inertStrategy;
/** Whether the FocusTrap is enabled. */
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
if (this._enabled) {
this._focusTrapManager.register(this);
}
else {
this._focusTrapManager.deregister(this);
}
}
constructor(_element, _checker, _ngZone, _document, _focusTrapManager, _inertStrategy, config, injector) {
super(_element, _checker, _ngZone, _document, config.defer, injector);
this._focusTrapManager = _focusTrapManager;
this._inertStrategy = _inertStrategy;
this._focusTrapManager.register(this);
}
/** Notifies the FocusTrapManager that this FocusTrap will be destroyed. */
destroy() {
this._focusTrapManager.deregister(this);
super.destroy();
}
/** @docs-private Implemented as part of ManagedFocusTrap. */
_enable() {
this._inertStrategy.preventFocus(this);
this.toggleAnchors(true);
}
/** @docs-private Implemented as part of ManagedFocusTrap. */
_disable() {
this._inertStrategy.allowFocus(this);
this.toggleAnchors(false);
}
}
/**
* Lightweight FocusTrapInertStrategy that adds a document focus event
* listener to redirect focus back inside the FocusTrap.
*/
class EventListenerFocusTrapInertStrategy {
/** Focus event handler. */
_listener = null;
/** Adds a document event listener that keeps focus inside the FocusTrap. */
preventFocus(focusTrap) {
// Ensure there's only one listener per document
if (this._listener) {
focusTrap._document.removeEventListener('focus', this._listener, true);
}
this._listener = (e) => this._trapFocus(focusTrap, e);
focusTrap._ngZone.runOutsideAngular(() => {
focusTrap._document.addEventListener('focus', this._listener, true);
});
}
/** Removes the event listener added in preventFocus. */
allowFocus(focusTrap) {
if (!this._listener) {
return;
}
focusTrap._document.removeEventListener('focus', this._listener, true);
this._listener = null;
}
/**
* Refocuses the first element in the FocusTrap if the focus event target was outside
* the FocusTrap.
*
* This is an event listener callback. The event listener is added in runOutsideAngular,
* so all this code runs outside Angular as well.
*/
_trapFocus(focusTrap, event) {
const target = event.target;
const focusTrapRoot = focusTrap._element;
// Don't refocus if target was in an overlay, because the overlay might be associated
// with an element inside the FocusTrap, ex. mat-select.
if (target && !focusTrapRoot.contains(target) && !target.closest?.('div.cdk-overlay-pane')) {
// Some legacy FocusTrap usages have logic that focuses some element on the page
// just before FocusTrap is destroyed. For backwards compatibility, wait
// to be sure FocusTrap is still enabled before refocusing.
setTimeout(() => {
// Check whether focus wasn't put back into the focus trap while the timeout was pending.
if (focusTrap.enabled && !focusTrapRoot.contains(focusTrap._document.activeElement)) {
focusTrap.focusFirstTabbableElement();
}
});
}
}
}
/** The injection token used to specify the inert strategy. */
const FOCUS_TRAP_INERT_STRATEGY = new InjectionToken('FOCUS_TRAP_INERT_STRATEGY');
/** Injectable that ensures only the most recently enabled FocusTrap is active. */
class FocusTrapManager {
// A stack of the FocusTraps on the page. Only the FocusTrap at the
// top of the stack is active.
_focusTrapStack = [];
/**
* Disables the FocusTrap at the top of the stack, and then pushes
* the new FocusTrap onto the stack.
*/
register(focusTrap) {
// Dedupe focusTraps that register multiple times.
this._focusTrapStack = this._focusTrapStack.filter(ft => ft !== focusTrap);
let stack = this._focusTrapStack;
if (stack.length) {
stack[stack.length - 1]._disable();
}
stack.push(focusTrap);
focusTrap._enable();
}
/**
* Removes the FocusTrap from the stack, and activates the
* FocusTrap that is the new top of the stack.
*/
deregister(focusTrap) {
focusTrap._disable();
const stack = this._focusTrapStack;
const i = stack.indexOf(focusTrap);
if (i !== -1) {
stack.splice(i, 1);
if (stack.length) {
stack[stack.length - 1]._enable();
}
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: FocusTrapManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: FocusTrapManager, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: FocusTrapManager, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/** Factory that allows easy instantiation of configurable focus traps. */
class ConfigurableFocusTrapFactory {
_checker = inject(InteractivityChecker);
_ngZone = inject(NgZone);
_focusTrapManager = inject(FocusTrapManager);
_document = inject(DOCUMENT);
_inertStrategy;
_injector = inject(Injector);
constructor() {
const inertStrategy = inject(FOCUS_TRAP_INERT_STRATEGY, { optional: true });
// TODO split up the strategies into different modules, similar to DateAdapter.
this._inertStrategy = inertStrategy || new EventListenerFocusTrapInertStrategy();
}
create(element, config = { defer: false }) {
let configObject;
if (typeof config === 'boolean') {
configObject = { defer: config };
}
else {
configObject = config;
}
return new ConfigurableFocusTrap(element, this._checker, this._ngZone, this._document, this._focusTrapManager, this._inertStrategy, configObject, this._injector);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: ConfigurableFocusTrapFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: ConfigurableFocusTrapFactory, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: ConfigurableFocusTrapFactory, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
export { AriaDescriber, CDK_DESCRIBEDBY_HOST_ATTRIBUTE, CDK_DESCRIBEDBY_ID_PREFIX, ConfigurableFocusTrap, ConfigurableFocusTrapFactory, EventListenerFocusTrapInertStrategy, FOCUS_TRAP_INERT_STRATEGY, MESSAGES_CONTAINER_ID, NOOP_TREE_KEY_MANAGER_FACTORY, NOOP_TREE_KEY_MANAGER_FACTORY_PROVIDER, NoopTreeKeyManager, addAriaReferencedId, getAriaReferenceIds, removeAriaReferencedId };
//# sourceMappingURL=a11y.mjs.map