@angular/cdk
Version:
Angular Material Component Development Kit
433 lines (429 loc) • 15.1 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, inject, NgZone, DOCUMENT, RendererFactory2, Injectable, ElementRef, EventEmitter, Directive, Output } from '@angular/core';
import { BehaviorSubject, Subject, of } from 'rxjs';
import { skip, distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { isFakeMousedownFromScreenReader, isFakeTouchstartFromScreenReader } from './_fake-event-detection-chunk.mjs';
import { ALT, CONTROL, MAC_META, META, SHIFT } from './_keycodes-chunk.mjs';
import { _getEventTarget, _getShadowRoot } from './_shadow-dom-chunk.mjs';
import { Platform } from './_platform-chunk.mjs';
import { normalizePassiveListenerOptions } from './_passive-listeners-chunk.mjs';
import { coerceElement } from './_element-chunk.mjs';
const INPUT_MODALITY_DETECTOR_OPTIONS = new InjectionToken('cdk-input-modality-detector-options');
const INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS = {
ignoreKeys: [ALT, CONTROL, MAC_META, META, SHIFT]
};
const TOUCH_BUFFER_MS = 650;
const modalityEventListenerOptions = {
passive: true,
capture: true
};
class InputModalityDetector {
_platform = inject(Platform);
_listenerCleanups;
modalityDetected;
modalityChanged;
get mostRecentModality() {
return this._modality.value;
}
_mostRecentTarget = null;
_modality = new BehaviorSubject(null);
_options;
_lastTouchMs = 0;
_onKeydown = event => {
if (this._options?.ignoreKeys?.some(keyCode => keyCode === event.keyCode)) {
return;
}
this._modality.next('keyboard');
this._mostRecentTarget = _getEventTarget(event);
};
_onMousedown = event => {
if (Date.now() - this._lastTouchMs < TOUCH_BUFFER_MS) {
return;
}
this._modality.next(isFakeMousedownFromScreenReader(event) ? 'keyboard' : 'mouse');
this._mostRecentTarget = _getEventTarget(event);
};
_onTouchstart = event => {
if (isFakeTouchstartFromScreenReader(event)) {
this._modality.next('keyboard');
return;
}
this._lastTouchMs = Date.now();
this._modality.next('touch');
this._mostRecentTarget = _getEventTarget(event);
};
constructor() {
const ngZone = inject(NgZone);
const document = inject(DOCUMENT);
const options = inject(INPUT_MODALITY_DETECTOR_OPTIONS, {
optional: true
});
this._options = {
...INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS,
...options
};
this.modalityDetected = this._modality.pipe(skip(1));
this.modalityChanged = this.modalityDetected.pipe(distinctUntilChanged());
if (this._platform.isBrowser) {
const renderer = inject(RendererFactory2).createRenderer(null, null);
this._listenerCleanups = ngZone.runOutsideAngular(() => {
return [renderer.listen(document, 'keydown', this._onKeydown, modalityEventListenerOptions), renderer.listen(document, 'mousedown', this._onMousedown, modalityEventListenerOptions), renderer.listen(document, 'touchstart', this._onTouchstart, modalityEventListenerOptions)];
});
}
}
ngOnDestroy() {
this._modality.complete();
this._listenerCleanups?.forEach(cleanup => cleanup());
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "21.0.0",
ngImport: i0,
type: InputModalityDetector,
deps: [],
target: i0.ɵɵFactoryTarget.Injectable
});
static ɵprov = i0.ɵɵngDeclareInjectable({
minVersion: "12.0.0",
version: "21.0.0",
ngImport: i0,
type: InputModalityDetector,
providedIn: 'root'
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "21.0.0",
ngImport: i0,
type: InputModalityDetector,
decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}],
ctorParameters: () => []
});
var FocusMonitorDetectionMode;
(function (FocusMonitorDetectionMode) {
FocusMonitorDetectionMode[FocusMonitorDetectionMode["IMMEDIATE"] = 0] = "IMMEDIATE";
FocusMonitorDetectionMode[FocusMonitorDetectionMode["EVENTUAL"] = 1] = "EVENTUAL";
})(FocusMonitorDetectionMode || (FocusMonitorDetectionMode = {}));
const FOCUS_MONITOR_DEFAULT_OPTIONS = new InjectionToken('cdk-focus-monitor-default-options');
const captureEventListenerOptions = normalizePassiveListenerOptions({
passive: true,
capture: true
});
class FocusMonitor {
_ngZone = inject(NgZone);
_platform = inject(Platform);
_inputModalityDetector = inject(InputModalityDetector);
_origin = null;
_lastFocusOrigin;
_windowFocused = false;
_windowFocusTimeoutId;
_originTimeoutId;
_originFromTouchInteraction = false;
_elementInfo = new Map();
_monitoredElementCount = 0;
_rootNodeFocusListenerCount = new Map();
_detectionMode;
_windowFocusListener = () => {
this._windowFocused = true;
this._windowFocusTimeoutId = setTimeout(() => this._windowFocused = false);
};
_document = inject(DOCUMENT);
_stopInputModalityDetector = new Subject();
constructor() {
const options = inject(FOCUS_MONITOR_DEFAULT_OPTIONS, {
optional: true
});
this._detectionMode = options?.detectionMode || FocusMonitorDetectionMode.IMMEDIATE;
}
_rootNodeFocusAndBlurListener = event => {
const target = _getEventTarget(event);
for (let element = target; element; element = element.parentElement) {
if (event.type === 'focus') {
this._onFocus(event, element);
} else {
this._onBlur(event, element);
}
}
};
monitor(element, checkChildren = false) {
const nativeElement = coerceElement(element);
if (!this._platform.isBrowser || nativeElement.nodeType !== 1) {
return of();
}
const rootNode = _getShadowRoot(nativeElement) || this._document;
const cachedInfo = this._elementInfo.get(nativeElement);
if (cachedInfo) {
if (checkChildren) {
cachedInfo.checkChildren = true;
}
return cachedInfo.subject;
}
const info = {
checkChildren: checkChildren,
subject: new Subject(),
rootNode
};
this._elementInfo.set(nativeElement, info);
this._registerGlobalListeners(info);
return info.subject;
}
stopMonitoring(element) {
const nativeElement = coerceElement(element);
const elementInfo = this._elementInfo.get(nativeElement);
if (elementInfo) {
elementInfo.subject.complete();
this._setClasses(nativeElement);
this._elementInfo.delete(nativeElement);
this._removeGlobalListeners(elementInfo);
}
}
focusVia(element, origin, options) {
const nativeElement = coerceElement(element);
const focusedElement = this._document.activeElement;
if (nativeElement === focusedElement) {
this._getClosestElementsInfo(nativeElement).forEach(([currentElement, info]) => this._originChanged(currentElement, origin, info));
} else {
this._setOrigin(origin);
if (typeof nativeElement.focus === 'function') {
nativeElement.focus(options);
}
}
}
ngOnDestroy() {
this._elementInfo.forEach((_info, element) => this.stopMonitoring(element));
}
_getWindow() {
return this._document.defaultView || window;
}
_getFocusOrigin(focusEventTarget) {
if (this._origin) {
if (this._originFromTouchInteraction) {
return this._shouldBeAttributedToTouch(focusEventTarget) ? 'touch' : 'program';
} else {
return this._origin;
}
}
if (this._windowFocused && this._lastFocusOrigin) {
return this._lastFocusOrigin;
}
if (focusEventTarget && this._isLastInteractionFromInputLabel(focusEventTarget)) {
return 'mouse';
}
return 'program';
}
_shouldBeAttributedToTouch(focusEventTarget) {
return this._detectionMode === FocusMonitorDetectionMode.EVENTUAL || !!focusEventTarget?.contains(this._inputModalityDetector._mostRecentTarget);
}
_setClasses(element, origin) {
element.classList.toggle('cdk-focused', !!origin);
element.classList.toggle('cdk-touch-focused', origin === 'touch');
element.classList.toggle('cdk-keyboard-focused', origin === 'keyboard');
element.classList.toggle('cdk-mouse-focused', origin === 'mouse');
element.classList.toggle('cdk-program-focused', origin === 'program');
}
_setOrigin(origin, isFromInteraction = false) {
this._ngZone.runOutsideAngular(() => {
this._origin = origin;
this._originFromTouchInteraction = origin === 'touch' && isFromInteraction;
if (this._detectionMode === FocusMonitorDetectionMode.IMMEDIATE) {
clearTimeout(this._originTimeoutId);
const ms = this._originFromTouchInteraction ? TOUCH_BUFFER_MS : 1;
this._originTimeoutId = setTimeout(() => this._origin = null, ms);
}
});
}
_onFocus(event, element) {
const elementInfo = this._elementInfo.get(element);
const focusEventTarget = _getEventTarget(event);
if (!elementInfo || !elementInfo.checkChildren && element !== focusEventTarget) {
return;
}
this._originChanged(element, this._getFocusOrigin(focusEventTarget), elementInfo);
}
_onBlur(event, element) {
const elementInfo = this._elementInfo.get(element);
if (!elementInfo || elementInfo.checkChildren && event.relatedTarget instanceof Node && element.contains(event.relatedTarget)) {
return;
}
this._setClasses(element);
this._emitOrigin(elementInfo, null);
}
_emitOrigin(info, origin) {
if (info.subject.observers.length) {
this._ngZone.run(() => info.subject.next(origin));
}
}
_registerGlobalListeners(elementInfo) {
if (!this._platform.isBrowser) {
return;
}
const rootNode = elementInfo.rootNode;
const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode) || 0;
if (!rootNodeFocusListeners) {
this._ngZone.runOutsideAngular(() => {
rootNode.addEventListener('focus', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
rootNode.addEventListener('blur', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
});
}
this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners + 1);
if (++this._monitoredElementCount === 1) {
this._ngZone.runOutsideAngular(() => {
const window = this._getWindow();
window.addEventListener('focus', this._windowFocusListener);
});
this._inputModalityDetector.modalityDetected.pipe(takeUntil(this._stopInputModalityDetector)).subscribe(modality => {
this._setOrigin(modality, true);
});
}
}
_removeGlobalListeners(elementInfo) {
const rootNode = elementInfo.rootNode;
if (this._rootNodeFocusListenerCount.has(rootNode)) {
const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode);
if (rootNodeFocusListeners > 1) {
this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners - 1);
} else {
rootNode.removeEventListener('focus', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
rootNode.removeEventListener('blur', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
this._rootNodeFocusListenerCount.delete(rootNode);
}
}
if (! --this._monitoredElementCount) {
const window = this._getWindow();
window.removeEventListener('focus', this._windowFocusListener);
this._stopInputModalityDetector.next();
clearTimeout(this._windowFocusTimeoutId);
clearTimeout(this._originTimeoutId);
}
}
_originChanged(element, origin, elementInfo) {
this._setClasses(element, origin);
this._emitOrigin(elementInfo, origin);
this._lastFocusOrigin = origin;
}
_getClosestElementsInfo(element) {
const results = [];
this._elementInfo.forEach((info, currentElement) => {
if (currentElement === element || info.checkChildren && currentElement.contains(element)) {
results.push([currentElement, info]);
}
});
return results;
}
_isLastInteractionFromInputLabel(focusEventTarget) {
const {
_mostRecentTarget: mostRecentTarget,
mostRecentModality
} = this._inputModalityDetector;
if (mostRecentModality !== 'mouse' || !mostRecentTarget || mostRecentTarget === focusEventTarget || focusEventTarget.nodeName !== 'INPUT' && focusEventTarget.nodeName !== 'TEXTAREA' || focusEventTarget.disabled) {
return false;
}
const labels = focusEventTarget.labels;
if (labels) {
for (let i = 0; i < labels.length; i++) {
if (labels[i].contains(mostRecentTarget)) {
return true;
}
}
}
return false;
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "21.0.0",
ngImport: i0,
type: FocusMonitor,
deps: [],
target: i0.ɵɵFactoryTarget.Injectable
});
static ɵprov = i0.ɵɵngDeclareInjectable({
minVersion: "12.0.0",
version: "21.0.0",
ngImport: i0,
type: FocusMonitor,
providedIn: 'root'
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "21.0.0",
ngImport: i0,
type: FocusMonitor,
decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}],
ctorParameters: () => []
});
class CdkMonitorFocus {
_elementRef = inject(ElementRef);
_focusMonitor = inject(FocusMonitor);
_monitorSubscription;
_focusOrigin = null;
cdkFocusChange = new EventEmitter();
constructor() {}
get focusOrigin() {
return this._focusOrigin;
}
ngAfterViewInit() {
const element = this._elementRef.nativeElement;
this._monitorSubscription = this._focusMonitor.monitor(element, element.nodeType === 1 && element.hasAttribute('cdkMonitorSubtreeFocus')).subscribe(origin => {
this._focusOrigin = origin;
this.cdkFocusChange.emit(origin);
});
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._elementRef);
if (this._monitorSubscription) {
this._monitorSubscription.unsubscribe();
}
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "21.0.0",
ngImport: i0,
type: CdkMonitorFocus,
deps: [],
target: i0.ɵɵFactoryTarget.Directive
});
static ɵdir = i0.ɵɵngDeclareDirective({
minVersion: "14.0.0",
version: "21.0.0",
type: CdkMonitorFocus,
isStandalone: true,
selector: "[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]",
outputs: {
cdkFocusChange: "cdkFocusChange"
},
exportAs: ["cdkMonitorFocus"],
ngImport: i0
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "21.0.0",
ngImport: i0,
type: CdkMonitorFocus,
decorators: [{
type: Directive,
args: [{
selector: '[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]',
exportAs: 'cdkMonitorFocus'
}]
}],
ctorParameters: () => [],
propDecorators: {
cdkFocusChange: [{
type: Output
}]
}
});
export { CdkMonitorFocus, FOCUS_MONITOR_DEFAULT_OPTIONS, FocusMonitor, FocusMonitorDetectionMode, INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS, INPUT_MODALITY_DETECTOR_OPTIONS, InputModalityDetector };
//# sourceMappingURL=_focus-monitor-chunk.mjs.map