UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

540 lines (530 loc) 24.4 kB
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