@angular/cdk 10.0.0-rc.3 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/a11y/aria-describer/aria-describer.d.ts +10 -1
- package/a11y/focus-trap/focus-trap.d.ts +4 -2
- package/a11y/index.metadata.json +1 -1
- package/a11y/interactivity-checker/interactivity-checker.d.ts +11 -1
- package/a11y/key-manager/list-key-manager.d.ts +6 -0
- package/accordion/accordion.d.ts +7 -1
- package/accordion/index.d.ts +1 -0
- package/accordion/index.metadata.json +1 -1
- package/bundles/cdk-a11y.umd.js +78 -15
- package/bundles/cdk-a11y.umd.js.map +1 -1
- package/bundles/cdk-a11y.umd.min.js +11 -11
- package/bundles/cdk-a11y.umd.min.js.map +1 -1
- package/bundles/cdk-accordion.umd.js +12 -4
- package/bundles/cdk-accordion.umd.js.map +1 -1
- package/bundles/cdk-accordion.umd.min.js +2 -2
- package/bundles/cdk-accordion.umd.min.js.map +1 -1
- package/bundles/cdk-drag-drop.umd.js +717 -641
- package/bundles/cdk-drag-drop.umd.js.map +1 -1
- package/bundles/cdk-drag-drop.umd.min.js +8 -16
- package/bundles/cdk-drag-drop.umd.min.js.map +1 -1
- package/bundles/cdk-overlay.umd.js +199 -42
- package/bundles/cdk-overlay.umd.js.map +1 -1
- package/bundles/cdk-overlay.umd.min.js +11 -18
- package/bundles/cdk-overlay.umd.min.js.map +1 -1
- package/bundles/cdk-platform.umd.js +0 -1
- package/bundles/cdk-platform.umd.js.map +1 -1
- package/bundles/cdk-platform.umd.min.js +2 -2
- package/bundles/cdk-platform.umd.min.js.map +1 -1
- package/bundles/cdk-scrolling.umd.js +26 -4
- package/bundles/cdk-scrolling.umd.js.map +1 -1
- package/bundles/cdk-scrolling.umd.min.js +11 -4
- package/bundles/cdk-scrolling.umd.min.js.map +1 -1
- package/bundles/cdk-testing-protractor.umd.min.js +1 -1
- package/bundles/cdk-testing-protractor.umd.min.js.map +1 -1
- package/bundles/cdk-testing-testbed.umd.min.js +8 -8
- package/bundles/cdk-testing-testbed.umd.min.js.map +1 -1
- package/bundles/cdk-testing.umd.js +32 -0
- package/bundles/cdk-testing.umd.js.map +1 -1
- package/bundles/cdk-testing.umd.min.js +5 -5
- package/bundles/cdk-testing.umd.min.js.map +1 -1
- package/bundles/cdk-tree.umd.js +6 -2
- package/bundles/cdk-tree.umd.js.map +1 -1
- package/bundles/cdk-tree.umd.min.js +3 -3
- package/bundles/cdk-tree.umd.min.js.map +1 -1
- package/bundles/cdk.umd.js +1 -1
- package/bundles/cdk.umd.js.map +1 -1
- package/bundles/cdk.umd.min.js +1 -1
- package/bundles/cdk.umd.min.js.map +1 -1
- package/drag-drop/directives/drag-handle.d.ts +7 -1
- package/drag-drop/directives/drag-placeholder.d.ts +7 -1
- package/drag-drop/directives/drag-preview.d.ts +7 -1
- package/drag-drop/directives/drag.d.ts +4 -14
- package/drag-drop/directives/drop-list-group.d.ts +7 -1
- package/drag-drop/directives/drop-list.d.ts +7 -1
- package/drag-drop/drag-ref.d.ts +6 -0
- package/drag-drop/drop-list-ref.d.ts +3 -2
- package/drag-drop/index.d.ts +2 -2
- package/drag-drop/index.metadata.json +1 -1
- package/esm2015/a11y/a11y-module.js +15 -19
- package/esm2015/a11y/aria-describer/aria-describer.js +177 -167
- package/esm2015/a11y/focus-monitor/focus-monitor.js +337 -345
- package/esm2015/a11y/focus-trap/configurable-focus-trap-factory.js +30 -34
- package/esm2015/a11y/focus-trap/focus-trap-manager.js +36 -40
- package/esm2015/a11y/focus-trap/focus-trap.js +85 -82
- package/esm2015/a11y/high-contrast-mode/high-contrast-mode-detector.js +56 -60
- package/esm2015/a11y/interactivity-checker/interactivity-checker.js +113 -104
- package/esm2015/a11y/key-manager/list-key-manager.js +29 -4
- package/esm2015/a11y/live-announcer/live-announcer.js +138 -146
- package/esm2015/accordion/accordion-item.js +119 -123
- package/esm2015/accordion/accordion-module.js +9 -13
- package/esm2015/accordion/accordion.js +49 -46
- package/esm2015/accordion/index.js +2 -1
- package/esm2015/bidi/bidi-module.js +9 -13
- package/esm2015/bidi/dir.js +41 -45
- package/esm2015/bidi/directionality.js +27 -31
- package/esm2015/clipboard/clipboard-module.js +9 -13
- package/esm2015/clipboard/clipboard.js +36 -40
- package/esm2015/clipboard/copy-to-clipboard.js +71 -75
- package/esm2015/collections/unique-selection-dispatcher.js +33 -37
- package/esm2015/drag-drop/directives/drag-handle.js +42 -39
- package/esm2015/drag-drop/directives/drag-placeholder.js +24 -21
- package/esm2015/drag-drop/directives/drag-preview.js +29 -26
- package/esm2015/drag-drop/directives/drag.js +313 -319
- package/esm2015/drag-drop/directives/drop-list-group.js +32 -29
- package/esm2015/drag-drop/directives/drop-list.js +251 -250
- package/esm2015/drag-drop/drag-drop-module.js +27 -31
- package/esm2015/drag-drop/drag-drop-registry.js +139 -143
- package/esm2015/drag-drop/drag-drop.js +33 -37
- package/esm2015/drag-drop/drag-ref.js +59 -25
- package/esm2015/drag-drop/drop-list-ref.js +15 -9
- package/esm2015/drag-drop/index.js +3 -2
- package/esm2015/layout/breakpoints-observer.js +81 -85
- package/esm2015/layout/layout-module.js +6 -10
- package/esm2015/layout/media-matcher.js +28 -32
- package/esm2015/observers/observe-content.js +147 -163
- package/esm2015/overlay/dispatchers/base-overlay-dispatcher.js +51 -0
- package/esm2015/overlay/dispatchers/index.js +10 -0
- package/esm2015/overlay/dispatchers/overlay-keyboard-dispatcher.js +79 -0
- package/esm2015/overlay/dispatchers/overlay-outside-click-dispatcher.js +94 -0
- package/esm2015/overlay/fullscreen-overlay-container.js +70 -74
- package/esm2015/overlay/index.js +5 -4
- package/esm2015/overlay/overlay-config.js +5 -1
- package/esm2015/overlay/overlay-container.js +69 -73
- package/esm2015/overlay/overlay-directives.js +245 -243
- package/esm2015/overlay/overlay-module.js +15 -19
- package/esm2015/overlay/overlay-ref.js +24 -2
- package/esm2015/overlay/overlay-reference.js +1 -1
- package/esm2015/overlay/overlay.js +93 -92
- package/esm2015/overlay/position/connected-position.js +14 -18
- package/esm2015/overlay/position/overlay-position-builder.js +43 -47
- package/esm2015/overlay/public-api.js +2 -2
- package/esm2015/overlay/scroll/scroll-strategy-options.js +33 -37
- package/esm2015/platform/features/scrolling.js +1 -2
- package/esm2015/platform/platform-module.js +6 -10
- package/esm2015/platform/platform.js +48 -52
- package/esm2015/portal/portal-directives.js +181 -201
- package/esm2015/scrolling/fixed-size-virtual-scroll.js +57 -47
- package/esm2015/scrolling/public-api.js +2 -1
- package/esm2015/scrolling/scroll-dispatcher.js +139 -143
- package/esm2015/scrolling/scrollable.js +135 -139
- package/esm2015/scrolling/scrolling-module.js +32 -40
- package/esm2015/scrolling/viewport-ruler.js +105 -109
- package/esm2015/scrolling/virtual-for-of.js +264 -268
- package/esm2015/scrolling/virtual-scroll-repeater.js +8 -0
- package/esm2015/scrolling/virtual-scroll-viewport.js +318 -322
- package/esm2015/stepper/step-header.js +20 -24
- package/esm2015/stepper/step-label.js +13 -17
- package/esm2015/stepper/stepper-button.js +59 -67
- package/esm2015/stepper/stepper-module.js +24 -28
- package/esm2015/stepper/stepper.js +313 -321
- package/esm2015/table/cell.js +129 -157
- package/esm2015/table/row.js +183 -219
- package/esm2015/table/table-module.js +9 -13
- package/esm2015/table/table.js +765 -785
- package/esm2015/table/text-column.js +85 -89
- package/esm2015/testing/component-harness.js +19 -1
- package/esm2015/testing/harness-environment.js +7 -1
- package/esm2015/text-field/autofill.js +89 -97
- package/esm2015/text-field/autosize.js +225 -229
- package/esm2015/text-field/text-field-module.js +10 -14
- package/esm2015/tree/control/nested-tree-control.js +7 -3
- package/esm2015/tree/nested-node.js +79 -83
- package/esm2015/tree/node.js +17 -21
- package/esm2015/tree/outlet.js +15 -19
- package/esm2015/tree/padding.js +88 -92
- package/esm2015/tree/toggle.js +32 -36
- package/esm2015/tree/tree-module.js +10 -14
- package/esm2015/tree/tree.js +266 -274
- package/esm2015/version.js +1 -1
- package/fesm2015/a11y.js +1021 -996
- package/fesm2015/a11y.js.map +1 -1
- package/fesm2015/accordion.js +173 -175
- package/fesm2015/accordion.js.map +1 -1
- package/fesm2015/bidi.js +74 -83
- package/fesm2015/bidi.js.map +1 -1
- package/fesm2015/cdk.js +1 -1
- package/fesm2015/cdk.js.map +1 -1
- package/fesm2015/clipboard.js +113 -122
- package/fesm2015/clipboard.js.map +1 -1
- package/fesm2015/collections.js +32 -35
- package/fesm2015/collections.js.map +1 -1
- package/fesm2015/drag-drop.js +1005 -961
- package/fesm2015/drag-drop.js.map +1 -1
- package/fesm2015/layout.js +111 -120
- package/fesm2015/layout.js.map +1 -1
- package/fesm2015/observers.js +146 -158
- package/fesm2015/observers.js.map +1 -1
- package/fesm2015/overlay.js +793 -659
- package/fesm2015/overlay.js.map +1 -1
- package/fesm2015/platform.js +52 -59
- package/fesm2015/platform.js.map +1 -1
- package/fesm2015/portal.js +180 -195
- package/fesm2015/portal.js.map +1 -1
- package/fesm2015/scrolling.js +1058 -1060
- package/fesm2015/scrolling.js.map +1 -1
- package/fesm2015/stepper.js +424 -445
- package/fesm2015/stepper.js.map +1 -1
- package/fesm2015/table.js +1178 -1247
- package/fesm2015/table.js.map +1 -1
- package/fesm2015/testing.js +25 -1
- package/fesm2015/testing.js.map +1 -1
- package/fesm2015/text-field.js +318 -330
- package/fesm2015/text-field.js.map +1 -1
- package/fesm2015/tree.js +517 -537
- package/fesm2015/tree.js.map +1 -1
- package/overlay/dispatchers/base-overlay-dispatcher.d.ts +28 -0
- package/overlay/dispatchers/index.d.ts +9 -0
- package/overlay/{keyboard → dispatchers}/overlay-keyboard-dispatcher.d.ts +4 -10
- package/overlay/dispatchers/overlay-outside-click-dispatcher.d.ts +27 -0
- package/overlay/index.d.ts +4 -3
- package/overlay/index.metadata.json +1 -1
- package/overlay/overlay-config.d.ts +4 -0
- package/overlay/overlay-directives.d.ts +4 -0
- package/overlay/overlay-ref.d.ts +8 -2
- package/overlay/overlay-reference.d.ts +1 -0
- package/overlay/overlay.d.ts +4 -2
- package/overlay/public-api.d.ts +1 -1
- package/package.json +3 -3
- package/schematics/index.js +1 -1
- package/schematics/migration.json +5 -0
- package/schematics/ng-add/index.js +1 -1
- package/schematics/ng-update/data/index.js +1 -1
- package/schematics/ng-update/devkit-file-system.d.ts +5 -5
- package/schematics/ng-update/devkit-file-system.js +21 -16
- package/schematics/ng-update/devkit-migration-rule.js +13 -20
- package/schematics/ng-update/devkit-migration.d.ts +0 -2
- package/schematics/ng-update/devkit-migration.js +1 -1
- package/schematics/ng-update/find-stylesheets.d.ts +10 -0
- package/schematics/ng-update/find-stylesheets.js +31 -0
- package/schematics/ng-update/index.d.ts +2 -0
- package/schematics/ng-update/index.js +7 -2
- package/schematics/ng-update/migrations/attribute-selectors.js +3 -3
- package/schematics/ng-update/migrations/css-selectors.js +3 -3
- package/schematics/ng-update/migrations/element-selectors.js +3 -3
- package/schematics/ng-update/public-api.js +1 -1
- package/schematics/update-tool/component-resource-collector.d.ts +1 -1
- package/schematics/update-tool/component-resource-collector.js +18 -14
- package/schematics/update-tool/file-system.d.ts +19 -14
- package/schematics/update-tool/file-system.js +1 -1
- package/schematics/update-tool/index.d.ts +21 -3
- package/schematics/update-tool/index.js +29 -23
- package/schematics/update-tool/public-api.js +1 -1
- package/schematics/update-tool/target-version.d.ts +2 -1
- package/schematics/update-tool/target-version.js +5 -3
- package/schematics/update-tool/utils/parse-tsconfig.d.ts +2 -1
- package/schematics/update-tool/utils/parse-tsconfig.js +6 -10
- package/schematics/update-tool/utils/virtual-host.d.ts +34 -0
- package/schematics/update-tool/utils/virtual-host.js +62 -0
- package/schematics/update-tool/version-changes.js +4 -6
- package/schematics/utils/index.js +1 -1
- package/schematics/utils/project-tsconfig-paths.d.ts +2 -1
- package/schematics/utils/project-tsconfig-paths.js +1 -1
- package/scrolling/index.metadata.json +1 -1
- package/scrolling/public-api.d.ts +1 -0
- package/scrolling/virtual-for-of.d.ts +2 -1
- package/scrolling/virtual-scroll-repeater.d.ts +16 -0
- package/scrolling/virtual-scroll-viewport.d.ts +4 -4
- package/testing/component-harness.d.ts +12 -0
- package/testing/harness-environment.d.ts +1 -0
- package/tree/control/nested-tree-control.d.ts +7 -2
- package/tree/index.metadata.json +1 -1
- package/esm2015/overlay/keyboard/overlay-keyboard-dispatcher.js +0 -100
package/fesm2015/a11y.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { DOCUMENT } from '@angular/common';
|
|
2
2
|
import { ɵɵdefineInjectable, ɵɵinject, Injectable, Inject, QueryList, isDevMode, NgZone, Directive, ElementRef, Input, InjectionToken, Optional, EventEmitter, Output, NgModule } from '@angular/core';
|
|
3
|
+
import { Platform, normalizePassiveListenerOptions, _getShadowRoot, PlatformModule } from '@angular/cdk/platform';
|
|
3
4
|
import { Subject, Subscription, of } from 'rxjs';
|
|
4
|
-
import { hasModifierKey, A, Z, ZERO, NINE, LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, TAB } from '@angular/cdk/keycodes';
|
|
5
|
+
import { hasModifierKey, A, Z, ZERO, NINE, END, HOME, LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, TAB } from '@angular/cdk/keycodes';
|
|
5
6
|
import { tap, debounceTime, filter, map, take } from 'rxjs/operators';
|
|
6
7
|
import { coerceBooleanProperty, coerceElement } from '@angular/cdk/coercion';
|
|
7
|
-
import { Platform, normalizePassiveListenerOptions, _getShadowRoot, PlatformModule } from '@angular/cdk/platform';
|
|
8
8
|
import { ContentObserver, ObserversModule } from '@angular/cdk/observers';
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -75,183 +75,192 @@ let messagesContainer = null;
|
|
|
75
75
|
* want to use aria-describedby to further describe themselves without adding additional visual
|
|
76
76
|
* content.
|
|
77
77
|
*/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
class AriaDescriber {
|
|
79
|
+
constructor(_document,
|
|
80
|
+
/**
|
|
81
|
+
* @breaking-change 8.0.0 `_platform` parameter to be made required.
|
|
82
|
+
*/
|
|
83
|
+
_platform) {
|
|
84
|
+
this._platform = _platform;
|
|
85
|
+
this._document = _document;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Adds to the host element an aria-describedby reference to a hidden element that contains
|
|
89
|
+
* the message. If the same message has already been registered, then it will reuse the created
|
|
90
|
+
* message element.
|
|
91
|
+
*/
|
|
92
|
+
describe(hostElement, message) {
|
|
93
|
+
if (!this._canBeDescribed(hostElement, message)) {
|
|
94
|
+
return;
|
|
82
95
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*/
|
|
88
|
-
describe(hostElement, message) {
|
|
89
|
-
if (!this._canBeDescribed(hostElement, message)) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (typeof message !== 'string') {
|
|
93
|
-
// We need to ensure that the element has an ID.
|
|
94
|
-
this._setMessageId(message);
|
|
95
|
-
messageRegistry.set(message, { messageElement: message, referenceCount: 0 });
|
|
96
|
-
}
|
|
97
|
-
else if (!messageRegistry.has(message)) {
|
|
98
|
-
this._createMessageElement(message);
|
|
99
|
-
}
|
|
100
|
-
if (!this._isElementDescribedByMessage(hostElement, message)) {
|
|
101
|
-
this._addMessageReference(hostElement, message);
|
|
102
|
-
}
|
|
96
|
+
if (typeof message !== 'string') {
|
|
97
|
+
// We need to ensure that the element has an ID.
|
|
98
|
+
this._setMessageId(message);
|
|
99
|
+
messageRegistry.set(message, { messageElement: message, referenceCount: 0 });
|
|
103
100
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (!this._isElementNode(hostElement)) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (this._isElementDescribedByMessage(hostElement, message)) {
|
|
110
|
-
this._removeMessageReference(hostElement, message);
|
|
111
|
-
}
|
|
112
|
-
// If the message is a string, it means that it's one that we created for the
|
|
113
|
-
// consumer so we can remove it safely, otherwise we should leave it in place.
|
|
114
|
-
if (typeof message === 'string') {
|
|
115
|
-
const registeredMessage = messageRegistry.get(message);
|
|
116
|
-
if (registeredMessage && registeredMessage.referenceCount === 0) {
|
|
117
|
-
this._deleteMessageElement(message);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (messagesContainer && messagesContainer.childNodes.length === 0) {
|
|
121
|
-
this._deleteMessagesContainer();
|
|
122
|
-
}
|
|
101
|
+
else if (!messageRegistry.has(message)) {
|
|
102
|
+
this._createMessageElement(message);
|
|
123
103
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const describedElements = this._document.querySelectorAll(`[${CDK_DESCRIBEDBY_HOST_ATTRIBUTE}]`);
|
|
127
|
-
for (let i = 0; i < describedElements.length; i++) {
|
|
128
|
-
this._removeCdkDescribedByReferenceIds(describedElements[i]);
|
|
129
|
-
describedElements[i].removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
|
|
130
|
-
}
|
|
131
|
-
if (messagesContainer) {
|
|
132
|
-
this._deleteMessagesContainer();
|
|
133
|
-
}
|
|
134
|
-
messageRegistry.clear();
|
|
104
|
+
if (!this._isElementDescribedByMessage(hostElement, message)) {
|
|
105
|
+
this._addMessageReference(hostElement, message);
|
|
135
106
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const messageElement = this._document.createElement('div');
|
|
142
|
-
this._setMessageId(messageElement);
|
|
143
|
-
messageElement.textContent = message;
|
|
144
|
-
this._createMessagesContainer();
|
|
145
|
-
messagesContainer.appendChild(messageElement);
|
|
146
|
-
messageRegistry.set(message, { messageElement, referenceCount: 0 });
|
|
147
|
-
}
|
|
148
|
-
/** Assigns a unique ID to an element, if it doesn't have one already. */
|
|
149
|
-
_setMessageId(element) {
|
|
150
|
-
if (!element.id) {
|
|
151
|
-
element.id = `${CDK_DESCRIBEDBY_ID_PREFIX}-${nextId++}`;
|
|
152
|
-
}
|
|
107
|
+
}
|
|
108
|
+
/** Removes the host element's aria-describedby reference to the message element. */
|
|
109
|
+
removeDescription(hostElement, message) {
|
|
110
|
+
if (!this._isElementNode(hostElement)) {
|
|
111
|
+
return;
|
|
153
112
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const registeredMessage = messageRegistry.get(message);
|
|
157
|
-
const messageElement = registeredMessage && registeredMessage.messageElement;
|
|
158
|
-
if (messagesContainer && messageElement) {
|
|
159
|
-
messagesContainer.removeChild(messageElement);
|
|
160
|
-
}
|
|
161
|
-
messageRegistry.delete(message);
|
|
162
|
-
}
|
|
163
|
-
/** Creates the global container for all aria-describedby messages. */
|
|
164
|
-
_createMessagesContainer() {
|
|
165
|
-
if (!messagesContainer) {
|
|
166
|
-
const preExistingContainer = this._document.getElementById(MESSAGES_CONTAINER_ID);
|
|
167
|
-
// When going from the server to the client, we may end up in a situation where there's
|
|
168
|
-
// already a container on the page, but we don't have a reference to it. Clear the
|
|
169
|
-
// old container so we don't get duplicates. Doing this, instead of emptying the previous
|
|
170
|
-
// container, should be slightly faster.
|
|
171
|
-
if (preExistingContainer) {
|
|
172
|
-
preExistingContainer.parentNode.removeChild(preExistingContainer);
|
|
173
|
-
}
|
|
174
|
-
messagesContainer = this._document.createElement('div');
|
|
175
|
-
messagesContainer.id = MESSAGES_CONTAINER_ID;
|
|
176
|
-
messagesContainer.setAttribute('aria-hidden', 'true');
|
|
177
|
-
messagesContainer.style.display = 'none';
|
|
178
|
-
this._document.body.appendChild(messagesContainer);
|
|
179
|
-
}
|
|
113
|
+
if (this._isElementDescribedByMessage(hostElement, message)) {
|
|
114
|
+
this._removeMessageReference(hostElement, message);
|
|
180
115
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
116
|
+
// If the message is a string, it means that it's one that we created for the
|
|
117
|
+
// consumer so we can remove it safely, otherwise we should leave it in place.
|
|
118
|
+
if (typeof message === 'string') {
|
|
119
|
+
const registeredMessage = messageRegistry.get(message);
|
|
120
|
+
if (registeredMessage && registeredMessage.referenceCount === 0) {
|
|
121
|
+
this._deleteMessageElement(message);
|
|
186
122
|
}
|
|
187
123
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// Remove all aria-describedby reference IDs that are prefixed by CDK_DESCRIBEDBY_ID_PREFIX
|
|
191
|
-
const originalReferenceIds = getAriaReferenceIds(element, 'aria-describedby')
|
|
192
|
-
.filter(id => id.indexOf(CDK_DESCRIBEDBY_ID_PREFIX) != 0);
|
|
193
|
-
element.setAttribute('aria-describedby', originalReferenceIds.join(' '));
|
|
124
|
+
if (messagesContainer && messagesContainer.childNodes.length === 0) {
|
|
125
|
+
this._deleteMessagesContainer();
|
|
194
126
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// describedby_host attribute to mark the element.
|
|
203
|
-
addAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
|
|
204
|
-
element.setAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE, '');
|
|
205
|
-
registeredMessage.referenceCount++;
|
|
127
|
+
}
|
|
128
|
+
/** Unregisters all created message elements and removes the message container. */
|
|
129
|
+
ngOnDestroy() {
|
|
130
|
+
const describedElements = this._document.querySelectorAll(`[${CDK_DESCRIBEDBY_HOST_ATTRIBUTE}]`);
|
|
131
|
+
for (let i = 0; i < describedElements.length; i++) {
|
|
132
|
+
this._removeCdkDescribedByReferenceIds(describedElements[i]);
|
|
133
|
+
describedElements[i].removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
|
|
206
134
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
* and decrements the registered message's reference count.
|
|
210
|
-
*/
|
|
211
|
-
_removeMessageReference(element, message) {
|
|
212
|
-
const registeredMessage = messageRegistry.get(message);
|
|
213
|
-
registeredMessage.referenceCount--;
|
|
214
|
-
removeAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
|
|
215
|
-
element.removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
|
|
135
|
+
if (messagesContainer) {
|
|
136
|
+
this._deleteMessagesContainer();
|
|
216
137
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
138
|
+
messageRegistry.clear();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Creates a new element in the visually hidden message container element with the message
|
|
142
|
+
* as its content and adds it to the message registry.
|
|
143
|
+
*/
|
|
144
|
+
_createMessageElement(message) {
|
|
145
|
+
const messageElement = this._document.createElement('div');
|
|
146
|
+
this._setMessageId(messageElement);
|
|
147
|
+
messageElement.textContent = message;
|
|
148
|
+
this._createMessagesContainer();
|
|
149
|
+
messagesContainer.appendChild(messageElement);
|
|
150
|
+
messageRegistry.set(message, { messageElement, referenceCount: 0 });
|
|
151
|
+
}
|
|
152
|
+
/** Assigns a unique ID to an element, if it doesn't have one already. */
|
|
153
|
+
_setMessageId(element) {
|
|
154
|
+
if (!element.id) {
|
|
155
|
+
element.id = `${CDK_DESCRIBEDBY_ID_PREFIX}-${nextId++}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Deletes the message element from the global messages container. */
|
|
159
|
+
_deleteMessageElement(message) {
|
|
160
|
+
const registeredMessage = messageRegistry.get(message);
|
|
161
|
+
const messageElement = registeredMessage && registeredMessage.messageElement;
|
|
162
|
+
if (messagesContainer && messageElement) {
|
|
163
|
+
messagesContainer.removeChild(messageElement);
|
|
164
|
+
}
|
|
165
|
+
messageRegistry.delete(message);
|
|
166
|
+
}
|
|
167
|
+
/** Creates the global container for all aria-describedby messages. */
|
|
168
|
+
_createMessagesContainer() {
|
|
169
|
+
if (!messagesContainer) {
|
|
170
|
+
// @breaking-change 8.0.0 `_platform` null check can be removed once the parameter is required
|
|
171
|
+
const canBeAriaHidden = !this._platform || (!this._platform.EDGE && !this._platform.TRIDENT);
|
|
172
|
+
const preExistingContainer = this._document.getElementById(MESSAGES_CONTAINER_ID);
|
|
173
|
+
// When going from the server to the client, we may end up in a situation where there's
|
|
174
|
+
// already a container on the page, but we don't have a reference to it. Clear the
|
|
175
|
+
// old container so we don't get duplicates. Doing this, instead of emptying the previous
|
|
176
|
+
// container, should be slightly faster.
|
|
177
|
+
if (preExistingContainer) {
|
|
178
|
+
preExistingContainer.parentNode.removeChild(preExistingContainer);
|
|
179
|
+
}
|
|
180
|
+
messagesContainer = this._document.createElement('div');
|
|
181
|
+
messagesContainer.id = MESSAGES_CONTAINER_ID;
|
|
182
|
+
messagesContainer.classList.add('cdk-visually-hidden');
|
|
183
|
+
// IE and Edge won't read out the messages if they're in an `aria-hidden` container.
|
|
184
|
+
// We only disable `aria-hidden` for these platforms, because it comes with the
|
|
185
|
+
// disadvantage that people might hit the messages when they've navigated past
|
|
186
|
+
// the end of the document using the arrow keys.
|
|
187
|
+
messagesContainer.setAttribute('aria-hidden', canBeAriaHidden + '');
|
|
188
|
+
this._document.body.appendChild(messagesContainer);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Deletes the global messages container. */
|
|
192
|
+
_deleteMessagesContainer() {
|
|
193
|
+
if (messagesContainer && messagesContainer.parentNode) {
|
|
194
|
+
messagesContainer.parentNode.removeChild(messagesContainer);
|
|
195
|
+
messagesContainer = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Removes all cdk-describedby messages that are hosted through the element. */
|
|
199
|
+
_removeCdkDescribedByReferenceIds(element) {
|
|
200
|
+
// Remove all aria-describedby reference IDs that are prefixed by CDK_DESCRIBEDBY_ID_PREFIX
|
|
201
|
+
const originalReferenceIds = getAriaReferenceIds(element, 'aria-describedby')
|
|
202
|
+
.filter(id => id.indexOf(CDK_DESCRIBEDBY_ID_PREFIX) != 0);
|
|
203
|
+
element.setAttribute('aria-describedby', originalReferenceIds.join(' '));
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Adds a message reference to the element using aria-describedby and increments the registered
|
|
207
|
+
* message's reference count.
|
|
208
|
+
*/
|
|
209
|
+
_addMessageReference(element, message) {
|
|
210
|
+
const registeredMessage = messageRegistry.get(message);
|
|
211
|
+
// Add the aria-describedby reference and set the
|
|
212
|
+
// describedby_host attribute to mark the element.
|
|
213
|
+
addAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
|
|
214
|
+
element.setAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE, '');
|
|
215
|
+
registeredMessage.referenceCount++;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Removes a message reference from the element using aria-describedby
|
|
219
|
+
* and decrements the registered message's reference count.
|
|
220
|
+
*/
|
|
221
|
+
_removeMessageReference(element, message) {
|
|
222
|
+
const registeredMessage = messageRegistry.get(message);
|
|
223
|
+
registeredMessage.referenceCount--;
|
|
224
|
+
removeAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
|
|
225
|
+
element.removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
|
|
226
|
+
}
|
|
227
|
+
/** Returns true if the element has been described by the provided message ID. */
|
|
228
|
+
_isElementDescribedByMessage(element, message) {
|
|
229
|
+
const referenceIds = getAriaReferenceIds(element, 'aria-describedby');
|
|
230
|
+
const registeredMessage = messageRegistry.get(message);
|
|
231
|
+
const messageId = registeredMessage && registeredMessage.messageElement.id;
|
|
232
|
+
return !!messageId && referenceIds.indexOf(messageId) != -1;
|
|
233
|
+
}
|
|
234
|
+
/** Determines whether a message can be described on a particular element. */
|
|
235
|
+
_canBeDescribed(element, message) {
|
|
236
|
+
if (!this._isElementNode(element)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (message && typeof message === 'object') {
|
|
240
|
+
// We'd have to make some assumptions about the description element's text, if the consumer
|
|
241
|
+
// passed in an element. Assume that if an element is passed in, the consumer has verified
|
|
242
|
+
// that it can be used as a description.
|
|
243
|
+
return true;
|
|
223
244
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return element.nodeType === this._document.ELEMENT_NODE;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
AriaDescriber.ɵprov = ɵɵdefineInjectable({ factory: function AriaDescriber_Factory() { return new AriaDescriber(ɵɵinject(DOCUMENT)); }, token: AriaDescriber, providedIn: "root" });
|
|
247
|
-
AriaDescriber.decorators = [
|
|
248
|
-
{ type: Injectable, args: [{ providedIn: 'root' },] }
|
|
249
|
-
];
|
|
250
|
-
AriaDescriber.ctorParameters = () => [
|
|
251
|
-
{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }
|
|
252
|
-
];
|
|
253
|
-
return AriaDescriber;
|
|
254
|
-
})();
|
|
245
|
+
const trimmedMessage = message == null ? '' : `${message}`.trim();
|
|
246
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
247
|
+
// We shouldn't set descriptions if they're exactly the same as the `aria-label` of the
|
|
248
|
+
// element, because screen readers will end up reading out the same text twice in a row.
|
|
249
|
+
return trimmedMessage ? (!ariaLabel || ariaLabel.trim() !== trimmedMessage) : false;
|
|
250
|
+
}
|
|
251
|
+
/** Checks whether a node is an Element node. */
|
|
252
|
+
_isElementNode(element) {
|
|
253
|
+
return element.nodeType === this._document.ELEMENT_NODE;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
AriaDescriber.ɵprov = ɵɵdefineInjectable({ factory: function AriaDescriber_Factory() { return new AriaDescriber(ɵɵinject(DOCUMENT), ɵɵinject(Platform)); }, token: AriaDescriber, providedIn: "root" });
|
|
257
|
+
AriaDescriber.decorators = [
|
|
258
|
+
{ type: Injectable, args: [{ providedIn: 'root' },] }
|
|
259
|
+
];
|
|
260
|
+
AriaDescriber.ctorParameters = () => [
|
|
261
|
+
{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] },
|
|
262
|
+
{ type: Platform }
|
|
263
|
+
];
|
|
255
264
|
|
|
256
265
|
/**
|
|
257
266
|
* @license
|
|
@@ -274,6 +283,7 @@ class ListKeyManager {
|
|
|
274
283
|
this._typeaheadSubscription = Subscription.EMPTY;
|
|
275
284
|
this._vertical = true;
|
|
276
285
|
this._allowedModifierKeys = [];
|
|
286
|
+
this._homeAndEnd = false;
|
|
277
287
|
/**
|
|
278
288
|
* Predicate function that can be used to check whether an item should be skipped
|
|
279
289
|
* by the key manager. By default, disabled items are skipped.
|
|
@@ -375,10 +385,18 @@ class ListKeyManager {
|
|
|
375
385
|
});
|
|
376
386
|
return this;
|
|
377
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Configures the key manager to focus the first and last items
|
|
390
|
+
* respectively when the Home key and End Key are pressed.
|
|
391
|
+
*/
|
|
392
|
+
withHomeAndEnd() {
|
|
393
|
+
this._homeAndEnd = true;
|
|
394
|
+
return this;
|
|
395
|
+
}
|
|
378
396
|
setActiveItem(item) {
|
|
379
|
-
const
|
|
397
|
+
const previousActiveItem = this._activeItem;
|
|
380
398
|
this.updateActiveItem(item);
|
|
381
|
-
if (this.
|
|
399
|
+
if (this._activeItem !== previousActiveItem) {
|
|
382
400
|
this.change.next(this._activeItemIndex);
|
|
383
401
|
}
|
|
384
402
|
}
|
|
@@ -428,6 +446,22 @@ class ListKeyManager {
|
|
|
428
446
|
else {
|
|
429
447
|
return;
|
|
430
448
|
}
|
|
449
|
+
case HOME:
|
|
450
|
+
if (this._homeAndEnd && isModifierAllowed) {
|
|
451
|
+
this.setFirstItemActive();
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
case END:
|
|
458
|
+
if (this._homeAndEnd && isModifierAllowed) {
|
|
459
|
+
this.setLastItemActive();
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
431
465
|
default:
|
|
432
466
|
if (isModifierAllowed || hasModifierKey(event, 'shiftKey')) {
|
|
433
467
|
// Attempt to use the `event.key` which also maps it to the user's keyboard language,
|
|
@@ -593,6 +627,17 @@ class FocusKeyManager extends ListKeyManager {
|
|
|
593
627
|
* Use of this source code is governed by an MIT-style license that can be
|
|
594
628
|
* found in the LICENSE file at https://angular.io/license
|
|
595
629
|
*/
|
|
630
|
+
/**
|
|
631
|
+
* Configuration for the isFocusable method.
|
|
632
|
+
*/
|
|
633
|
+
class IsFocusableConfig {
|
|
634
|
+
constructor() {
|
|
635
|
+
/**
|
|
636
|
+
* Whether to count an element as focusable even if it is not currently visible.
|
|
637
|
+
*/
|
|
638
|
+
this.ignoreVisibility = false;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
596
641
|
// The InteractivityChecker leans heavily on the ally.js accessibility utilities.
|
|
597
642
|
// Methods like `isTabbable` are only covering specific edge-cases for the browsers which are
|
|
598
643
|
// supported.
|
|
@@ -600,122 +645,121 @@ class FocusKeyManager extends ListKeyManager {
|
|
|
600
645
|
* Utility for checking the interactivity of an element, such as whether is is focusable or
|
|
601
646
|
* tabbable.
|
|
602
647
|
*/
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
648
|
+
class InteractivityChecker {
|
|
649
|
+
constructor(_platform) {
|
|
650
|
+
this._platform = _platform;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Gets whether an element is disabled.
|
|
654
|
+
*
|
|
655
|
+
* @param element Element to be checked.
|
|
656
|
+
* @returns Whether the element is disabled.
|
|
657
|
+
*/
|
|
658
|
+
isDisabled(element) {
|
|
659
|
+
// This does not capture some cases, such as a non-form control with a disabled attribute or
|
|
660
|
+
// a form control inside of a disabled form, but should capture the most common cases.
|
|
661
|
+
return element.hasAttribute('disabled');
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Gets whether an element is visible for the purposes of interactivity.
|
|
665
|
+
*
|
|
666
|
+
* This will capture states like `display: none` and `visibility: hidden`, but not things like
|
|
667
|
+
* being clipped by an `overflow: hidden` parent or being outside the viewport.
|
|
668
|
+
*
|
|
669
|
+
* @returns Whether the element is visible.
|
|
670
|
+
*/
|
|
671
|
+
isVisible(element) {
|
|
672
|
+
return hasGeometry(element) && getComputedStyle(element).visibility === 'visible';
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Gets whether an element can be reached via Tab key.
|
|
676
|
+
* Assumes that the element has already been checked with isFocusable.
|
|
677
|
+
*
|
|
678
|
+
* @param element Element to be checked.
|
|
679
|
+
* @returns Whether the element is tabbable.
|
|
680
|
+
*/
|
|
681
|
+
isTabbable(element) {
|
|
682
|
+
// Nothing is tabbable on the server 😎
|
|
683
|
+
if (!this._platform.isBrowser) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
const frameElement = getFrameElement(getWindow(element));
|
|
687
|
+
if (frameElement) {
|
|
688
|
+
const frameType = frameElement && frameElement.nodeName.toLowerCase();
|
|
689
|
+
// Frame elements inherit their tabindex onto all child elements.
|
|
690
|
+
if (getTabIndexValue(frameElement) === -1) {
|
|
640
691
|
return false;
|
|
641
692
|
}
|
|
642
|
-
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
// Frame elements inherit their tabindex onto all child elements.
|
|
646
|
-
if (getTabIndexValue(frameElement) === -1) {
|
|
647
|
-
return false;
|
|
648
|
-
}
|
|
649
|
-
// Webkit and Blink consider anything inside of an <object> element as non-tabbable.
|
|
650
|
-
if ((this._platform.BLINK || this._platform.WEBKIT) && frameType === 'object') {
|
|
651
|
-
return false;
|
|
652
|
-
}
|
|
653
|
-
// Webkit and Blink disable tabbing to an element inside of an invisible frame.
|
|
654
|
-
if ((this._platform.BLINK || this._platform.WEBKIT) && !this.isVisible(frameElement)) {
|
|
655
|
-
return false;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
let nodeName = element.nodeName.toLowerCase();
|
|
659
|
-
let tabIndexValue = getTabIndexValue(element);
|
|
660
|
-
if (element.hasAttribute('contenteditable')) {
|
|
661
|
-
return tabIndexValue !== -1;
|
|
693
|
+
// Webkit and Blink consider anything inside of an <object> element as non-tabbable.
|
|
694
|
+
if ((this._platform.BLINK || this._platform.WEBKIT) && frameType === 'object') {
|
|
695
|
+
return false;
|
|
662
696
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
// investigate the content of the frames.
|
|
697
|
+
// Webkit and Blink disable tabbing to an element inside of an invisible frame.
|
|
698
|
+
if ((this._platform.BLINK || this._platform.WEBKIT) && !this.isVisible(frameElement)) {
|
|
666
699
|
return false;
|
|
667
700
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
701
|
+
}
|
|
702
|
+
let nodeName = element.nodeName.toLowerCase();
|
|
703
|
+
let tabIndexValue = getTabIndexValue(element);
|
|
704
|
+
if (element.hasAttribute('contenteditable')) {
|
|
705
|
+
return tabIndexValue !== -1;
|
|
706
|
+
}
|
|
707
|
+
if (nodeName === 'iframe') {
|
|
708
|
+
// The frames may be tabbable depending on content, but it's not possibly to reliably
|
|
709
|
+
// investigate the content of the frames.
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
if (nodeName === 'audio') {
|
|
713
|
+
if (!element.hasAttribute('controls')) {
|
|
714
|
+
// By default an <audio> element without the controls enabled is not tabbable.
|
|
715
|
+
return false;
|
|
677
716
|
}
|
|
678
|
-
if (
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
return false;
|
|
682
|
-
}
|
|
683
|
-
else if (this._platform.BLINK || this._platform.FIREFOX) {
|
|
684
|
-
// In Chrome and Firefox <video controls> elements are always tabbable.
|
|
685
|
-
return true;
|
|
686
|
-
}
|
|
717
|
+
else if (this._platform.BLINK) {
|
|
718
|
+
// In Blink <audio controls> elements are always tabbable.
|
|
719
|
+
return true;
|
|
687
720
|
}
|
|
688
|
-
|
|
689
|
-
|
|
721
|
+
}
|
|
722
|
+
if (nodeName === 'video') {
|
|
723
|
+
if (!element.hasAttribute('controls') && this._platform.TRIDENT) {
|
|
724
|
+
// In Trident a <video> element without the controls enabled is not tabbable.
|
|
690
725
|
return false;
|
|
691
726
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
return
|
|
727
|
+
else if (this._platform.BLINK || this._platform.FIREFOX) {
|
|
728
|
+
// In Chrome and Firefox <video controls> elements are always tabbable.
|
|
729
|
+
return true;
|
|
695
730
|
}
|
|
696
|
-
return element.tabIndex >= 0;
|
|
697
731
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
732
|
+
if (nodeName === 'object' && (this._platform.BLINK || this._platform.WEBKIT)) {
|
|
733
|
+
// In all Blink and WebKit based browsers <object> elements are never tabbable.
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
// In iOS the browser only considers some specific elements as tabbable.
|
|
737
|
+
if (this._platform.WEBKIT && this._platform.IOS && !isPotentiallyTabbableIOS(element)) {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
return element.tabIndex >= 0;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Gets whether an element can be focused by the user.
|
|
744
|
+
*
|
|
745
|
+
* @param element Element to be checked.
|
|
746
|
+
* @param config The config object with options to customize this method's behavior
|
|
747
|
+
* @returns Whether the element is focusable.
|
|
748
|
+
*/
|
|
749
|
+
isFocusable(element, config) {
|
|
750
|
+
// Perform checks in order of left to most expensive.
|
|
751
|
+
// Again, naive approach that does not capture many edge cases and browser quirks.
|
|
752
|
+
return isPotentiallyFocusable(element) && !this.isDisabled(element) &&
|
|
753
|
+
((config === null || config === void 0 ? void 0 : config.ignoreVisibility) || this.isVisible(element));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
InteractivityChecker.ɵprov = ɵɵdefineInjectable({ factory: function InteractivityChecker_Factory() { return new InteractivityChecker(ɵɵinject(Platform)); }, token: InteractivityChecker, providedIn: "root" });
|
|
757
|
+
InteractivityChecker.decorators = [
|
|
758
|
+
{ type: Injectable, args: [{ providedIn: 'root' },] }
|
|
759
|
+
];
|
|
760
|
+
InteractivityChecker.ctorParameters = () => [
|
|
761
|
+
{ type: Platform }
|
|
762
|
+
];
|
|
719
763
|
/**
|
|
720
764
|
* Returns the frame element from a window object. Since browsers like MS Edge throw errors if
|
|
721
765
|
* the frameElement property is being accessed from a different host address, this property
|
|
@@ -871,6 +915,7 @@ class FocusTrap {
|
|
|
871
915
|
}
|
|
872
916
|
}
|
|
873
917
|
this._startAnchor = this._endAnchor = null;
|
|
918
|
+
this._hasAttached = false;
|
|
874
919
|
}
|
|
875
920
|
/**
|
|
876
921
|
* Inserts the anchors into the DOM. This is usually done automatically
|
|
@@ -1094,94 +1139,98 @@ class FocusTrap {
|
|
|
1094
1139
|
* @deprecated Use `ConfigurableFocusTrapFactory` instead.
|
|
1095
1140
|
* @breaking-change for 11.0.0 Remove this class.
|
|
1096
1141
|
*/
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
];
|
|
1124
|
-
return FocusTrapFactory;
|
|
1125
|
-
})();
|
|
1142
|
+
class FocusTrapFactory {
|
|
1143
|
+
constructor(_checker, _ngZone, _document) {
|
|
1144
|
+
this._checker = _checker;
|
|
1145
|
+
this._ngZone = _ngZone;
|
|
1146
|
+
this._document = _document;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Creates a focus-trapped region around the given element.
|
|
1150
|
+
* @param element The element around which focus will be trapped.
|
|
1151
|
+
* @param deferCaptureElements Defers the creation of focus-capturing elements to be done
|
|
1152
|
+
* manually by the user.
|
|
1153
|
+
* @returns The created focus trap instance.
|
|
1154
|
+
*/
|
|
1155
|
+
create(element, deferCaptureElements = false) {
|
|
1156
|
+
return new FocusTrap(element, this._checker, this._ngZone, this._document, deferCaptureElements);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
FocusTrapFactory.ɵprov = ɵɵdefineInjectable({ factory: function FocusTrapFactory_Factory() { return new FocusTrapFactory(ɵɵinject(InteractivityChecker), ɵɵinject(NgZone), ɵɵinject(DOCUMENT)); }, token: FocusTrapFactory, providedIn: "root" });
|
|
1160
|
+
FocusTrapFactory.decorators = [
|
|
1161
|
+
{ type: Injectable, args: [{ providedIn: 'root' },] }
|
|
1162
|
+
];
|
|
1163
|
+
FocusTrapFactory.ctorParameters = () => [
|
|
1164
|
+
{ type: InteractivityChecker },
|
|
1165
|
+
{ type: NgZone },
|
|
1166
|
+
{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }
|
|
1167
|
+
];
|
|
1126
1168
|
/** Directive for trapping focus within a region. */
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1169
|
+
class CdkTrapFocus {
|
|
1170
|
+
constructor(_elementRef, _focusTrapFactory, _document) {
|
|
1171
|
+
this._elementRef = _elementRef;
|
|
1172
|
+
this._focusTrapFactory = _focusTrapFactory;
|
|
1173
|
+
/** Previously focused element to restore focus to upon destroy when using autoCapture. */
|
|
1174
|
+
this._previouslyFocusedElement = null;
|
|
1175
|
+
this._document = _document;
|
|
1176
|
+
this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
|
|
1177
|
+
}
|
|
1178
|
+
/** Whether the focus trap is active. */
|
|
1179
|
+
get enabled() { return this.focusTrap.enabled; }
|
|
1180
|
+
set enabled(value) { this.focusTrap.enabled = coerceBooleanProperty(value); }
|
|
1181
|
+
/**
|
|
1182
|
+
* Whether the directive should automatially move focus into the trapped region upon
|
|
1183
|
+
* initialization and return focus to the previous activeElement upon destruction.
|
|
1184
|
+
*/
|
|
1185
|
+
get autoCapture() { return this._autoCapture; }
|
|
1186
|
+
set autoCapture(value) { this._autoCapture = coerceBooleanProperty(value); }
|
|
1187
|
+
ngOnDestroy() {
|
|
1188
|
+
this.focusTrap.destroy();
|
|
1189
|
+
// If we stored a previously focused element when using autoCapture, return focus to that
|
|
1190
|
+
// element now that the trapped region is being destroyed.
|
|
1191
|
+
if (this._previouslyFocusedElement) {
|
|
1192
|
+
this._previouslyFocusedElement.focus();
|
|
1133
1193
|
this._previouslyFocusedElement = null;
|
|
1134
|
-
this._document = _document;
|
|
1135
|
-
this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
|
|
1136
1194
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
* initialization and return focus to the previous activeElement upon destruction.
|
|
1143
|
-
*/
|
|
1144
|
-
get autoCapture() { return this._autoCapture; }
|
|
1145
|
-
set autoCapture(value) { this._autoCapture = coerceBooleanProperty(value); }
|
|
1146
|
-
ngOnDestroy() {
|
|
1147
|
-
this.focusTrap.destroy();
|
|
1148
|
-
// If we stored a previously focused element when using autoCapture, return focus to that
|
|
1149
|
-
// element now that the trapped region is being destroyed.
|
|
1150
|
-
if (this._previouslyFocusedElement) {
|
|
1151
|
-
this._previouslyFocusedElement.focus();
|
|
1152
|
-
this._previouslyFocusedElement = null;
|
|
1153
|
-
}
|
|
1195
|
+
}
|
|
1196
|
+
ngAfterContentInit() {
|
|
1197
|
+
this.focusTrap.attachAnchors();
|
|
1198
|
+
if (this.autoCapture) {
|
|
1199
|
+
this._captureFocus();
|
|
1154
1200
|
}
|
|
1155
|
-
|
|
1201
|
+
}
|
|
1202
|
+
ngDoCheck() {
|
|
1203
|
+
if (!this.focusTrap.hasAttached()) {
|
|
1156
1204
|
this.focusTrap.attachAnchors();
|
|
1157
|
-
if (this.autoCapture) {
|
|
1158
|
-
this._previouslyFocusedElement = this._document.activeElement;
|
|
1159
|
-
this.focusTrap.focusInitialElementWhenReady();
|
|
1160
|
-
}
|
|
1161
1205
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1206
|
+
}
|
|
1207
|
+
ngOnChanges(changes) {
|
|
1208
|
+
const autoCaptureChange = changes['autoCapture'];
|
|
1209
|
+
if (autoCaptureChange && !autoCaptureChange.firstChange && this.autoCapture &&
|
|
1210
|
+
this.focusTrap.hasAttached()) {
|
|
1211
|
+
this._captureFocus();
|
|
1166
1212
|
}
|
|
1167
1213
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1214
|
+
_captureFocus() {
|
|
1215
|
+
this._previouslyFocusedElement = this._document.activeElement;
|
|
1216
|
+
this.focusTrap.focusInitialElementWhenReady();
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
CdkTrapFocus.decorators = [
|
|
1220
|
+
{ type: Directive, args: [{
|
|
1221
|
+
selector: '[cdkTrapFocus]',
|
|
1222
|
+
exportAs: 'cdkTrapFocus',
|
|
1223
|
+
},] }
|
|
1224
|
+
];
|
|
1225
|
+
CdkTrapFocus.ctorParameters = () => [
|
|
1226
|
+
{ type: ElementRef },
|
|
1227
|
+
{ type: FocusTrapFactory },
|
|
1228
|
+
{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }
|
|
1229
|
+
];
|
|
1230
|
+
CdkTrapFocus.propDecorators = {
|
|
1231
|
+
enabled: [{ type: Input, args: ['cdkTrapFocus',] }],
|
|
1232
|
+
autoCapture: [{ type: Input, args: ['cdkTrapFocusAutoCapture',] }]
|
|
1233
|
+
};
|
|
1185
1234
|
|
|
1186
1235
|
/**
|
|
1187
1236
|
* @license
|
|
@@ -1366,49 +1415,46 @@ const FOCUS_TRAP_INERT_STRATEGY = new InjectionToken('FOCUS_TRAP_INERT_STRATEGY'
|
|
|
1366
1415
|
* found in the LICENSE file at https://angular.io/license
|
|
1367
1416
|
*/
|
|
1368
1417
|
/** Injectable that ensures only the most recently enabled FocusTrap is active. */
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1418
|
+
class FocusTrapManager {
|
|
1419
|
+
constructor() {
|
|
1420
|
+
// A stack of the FocusTraps on the page. Only the FocusTrap at the
|
|
1421
|
+
// top of the stack is active.
|
|
1422
|
+
this._focusTrapStack = [];
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Disables the FocusTrap at the top of the stack, and then pushes
|
|
1426
|
+
* the new FocusTrap onto the stack.
|
|
1427
|
+
*/
|
|
1428
|
+
register(focusTrap) {
|
|
1429
|
+
// Dedupe focusTraps that register multiple times.
|
|
1430
|
+
this._focusTrapStack = this._focusTrapStack.filter((ft) => ft !== focusTrap);
|
|
1431
|
+
let stack = this._focusTrapStack;
|
|
1432
|
+
if (stack.length) {
|
|
1433
|
+
stack[stack.length - 1]._disable();
|
|
1375
1434
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1435
|
+
stack.push(focusTrap);
|
|
1436
|
+
focusTrap._enable();
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Removes the FocusTrap from the stack, and activates the
|
|
1440
|
+
* FocusTrap that is the new top of the stack.
|
|
1441
|
+
*/
|
|
1442
|
+
deregister(focusTrap) {
|
|
1443
|
+
focusTrap._disable();
|
|
1444
|
+
const stack = this._focusTrapStack;
|
|
1445
|
+
const i = stack.indexOf(focusTrap);
|
|
1446
|
+
if (i !== -1) {
|
|
1447
|
+
stack.splice(i, 1);
|
|
1384
1448
|
if (stack.length) {
|
|
1385
|
-
stack[stack.length - 1].
|
|
1386
|
-
}
|
|
1387
|
-
stack.push(focusTrap);
|
|
1388
|
-
focusTrap._enable();
|
|
1389
|
-
}
|
|
1390
|
-
/**
|
|
1391
|
-
* Removes the FocusTrap from the stack, and activates the
|
|
1392
|
-
* FocusTrap that is the new top of the stack.
|
|
1393
|
-
*/
|
|
1394
|
-
deregister(focusTrap) {
|
|
1395
|
-
focusTrap._disable();
|
|
1396
|
-
const stack = this._focusTrapStack;
|
|
1397
|
-
const i = stack.indexOf(focusTrap);
|
|
1398
|
-
if (i !== -1) {
|
|
1399
|
-
stack.splice(i, 1);
|
|
1400
|
-
if (stack.length) {
|
|
1401
|
-
stack[stack.length - 1]._enable();
|
|
1402
|
-
}
|
|
1449
|
+
stack[stack.length - 1]._enable();
|
|
1403
1450
|
}
|
|
1404
1451
|
}
|
|
1405
1452
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
]
|
|
1410
|
-
|
|
1411
|
-
})();
|
|
1453
|
+
}
|
|
1454
|
+
FocusTrapManager.ɵprov = ɵɵdefineInjectable({ factory: function FocusTrapManager_Factory() { return new FocusTrapManager(); }, token: FocusTrapManager, providedIn: "root" });
|
|
1455
|
+
FocusTrapManager.decorators = [
|
|
1456
|
+
{ type: Injectable, args: [{ providedIn: 'root' },] }
|
|
1457
|
+
];
|
|
1412
1458
|
|
|
1413
1459
|
/**
|
|
1414
1460
|
* @license
|
|
@@ -1418,41 +1464,38 @@ let FocusTrapManager = /** @class */ (() => {
|
|
|
1418
1464
|
* found in the LICENSE file at https://angular.io/license
|
|
1419
1465
|
*/
|
|
1420
1466
|
/** Factory that allows easy instantiation of configurable focus traps. */
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
];
|
|
1454
|
-
return ConfigurableFocusTrapFactory;
|
|
1455
|
-
})();
|
|
1467
|
+
class ConfigurableFocusTrapFactory {
|
|
1468
|
+
constructor(_checker, _ngZone, _focusTrapManager, _document, _inertStrategy) {
|
|
1469
|
+
this._checker = _checker;
|
|
1470
|
+
this._ngZone = _ngZone;
|
|
1471
|
+
this._focusTrapManager = _focusTrapManager;
|
|
1472
|
+
this._document = _document;
|
|
1473
|
+
// TODO split up the strategies into different modules, similar to DateAdapter.
|
|
1474
|
+
this._inertStrategy = _inertStrategy || new EventListenerFocusTrapInertStrategy();
|
|
1475
|
+
}
|
|
1476
|
+
create(element, config = new ConfigurableFocusTrapConfig()) {
|
|
1477
|
+
let configObject;
|
|
1478
|
+
if (typeof config === 'boolean') {
|
|
1479
|
+
configObject = new ConfigurableFocusTrapConfig();
|
|
1480
|
+
configObject.defer = config;
|
|
1481
|
+
}
|
|
1482
|
+
else {
|
|
1483
|
+
configObject = config;
|
|
1484
|
+
}
|
|
1485
|
+
return new ConfigurableFocusTrap(element, this._checker, this._ngZone, this._document, this._focusTrapManager, this._inertStrategy, configObject);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
ConfigurableFocusTrapFactory.ɵprov = ɵɵdefineInjectable({ factory: function ConfigurableFocusTrapFactory_Factory() { return new ConfigurableFocusTrapFactory(ɵɵinject(InteractivityChecker), ɵɵinject(NgZone), ɵɵinject(FocusTrapManager), ɵɵinject(DOCUMENT), ɵɵinject(FOCUS_TRAP_INERT_STRATEGY, 8)); }, token: ConfigurableFocusTrapFactory, providedIn: "root" });
|
|
1489
|
+
ConfigurableFocusTrapFactory.decorators = [
|
|
1490
|
+
{ type: Injectable, args: [{ providedIn: 'root' },] }
|
|
1491
|
+
];
|
|
1492
|
+
ConfigurableFocusTrapFactory.ctorParameters = () => [
|
|
1493
|
+
{ type: InteractivityChecker },
|
|
1494
|
+
{ type: NgZone },
|
|
1495
|
+
{ type: FocusTrapManager },
|
|
1496
|
+
{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] },
|
|
1497
|
+
{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [FOCUS_TRAP_INERT_STRATEGY,] }] }
|
|
1498
|
+
];
|
|
1456
1499
|
|
|
1457
1500
|
/**
|
|
1458
1501
|
* @license
|
|
@@ -1479,164 +1522,158 @@ const LIVE_ANNOUNCER_DEFAULT_OPTIONS = new InjectionToken('LIVE_ANNOUNCER_DEFAUL
|
|
|
1479
1522
|
* Use of this source code is governed by an MIT-style license that can be
|
|
1480
1523
|
* found in the LICENSE file at https://angular.io/license
|
|
1481
1524
|
*/
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
politeness
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
return
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
this.
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
}, 100);
|
|
1529
|
-
});
|
|
1525
|
+
class LiveAnnouncer {
|
|
1526
|
+
constructor(elementToken, _ngZone, _document, _defaultOptions) {
|
|
1527
|
+
this._ngZone = _ngZone;
|
|
1528
|
+
this._defaultOptions = _defaultOptions;
|
|
1529
|
+
// We inject the live element and document as `any` because the constructor signature cannot
|
|
1530
|
+
// reference browser globals (HTMLElement, Document) on non-browser environments, since having
|
|
1531
|
+
// a class decorator causes TypeScript to preserve the constructor signature types.
|
|
1532
|
+
this._document = _document;
|
|
1533
|
+
this._liveElement = elementToken || this._createLiveElement();
|
|
1534
|
+
}
|
|
1535
|
+
announce(message, ...args) {
|
|
1536
|
+
const defaultOptions = this._defaultOptions;
|
|
1537
|
+
let politeness;
|
|
1538
|
+
let duration;
|
|
1539
|
+
if (args.length === 1 && typeof args[0] === 'number') {
|
|
1540
|
+
duration = args[0];
|
|
1541
|
+
}
|
|
1542
|
+
else {
|
|
1543
|
+
[politeness, duration] = args;
|
|
1544
|
+
}
|
|
1545
|
+
this.clear();
|
|
1546
|
+
clearTimeout(this._previousTimeout);
|
|
1547
|
+
if (!politeness) {
|
|
1548
|
+
politeness =
|
|
1549
|
+
(defaultOptions && defaultOptions.politeness) ? defaultOptions.politeness : 'polite';
|
|
1550
|
+
}
|
|
1551
|
+
if (duration == null && defaultOptions) {
|
|
1552
|
+
duration = defaultOptions.duration;
|
|
1553
|
+
}
|
|
1554
|
+
// TODO: ensure changing the politeness works on all environments we support.
|
|
1555
|
+
this._liveElement.setAttribute('aria-live', politeness);
|
|
1556
|
+
// This 100ms timeout is necessary for some browser + screen-reader combinations:
|
|
1557
|
+
// - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout.
|
|
1558
|
+
// - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a
|
|
1559
|
+
// second time without clearing and then using a non-zero delay.
|
|
1560
|
+
// (using JAWS 17 at time of this writing).
|
|
1561
|
+
return this._ngZone.runOutsideAngular(() => {
|
|
1562
|
+
return new Promise(resolve => {
|
|
1563
|
+
clearTimeout(this._previousTimeout);
|
|
1564
|
+
this._previousTimeout = setTimeout(() => {
|
|
1565
|
+
this._liveElement.textContent = message;
|
|
1566
|
+
resolve();
|
|
1567
|
+
if (typeof duration === 'number') {
|
|
1568
|
+
this._previousTimeout = setTimeout(() => this.clear(), duration);
|
|
1569
|
+
}
|
|
1570
|
+
}, 100);
|
|
1530
1571
|
});
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Clears the current text from the announcer element. Can be used to prevent
|
|
1576
|
+
* screen readers from reading the text out again while the user is going
|
|
1577
|
+
* through the page landmarks.
|
|
1578
|
+
*/
|
|
1579
|
+
clear() {
|
|
1580
|
+
if (this._liveElement) {
|
|
1581
|
+
this._liveElement.textContent = '';
|
|
1531
1582
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
if (this._liveElement) {
|
|
1539
|
-
this._liveElement.textContent = '';
|
|
1540
|
-
}
|
|
1583
|
+
}
|
|
1584
|
+
ngOnDestroy() {
|
|
1585
|
+
clearTimeout(this._previousTimeout);
|
|
1586
|
+
if (this._liveElement && this._liveElement.parentNode) {
|
|
1587
|
+
this._liveElement.parentNode.removeChild(this._liveElement);
|
|
1588
|
+
this._liveElement = null;
|
|
1541
1589
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1590
|
+
}
|
|
1591
|
+
_createLiveElement() {
|
|
1592
|
+
const elementClass = 'cdk-live-announcer-element';
|
|
1593
|
+
const previousElements = this._document.getElementsByClassName(elementClass);
|
|
1594
|
+
const liveEl = this._document.createElement('div');
|
|
1595
|
+
// Remove any old containers. This can happen when coming in from a server-side-rendered page.
|
|
1596
|
+
for (let i = 0; i < previousElements.length; i++) {
|
|
1597
|
+
previousElements[i].parentNode.removeChild(previousElements[i]);
|
|
1548
1598
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
{ type: Injectable, args: [{ providedIn: 'root' },] }
|
|
1568
|
-
];
|
|
1569
|
-
LiveAnnouncer.ctorParameters = () => [
|
|
1570
|
-
{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [LIVE_ANNOUNCER_ELEMENT_TOKEN,] }] },
|
|
1571
|
-
{ type: NgZone },
|
|
1572
|
-
{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] },
|
|
1573
|
-
{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [LIVE_ANNOUNCER_DEFAULT_OPTIONS,] }] }
|
|
1574
|
-
];
|
|
1575
|
-
return LiveAnnouncer;
|
|
1576
|
-
})();
|
|
1599
|
+
liveEl.classList.add(elementClass);
|
|
1600
|
+
liveEl.classList.add('cdk-visually-hidden');
|
|
1601
|
+
liveEl.setAttribute('aria-atomic', 'true');
|
|
1602
|
+
liveEl.setAttribute('aria-live', 'polite');
|
|
1603
|
+
this._document.body.appendChild(liveEl);
|
|
1604
|
+
return liveEl;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
LiveAnnouncer.ɵprov = ɵɵdefineInjectable({ factory: function LiveAnnouncer_Factory() { return new LiveAnnouncer(ɵɵinject(LIVE_ANNOUNCER_ELEMENT_TOKEN, 8), ɵɵinject(NgZone), ɵɵinject(DOCUMENT), ɵɵinject(LIVE_ANNOUNCER_DEFAULT_OPTIONS, 8)); }, token: LiveAnnouncer, providedIn: "root" });
|
|
1608
|
+
LiveAnnouncer.decorators = [
|
|
1609
|
+
{ type: Injectable, args: [{ providedIn: 'root' },] }
|
|
1610
|
+
];
|
|
1611
|
+
LiveAnnouncer.ctorParameters = () => [
|
|
1612
|
+
{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [LIVE_ANNOUNCER_ELEMENT_TOKEN,] }] },
|
|
1613
|
+
{ type: NgZone },
|
|
1614
|
+
{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] },
|
|
1615
|
+
{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [LIVE_ANNOUNCER_DEFAULT_OPTIONS,] }] }
|
|
1616
|
+
];
|
|
1577
1617
|
/**
|
|
1578
1618
|
* A directive that works similarly to aria-live, but uses the LiveAnnouncer to ensure compatibility
|
|
1579
1619
|
* with a wider range of browsers and screen readers.
|
|
1580
1620
|
*/
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
if (this._politeness === 'off') {
|
|
1595
|
-
if (this._subscription) {
|
|
1596
|
-
this._subscription.unsubscribe();
|
|
1597
|
-
this._subscription = null;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
else if (!this._subscription) {
|
|
1601
|
-
this._subscription = this._ngZone.runOutsideAngular(() => {
|
|
1602
|
-
return this._contentObserver
|
|
1603
|
-
.observe(this._elementRef)
|
|
1604
|
-
.subscribe(() => {
|
|
1605
|
-
// Note that we use textContent here, rather than innerText, in order to avoid a reflow.
|
|
1606
|
-
const elementText = this._elementRef.nativeElement.textContent;
|
|
1607
|
-
// The `MutationObserver` fires also for attribute
|
|
1608
|
-
// changes which we don't want to announce.
|
|
1609
|
-
if (elementText !== this._previousAnnouncedText) {
|
|
1610
|
-
this._liveAnnouncer.announce(elementText, this._politeness);
|
|
1611
|
-
this._previousAnnouncedText = elementText;
|
|
1612
|
-
}
|
|
1613
|
-
});
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
ngOnDestroy() {
|
|
1621
|
+
class CdkAriaLive {
|
|
1622
|
+
constructor(_elementRef, _liveAnnouncer, _contentObserver, _ngZone) {
|
|
1623
|
+
this._elementRef = _elementRef;
|
|
1624
|
+
this._liveAnnouncer = _liveAnnouncer;
|
|
1625
|
+
this._contentObserver = _contentObserver;
|
|
1626
|
+
this._ngZone = _ngZone;
|
|
1627
|
+
this._politeness = 'off';
|
|
1628
|
+
}
|
|
1629
|
+
/** The aria-live politeness level to use when announcing messages. */
|
|
1630
|
+
get politeness() { return this._politeness; }
|
|
1631
|
+
set politeness(value) {
|
|
1632
|
+
this._politeness = value === 'polite' || value === 'assertive' ? value : 'off';
|
|
1633
|
+
if (this._politeness === 'off') {
|
|
1618
1634
|
if (this._subscription) {
|
|
1619
1635
|
this._subscription.unsubscribe();
|
|
1620
|
-
|
|
1636
|
+
this._subscription = null;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
else if (!this._subscription) {
|
|
1640
|
+
this._subscription = this._ngZone.runOutsideAngular(() => {
|
|
1641
|
+
return this._contentObserver
|
|
1642
|
+
.observe(this._elementRef)
|
|
1643
|
+
.subscribe(() => {
|
|
1644
|
+
// Note that we use textContent here, rather than innerText, in order to avoid a reflow.
|
|
1645
|
+
const elementText = this._elementRef.nativeElement.textContent;
|
|
1646
|
+
// The `MutationObserver` fires also for attribute
|
|
1647
|
+
// changes which we don't want to announce.
|
|
1648
|
+
if (elementText !== this._previousAnnouncedText) {
|
|
1649
|
+
this._liveAnnouncer.announce(elementText, this._politeness);
|
|
1650
|
+
this._previousAnnouncedText = elementText;
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
});
|
|
1621
1654
|
}
|
|
1622
1655
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
}
|
|
1656
|
+
ngOnDestroy() {
|
|
1657
|
+
if (this._subscription) {
|
|
1658
|
+
this._subscription.unsubscribe();
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
CdkAriaLive.decorators = [
|
|
1663
|
+
{ type: Directive, args: [{
|
|
1664
|
+
selector: '[cdkAriaLive]',
|
|
1665
|
+
exportAs: 'cdkAriaLive',
|
|
1666
|
+
},] }
|
|
1667
|
+
];
|
|
1668
|
+
CdkAriaLive.ctorParameters = () => [
|
|
1669
|
+
{ type: ElementRef },
|
|
1670
|
+
{ type: LiveAnnouncer },
|
|
1671
|
+
{ type: ContentObserver },
|
|
1672
|
+
{ type: NgZone }
|
|
1673
|
+
];
|
|
1674
|
+
CdkAriaLive.propDecorators = {
|
|
1675
|
+
politeness: [{ type: Input, args: ['cdkAriaLive',] }]
|
|
1676
|
+
};
|
|
1640
1677
|
|
|
1641
1678
|
/**
|
|
1642
1679
|
* @license
|
|
@@ -1677,342 +1714,339 @@ const captureEventListenerOptions = normalizePassiveListenerOptions({
|
|
|
1677
1714
|
capture: true
|
|
1678
1715
|
});
|
|
1679
1716
|
/** Monitors mouse and keyboard events to determine the cause of focus events. */
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
this._monitoredElementCount = 0;
|
|
1695
|
-
/**
|
|
1696
|
-
* Keeps track of the root nodes to which we've currently bound a focus/blur handler,
|
|
1697
|
-
* as well as the number of monitored elements that they contain. We have to treat focus/blur
|
|
1698
|
-
* handlers differently from the rest of the events, because the browser won't emit events
|
|
1699
|
-
* to the document when focus moves inside of a shadow root.
|
|
1700
|
-
*/
|
|
1701
|
-
this._rootNodeFocusListenerCount = new Map();
|
|
1702
|
-
/**
|
|
1703
|
-
* Event listener for `keydown` events on the document.
|
|
1704
|
-
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
1705
|
-
*/
|
|
1706
|
-
this._documentKeydownListener = () => {
|
|
1707
|
-
// On keydown record the origin and clear any touch event that may be in progress.
|
|
1708
|
-
this._lastTouchTarget = null;
|
|
1709
|
-
this._setOriginForCurrentEventQueue('keyboard');
|
|
1710
|
-
};
|
|
1711
|
-
/**
|
|
1712
|
-
* Event listener for `mousedown` events on the document.
|
|
1713
|
-
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
1714
|
-
*/
|
|
1715
|
-
this._documentMousedownListener = (event) => {
|
|
1716
|
-
// On mousedown record the origin only if there is not touch
|
|
1717
|
-
// target, since a mousedown can happen as a result of a touch event.
|
|
1718
|
-
if (!this._lastTouchTarget) {
|
|
1719
|
-
// In some cases screen readers fire fake `mousedown` events instead of `keydown`.
|
|
1720
|
-
// Resolve the focus source to `keyboard` if we detect one of them.
|
|
1721
|
-
const source = isFakeMousedownFromScreenReader(event) ? 'keyboard' : 'mouse';
|
|
1722
|
-
this._setOriginForCurrentEventQueue(source);
|
|
1723
|
-
}
|
|
1724
|
-
};
|
|
1725
|
-
/**
|
|
1726
|
-
* Event listener for `touchstart` events on the document.
|
|
1727
|
-
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
1728
|
-
*/
|
|
1729
|
-
this._documentTouchstartListener = (event) => {
|
|
1730
|
-
// When the touchstart event fires the focus event is not yet in the event queue. This means
|
|
1731
|
-
// we can't rely on the trick used above (setting timeout of 1ms). Instead we wait 650ms to
|
|
1732
|
-
// see if a focus happens.
|
|
1733
|
-
if (this._touchTimeoutId != null) {
|
|
1734
|
-
clearTimeout(this._touchTimeoutId);
|
|
1735
|
-
}
|
|
1736
|
-
this._lastTouchTarget = getTarget(event);
|
|
1737
|
-
this._touchTimeoutId = setTimeout(() => this._lastTouchTarget = null, TOUCH_BUFFER_MS);
|
|
1738
|
-
};
|
|
1739
|
-
/**
|
|
1740
|
-
* Event listener for `focus` events on the window.
|
|
1741
|
-
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
1742
|
-
*/
|
|
1743
|
-
this._windowFocusListener = () => {
|
|
1744
|
-
// Make a note of when the window regains focus, so we can
|
|
1745
|
-
// restore the origin info for the focused element.
|
|
1746
|
-
this._windowFocused = true;
|
|
1747
|
-
this._windowFocusTimeoutId = setTimeout(() => this._windowFocused = false);
|
|
1748
|
-
};
|
|
1749
|
-
/**
|
|
1750
|
-
* Event listener for `focus` and 'blur' events on the document.
|
|
1751
|
-
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
1752
|
-
*/
|
|
1753
|
-
this._rootNodeFocusAndBlurListener = (event) => {
|
|
1754
|
-
const target = getTarget(event);
|
|
1755
|
-
const handler = event.type === 'focus' ? this._onFocus : this._onBlur;
|
|
1756
|
-
// We need to walk up the ancestor chain in order to support `checkChildren`.
|
|
1757
|
-
for (let element = target; element; element = element.parentElement) {
|
|
1758
|
-
handler.call(this, event, element);
|
|
1759
|
-
}
|
|
1760
|
-
};
|
|
1761
|
-
this._document = document;
|
|
1762
|
-
this._detectionMode = (options === null || options === void 0 ? void 0 : options.detectionMode) || 0 /* IMMEDIATE */;
|
|
1763
|
-
}
|
|
1764
|
-
monitor(element, checkChildren = false) {
|
|
1765
|
-
// Do nothing if we're not on the browser platform.
|
|
1766
|
-
if (!this._platform.isBrowser) {
|
|
1767
|
-
return of(null);
|
|
1768
|
-
}
|
|
1769
|
-
const nativeElement = coerceElement(element);
|
|
1770
|
-
// If the element is inside the shadow DOM, we need to bind our focus/blur listeners to
|
|
1771
|
-
// the shadow root, rather than the `document`, because the browser won't emit focus events
|
|
1772
|
-
// to the `document`, if focus is moving within the same shadow root.
|
|
1773
|
-
const rootNode = _getShadowRoot(nativeElement) || this._getDocument();
|
|
1774
|
-
const cachedInfo = this._elementInfo.get(nativeElement);
|
|
1775
|
-
// Check if we're already monitoring this element.
|
|
1776
|
-
if (cachedInfo) {
|
|
1777
|
-
if (checkChildren) {
|
|
1778
|
-
// TODO(COMP-318): this can be problematic, because it'll turn all non-checkChildren
|
|
1779
|
-
// observers into ones that behave as if `checkChildren` was turned on. We need a more
|
|
1780
|
-
// robust solution.
|
|
1781
|
-
cachedInfo.checkChildren = true;
|
|
1782
|
-
}
|
|
1783
|
-
return cachedInfo.subject.asObservable();
|
|
1784
|
-
}
|
|
1785
|
-
// Create monitored element info.
|
|
1786
|
-
const info = {
|
|
1787
|
-
checkChildren: checkChildren,
|
|
1788
|
-
subject: new Subject(),
|
|
1789
|
-
rootNode
|
|
1790
|
-
};
|
|
1791
|
-
this._elementInfo.set(nativeElement, info);
|
|
1792
|
-
this._registerGlobalListeners(info);
|
|
1793
|
-
return info.subject.asObservable();
|
|
1794
|
-
}
|
|
1795
|
-
stopMonitoring(element) {
|
|
1796
|
-
const nativeElement = coerceElement(element);
|
|
1797
|
-
const elementInfo = this._elementInfo.get(nativeElement);
|
|
1798
|
-
if (elementInfo) {
|
|
1799
|
-
elementInfo.subject.complete();
|
|
1800
|
-
this._setClasses(nativeElement);
|
|
1801
|
-
this._elementInfo.delete(nativeElement);
|
|
1802
|
-
this._removeGlobalListeners(elementInfo);
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
focusVia(element, origin, options) {
|
|
1806
|
-
const nativeElement = coerceElement(element);
|
|
1807
|
-
this._setOriginForCurrentEventQueue(origin);
|
|
1808
|
-
// `focus` isn't available on the server
|
|
1809
|
-
if (typeof nativeElement.focus === 'function') {
|
|
1810
|
-
// Cast the element to `any`, because the TS typings don't have the `options` parameter yet.
|
|
1811
|
-
nativeElement.focus(options);
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
ngOnDestroy() {
|
|
1815
|
-
this._elementInfo.forEach((_info, element) => this.stopMonitoring(element));
|
|
1816
|
-
}
|
|
1817
|
-
/** Access injected document if available or fallback to global document reference */
|
|
1818
|
-
_getDocument() {
|
|
1819
|
-
return this._document || document;
|
|
1820
|
-
}
|
|
1821
|
-
/** Use defaultView of injected document if available or fallback to global window reference */
|
|
1822
|
-
_getWindow() {
|
|
1823
|
-
const doc = this._getDocument();
|
|
1824
|
-
return doc.defaultView || window;
|
|
1825
|
-
}
|
|
1826
|
-
_toggleClass(element, className, shouldSet) {
|
|
1827
|
-
if (shouldSet) {
|
|
1828
|
-
element.classList.add(className);
|
|
1829
|
-
}
|
|
1830
|
-
else {
|
|
1831
|
-
element.classList.remove(className);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
_getFocusOrigin(event) {
|
|
1835
|
-
// If we couldn't detect a cause for the focus event, it's due to one of three reasons:
|
|
1836
|
-
// 1) The window has just regained focus, in which case we want to restore the focused state of
|
|
1837
|
-
// the element from before the window blurred.
|
|
1838
|
-
// 2) It was caused by a touch event, in which case we mark the origin as 'touch'.
|
|
1839
|
-
// 3) The element was programmatically focused, in which case we should mark the origin as
|
|
1840
|
-
// 'program'.
|
|
1841
|
-
if (this._origin) {
|
|
1842
|
-
return this._origin;
|
|
1843
|
-
}
|
|
1844
|
-
if (this._windowFocused && this._lastFocusOrigin) {
|
|
1845
|
-
return this._lastFocusOrigin;
|
|
1846
|
-
}
|
|
1847
|
-
else if (this._wasCausedByTouch(event)) {
|
|
1848
|
-
return 'touch';
|
|
1849
|
-
}
|
|
1850
|
-
else {
|
|
1851
|
-
return 'program';
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1717
|
+
class FocusMonitor {
|
|
1718
|
+
constructor(_ngZone, _platform,
|
|
1719
|
+
/** @breaking-change 11.0.0 make document required */
|
|
1720
|
+
document, options) {
|
|
1721
|
+
this._ngZone = _ngZone;
|
|
1722
|
+
this._platform = _platform;
|
|
1723
|
+
/** The focus origin that the next focus event is a result of. */
|
|
1724
|
+
this._origin = null;
|
|
1725
|
+
/** Whether the window has just been focused. */
|
|
1726
|
+
this._windowFocused = false;
|
|
1727
|
+
/** Map of elements being monitored to their info. */
|
|
1728
|
+
this._elementInfo = new Map();
|
|
1729
|
+
/** The number of elements currently being monitored. */
|
|
1730
|
+
this._monitoredElementCount = 0;
|
|
1854
1731
|
/**
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
1857
|
-
*
|
|
1732
|
+
* Keeps track of the root nodes to which we've currently bound a focus/blur handler,
|
|
1733
|
+
* as well as the number of monitored elements that they contain. We have to treat focus/blur
|
|
1734
|
+
* handlers differently from the rest of the events, because the browser won't emit events
|
|
1735
|
+
* to the document when focus moves inside of a shadow root.
|
|
1858
1736
|
*/
|
|
1859
|
-
|
|
1860
|
-
this._toggleClass(element, 'cdk-focused', !!origin);
|
|
1861
|
-
this._toggleClass(element, 'cdk-touch-focused', origin === 'touch');
|
|
1862
|
-
this._toggleClass(element, 'cdk-keyboard-focused', origin === 'keyboard');
|
|
1863
|
-
this._toggleClass(element, 'cdk-mouse-focused', origin === 'mouse');
|
|
1864
|
-
this._toggleClass(element, 'cdk-program-focused', origin === 'program');
|
|
1865
|
-
}
|
|
1737
|
+
this._rootNodeFocusListenerCount = new Map();
|
|
1866
1738
|
/**
|
|
1867
|
-
*
|
|
1868
|
-
*
|
|
1869
|
-
* @param origin The origin to set.
|
|
1739
|
+
* Event listener for `keydown` events on the document.
|
|
1740
|
+
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
1870
1741
|
*/
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
// tick after the interaction event fired. To ensure the focus origin is always correct,
|
|
1877
|
-
// the focus origin will be determined at the beginning of the next tick.
|
|
1878
|
-
this._originTimeoutId = setTimeout(() => this._origin = null, 1);
|
|
1879
|
-
}
|
|
1880
|
-
});
|
|
1881
|
-
}
|
|
1742
|
+
this._documentKeydownListener = () => {
|
|
1743
|
+
// On keydown record the origin and clear any touch event that may be in progress.
|
|
1744
|
+
this._lastTouchTarget = null;
|
|
1745
|
+
this._setOriginForCurrentEventQueue('keyboard');
|
|
1746
|
+
};
|
|
1882
1747
|
/**
|
|
1883
|
-
*
|
|
1884
|
-
*
|
|
1885
|
-
* @returns Whether the event was caused by a touch.
|
|
1748
|
+
* Event listener for `mousedown` events on the document.
|
|
1749
|
+
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
1886
1750
|
*/
|
|
1887
|
-
|
|
1888
|
-
//
|
|
1889
|
-
//
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
// apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
|
|
1898
|
-
// relatively small edge-case that can be worked around by using
|
|
1899
|
-
// focusVia(parentEl, 'program') to focus the parent element.
|
|
1900
|
-
//
|
|
1901
|
-
// If we decide that we absolutely must handle this case correctly, we can do so by listening
|
|
1902
|
-
// for the first focus event after the touchstart, and then the first blur event after that
|
|
1903
|
-
// focus event. When that blur event fires we know that whatever follows is not a result of the
|
|
1904
|
-
// touchstart.
|
|
1905
|
-
const focusTarget = getTarget(event);
|
|
1906
|
-
return this._lastTouchTarget instanceof Node && focusTarget instanceof Node &&
|
|
1907
|
-
(focusTarget === this._lastTouchTarget || focusTarget.contains(this._lastTouchTarget));
|
|
1908
|
-
}
|
|
1751
|
+
this._documentMousedownListener = (event) => {
|
|
1752
|
+
// On mousedown record the origin only if there is not touch
|
|
1753
|
+
// target, since a mousedown can happen as a result of a touch event.
|
|
1754
|
+
if (!this._lastTouchTarget) {
|
|
1755
|
+
// In some cases screen readers fire fake `mousedown` events instead of `keydown`.
|
|
1756
|
+
// Resolve the focus source to `keyboard` if we detect one of them.
|
|
1757
|
+
const source = isFakeMousedownFromScreenReader(event) ? 'keyboard' : 'mouse';
|
|
1758
|
+
this._setOriginForCurrentEventQueue(source);
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1909
1761
|
/**
|
|
1910
|
-
*
|
|
1911
|
-
*
|
|
1912
|
-
* @param element The monitored element.
|
|
1762
|
+
* Event listener for `touchstart` events on the document.
|
|
1763
|
+
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
1913
1764
|
*/
|
|
1914
|
-
|
|
1915
|
-
//
|
|
1916
|
-
//
|
|
1917
|
-
//
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
// monitored element itself.
|
|
1921
|
-
const elementInfo = this._elementInfo.get(element);
|
|
1922
|
-
if (!elementInfo || (!elementInfo.checkChildren && element !== getTarget(event))) {
|
|
1923
|
-
return;
|
|
1765
|
+
this._documentTouchstartListener = (event) => {
|
|
1766
|
+
// When the touchstart event fires the focus event is not yet in the event queue. This means
|
|
1767
|
+
// we can't rely on the trick used above (setting timeout of 1ms). Instead we wait 650ms to
|
|
1768
|
+
// see if a focus happens.
|
|
1769
|
+
if (this._touchTimeoutId != null) {
|
|
1770
|
+
clearTimeout(this._touchTimeoutId);
|
|
1924
1771
|
}
|
|
1925
|
-
|
|
1926
|
-
this.
|
|
1927
|
-
|
|
1928
|
-
this._lastFocusOrigin = origin;
|
|
1929
|
-
}
|
|
1772
|
+
this._lastTouchTarget = getTarget(event);
|
|
1773
|
+
this._touchTimeoutId = setTimeout(() => this._lastTouchTarget = null, TOUCH_BUFFER_MS);
|
|
1774
|
+
};
|
|
1930
1775
|
/**
|
|
1931
|
-
*
|
|
1932
|
-
*
|
|
1933
|
-
* @param element The monitored element.
|
|
1776
|
+
* Event listener for `focus` events on the window.
|
|
1777
|
+
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
1934
1778
|
*/
|
|
1935
|
-
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1779
|
+
this._windowFocusListener = () => {
|
|
1780
|
+
// Make a note of when the window regains focus, so we can
|
|
1781
|
+
// restore the origin info for the focused element.
|
|
1782
|
+
this._windowFocused = true;
|
|
1783
|
+
this._windowFocusTimeoutId = setTimeout(() => this._windowFocused = false);
|
|
1784
|
+
};
|
|
1785
|
+
/**
|
|
1786
|
+
* Event listener for `focus` and 'blur' events on the document.
|
|
1787
|
+
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
|
1788
|
+
*/
|
|
1789
|
+
this._rootNodeFocusAndBlurListener = (event) => {
|
|
1790
|
+
const target = getTarget(event);
|
|
1791
|
+
const handler = event.type === 'focus' ? this._onFocus : this._onBlur;
|
|
1792
|
+
// We need to walk up the ancestor chain in order to support `checkChildren`.
|
|
1793
|
+
for (let element = target; element; element = element.parentElement) {
|
|
1794
|
+
handler.call(this, event, element);
|
|
1795
|
+
}
|
|
1796
|
+
};
|
|
1797
|
+
this._document = document;
|
|
1798
|
+
this._detectionMode = (options === null || options === void 0 ? void 0 : options.detectionMode) || 0 /* IMMEDIATE */;
|
|
1799
|
+
}
|
|
1800
|
+
monitor(element, checkChildren = false) {
|
|
1801
|
+
// Do nothing if we're not on the browser platform.
|
|
1802
|
+
if (!this._platform.isBrowser) {
|
|
1803
|
+
return of(null);
|
|
1804
|
+
}
|
|
1805
|
+
const nativeElement = coerceElement(element);
|
|
1806
|
+
// If the element is inside the shadow DOM, we need to bind our focus/blur listeners to
|
|
1807
|
+
// the shadow root, rather than the `document`, because the browser won't emit focus events
|
|
1808
|
+
// to the `document`, if focus is moving within the same shadow root.
|
|
1809
|
+
const rootNode = _getShadowRoot(nativeElement) || this._getDocument();
|
|
1810
|
+
const cachedInfo = this._elementInfo.get(nativeElement);
|
|
1811
|
+
// Check if we're already monitoring this element.
|
|
1812
|
+
if (cachedInfo) {
|
|
1813
|
+
if (checkChildren) {
|
|
1814
|
+
// TODO(COMP-318): this can be problematic, because it'll turn all non-checkChildren
|
|
1815
|
+
// observers into ones that behave as if `checkChildren` was turned on. We need a more
|
|
1816
|
+
// robust solution.
|
|
1817
|
+
cachedInfo.checkChildren = true;
|
|
1818
|
+
}
|
|
1819
|
+
return cachedInfo.subject.asObservable();
|
|
1820
|
+
}
|
|
1821
|
+
// Create monitored element info.
|
|
1822
|
+
const info = {
|
|
1823
|
+
checkChildren: checkChildren,
|
|
1824
|
+
subject: new Subject(),
|
|
1825
|
+
rootNode
|
|
1826
|
+
};
|
|
1827
|
+
this._elementInfo.set(nativeElement, info);
|
|
1828
|
+
this._registerGlobalListeners(info);
|
|
1829
|
+
return info.subject.asObservable();
|
|
1830
|
+
}
|
|
1831
|
+
stopMonitoring(element) {
|
|
1832
|
+
const nativeElement = coerceElement(element);
|
|
1833
|
+
const elementInfo = this._elementInfo.get(nativeElement);
|
|
1834
|
+
if (elementInfo) {
|
|
1835
|
+
elementInfo.subject.complete();
|
|
1836
|
+
this._setClasses(nativeElement);
|
|
1837
|
+
this._elementInfo.delete(nativeElement);
|
|
1838
|
+
this._removeGlobalListeners(elementInfo);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
focusVia(element, origin, options) {
|
|
1842
|
+
const nativeElement = coerceElement(element);
|
|
1843
|
+
this._setOriginForCurrentEventQueue(origin);
|
|
1844
|
+
// `focus` isn't available on the server
|
|
1845
|
+
if (typeof nativeElement.focus === 'function') {
|
|
1846
|
+
// Cast the element to `any`, because the TS typings don't have the `options` parameter yet.
|
|
1847
|
+
nativeElement.focus(options);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
ngOnDestroy() {
|
|
1851
|
+
this._elementInfo.forEach((_info, element) => this.stopMonitoring(element));
|
|
1852
|
+
}
|
|
1853
|
+
/** Access injected document if available or fallback to global document reference */
|
|
1854
|
+
_getDocument() {
|
|
1855
|
+
return this._document || document;
|
|
1856
|
+
}
|
|
1857
|
+
/** Use defaultView of injected document if available or fallback to global window reference */
|
|
1858
|
+
_getWindow() {
|
|
1859
|
+
const doc = this._getDocument();
|
|
1860
|
+
return doc.defaultView || window;
|
|
1861
|
+
}
|
|
1862
|
+
_toggleClass(element, className, shouldSet) {
|
|
1863
|
+
if (shouldSet) {
|
|
1864
|
+
element.classList.add(className);
|
|
1945
1865
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
1866
|
+
else {
|
|
1867
|
+
element.classList.remove(className);
|
|
1948
1868
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners + 1);
|
|
1962
|
-
// Register global listeners when first element is monitored.
|
|
1963
|
-
if (++this._monitoredElementCount === 1) {
|
|
1964
|
-
// Note: we listen to events in the capture phase so we
|
|
1965
|
-
// can detect them even if the user stops propagation.
|
|
1966
|
-
this._ngZone.runOutsideAngular(() => {
|
|
1967
|
-
const document = this._getDocument();
|
|
1968
|
-
const window = this._getWindow();
|
|
1969
|
-
document.addEventListener('keydown', this._documentKeydownListener, captureEventListenerOptions);
|
|
1970
|
-
document.addEventListener('mousedown', this._documentMousedownListener, captureEventListenerOptions);
|
|
1971
|
-
document.addEventListener('touchstart', this._documentTouchstartListener, captureEventListenerOptions);
|
|
1972
|
-
window.addEventListener('focus', this._windowFocusListener);
|
|
1973
|
-
});
|
|
1974
|
-
}
|
|
1869
|
+
}
|
|
1870
|
+
_getFocusOrigin(event) {
|
|
1871
|
+
// If we couldn't detect a cause for the focus event, it's due to one of three reasons:
|
|
1872
|
+
// 1) The window has just regained focus, in which case we want to restore the focused state of
|
|
1873
|
+
// the element from before the window blurred.
|
|
1874
|
+
// 2) It was caused by a touch event, in which case we mark the origin as 'touch'.
|
|
1875
|
+
// 3) The element was programmatically focused, in which case we should mark the origin as
|
|
1876
|
+
// 'program'.
|
|
1877
|
+
if (this._origin) {
|
|
1878
|
+
return this._origin;
|
|
1975
1879
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1880
|
+
if (this._windowFocused && this._lastFocusOrigin) {
|
|
1881
|
+
return this._lastFocusOrigin;
|
|
1882
|
+
}
|
|
1883
|
+
else if (this._wasCausedByTouch(event)) {
|
|
1884
|
+
return 'touch';
|
|
1885
|
+
}
|
|
1886
|
+
else {
|
|
1887
|
+
return 'program';
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Sets the focus classes on the element based on the given focus origin.
|
|
1892
|
+
* @param element The element to update the classes on.
|
|
1893
|
+
* @param origin The focus origin.
|
|
1894
|
+
*/
|
|
1895
|
+
_setClasses(element, origin) {
|
|
1896
|
+
this._toggleClass(element, 'cdk-focused', !!origin);
|
|
1897
|
+
this._toggleClass(element, 'cdk-touch-focused', origin === 'touch');
|
|
1898
|
+
this._toggleClass(element, 'cdk-keyboard-focused', origin === 'keyboard');
|
|
1899
|
+
this._toggleClass(element, 'cdk-mouse-focused', origin === 'mouse');
|
|
1900
|
+
this._toggleClass(element, 'cdk-program-focused', origin === 'program');
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Sets the origin and schedules an async function to clear it at the end of the event queue.
|
|
1904
|
+
* If the detection mode is 'eventual', the origin is never cleared.
|
|
1905
|
+
* @param origin The origin to set.
|
|
1906
|
+
*/
|
|
1907
|
+
_setOriginForCurrentEventQueue(origin) {
|
|
1908
|
+
this._ngZone.runOutsideAngular(() => {
|
|
1909
|
+
this._origin = origin;
|
|
1910
|
+
if (this._detectionMode === 0 /* IMMEDIATE */) {
|
|
1911
|
+
// Sometimes the focus origin won't be valid in Firefox because Firefox seems to focus *one*
|
|
1912
|
+
// tick after the interaction event fired. To ensure the focus origin is always correct,
|
|
1913
|
+
// the focus origin will be determined at the beginning of the next tick.
|
|
1914
|
+
this._originTimeoutId = setTimeout(() => this._origin = null, 1);
|
|
1988
1915
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Checks whether the given focus event was caused by a touchstart event.
|
|
1920
|
+
* @param event The focus event to check.
|
|
1921
|
+
* @returns Whether the event was caused by a touch.
|
|
1922
|
+
*/
|
|
1923
|
+
_wasCausedByTouch(event) {
|
|
1924
|
+
// Note(mmalerba): This implementation is not quite perfect, there is a small edge case.
|
|
1925
|
+
// Consider the following dom structure:
|
|
1926
|
+
//
|
|
1927
|
+
// <div #parent tabindex="0" cdkFocusClasses>
|
|
1928
|
+
// <div #child (click)="#parent.focus()"></div>
|
|
1929
|
+
// </div>
|
|
1930
|
+
//
|
|
1931
|
+
// If the user touches the #child element and the #parent is programmatically focused as a
|
|
1932
|
+
// result, this code will still consider it to have been caused by the touch event and will
|
|
1933
|
+
// apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
|
|
1934
|
+
// relatively small edge-case that can be worked around by using
|
|
1935
|
+
// focusVia(parentEl, 'program') to focus the parent element.
|
|
1936
|
+
//
|
|
1937
|
+
// If we decide that we absolutely must handle this case correctly, we can do so by listening
|
|
1938
|
+
// for the first focus event after the touchstart, and then the first blur event after that
|
|
1939
|
+
// focus event. When that blur event fires we know that whatever follows is not a result of the
|
|
1940
|
+
// touchstart.
|
|
1941
|
+
const focusTarget = getTarget(event);
|
|
1942
|
+
return this._lastTouchTarget instanceof Node && focusTarget instanceof Node &&
|
|
1943
|
+
(focusTarget === this._lastTouchTarget || focusTarget.contains(this._lastTouchTarget));
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Handles focus events on a registered element.
|
|
1947
|
+
* @param event The focus event.
|
|
1948
|
+
* @param element The monitored element.
|
|
1949
|
+
*/
|
|
1950
|
+
_onFocus(event, element) {
|
|
1951
|
+
// NOTE(mmalerba): We currently set the classes based on the focus origin of the most recent
|
|
1952
|
+
// focus event affecting the monitored element. If we want to use the origin of the first event
|
|
1953
|
+
// instead we should check for the cdk-focused class here and return if the element already has
|
|
1954
|
+
// it. (This only matters for elements that have includesChildren = true).
|
|
1955
|
+
// If we are not counting child-element-focus as focused, make sure that the event target is the
|
|
1956
|
+
// monitored element itself.
|
|
1957
|
+
const elementInfo = this._elementInfo.get(element);
|
|
1958
|
+
if (!elementInfo || (!elementInfo.checkChildren && element !== getTarget(event))) {
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
const origin = this._getFocusOrigin(event);
|
|
1962
|
+
this._setClasses(element, origin);
|
|
1963
|
+
this._emitOrigin(elementInfo.subject, origin);
|
|
1964
|
+
this._lastFocusOrigin = origin;
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Handles blur events on a registered element.
|
|
1968
|
+
* @param event The blur event.
|
|
1969
|
+
* @param element The monitored element.
|
|
1970
|
+
*/
|
|
1971
|
+
_onBlur(event, element) {
|
|
1972
|
+
// If we are counting child-element-focus as focused, make sure that we aren't just blurring in
|
|
1973
|
+
// order to focus another child of the monitored element.
|
|
1974
|
+
const elementInfo = this._elementInfo.get(element);
|
|
1975
|
+
if (!elementInfo || (elementInfo.checkChildren && event.relatedTarget instanceof Node &&
|
|
1976
|
+
element.contains(event.relatedTarget))) {
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
this._setClasses(element);
|
|
1980
|
+
this._emitOrigin(elementInfo.subject, null);
|
|
1981
|
+
}
|
|
1982
|
+
_emitOrigin(subject, origin) {
|
|
1983
|
+
this._ngZone.run(() => subject.next(origin));
|
|
1984
|
+
}
|
|
1985
|
+
_registerGlobalListeners(elementInfo) {
|
|
1986
|
+
if (!this._platform.isBrowser) {
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
const rootNode = elementInfo.rootNode;
|
|
1990
|
+
const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode) || 0;
|
|
1991
|
+
if (!rootNodeFocusListeners) {
|
|
1992
|
+
this._ngZone.runOutsideAngular(() => {
|
|
1993
|
+
rootNode.addEventListener('focus', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
|
|
1994
|
+
rootNode.addEventListener('blur', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners + 1);
|
|
1998
|
+
// Register global listeners when first element is monitored.
|
|
1999
|
+
if (++this._monitoredElementCount === 1) {
|
|
2000
|
+
// Note: we listen to events in the capture phase so we
|
|
2001
|
+
// can detect them even if the user stops propagation.
|
|
2002
|
+
this._ngZone.runOutsideAngular(() => {
|
|
1991
2003
|
const document = this._getDocument();
|
|
1992
2004
|
const window = this._getWindow();
|
|
1993
|
-
document.
|
|
1994
|
-
document.
|
|
1995
|
-
document.
|
|
1996
|
-
window.
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2005
|
+
document.addEventListener('keydown', this._documentKeydownListener, captureEventListenerOptions);
|
|
2006
|
+
document.addEventListener('mousedown', this._documentMousedownListener, captureEventListenerOptions);
|
|
2007
|
+
document.addEventListener('touchstart', this._documentTouchstartListener, captureEventListenerOptions);
|
|
2008
|
+
window.addEventListener('focus', this._windowFocusListener);
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
_removeGlobalListeners(elementInfo) {
|
|
2013
|
+
const rootNode = elementInfo.rootNode;
|
|
2014
|
+
if (this._rootNodeFocusListenerCount.has(rootNode)) {
|
|
2015
|
+
const rootNodeFocusListeners = this._rootNodeFocusListenerCount.get(rootNode);
|
|
2016
|
+
if (rootNodeFocusListeners > 1) {
|
|
2017
|
+
this._rootNodeFocusListenerCount.set(rootNode, rootNodeFocusListeners - 1);
|
|
2001
2018
|
}
|
|
2019
|
+
else {
|
|
2020
|
+
rootNode.removeEventListener('focus', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
|
|
2021
|
+
rootNode.removeEventListener('blur', this._rootNodeFocusAndBlurListener, captureEventListenerOptions);
|
|
2022
|
+
this._rootNodeFocusListenerCount.delete(rootNode);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
// Unregister global listeners when last element is unmonitored.
|
|
2026
|
+
if (!--this._monitoredElementCount) {
|
|
2027
|
+
const document = this._getDocument();
|
|
2028
|
+
const window = this._getWindow();
|
|
2029
|
+
document.removeEventListener('keydown', this._documentKeydownListener, captureEventListenerOptions);
|
|
2030
|
+
document.removeEventListener('mousedown', this._documentMousedownListener, captureEventListenerOptions);
|
|
2031
|
+
document.removeEventListener('touchstart', this._documentTouchstartListener, captureEventListenerOptions);
|
|
2032
|
+
window.removeEventListener('focus', this._windowFocusListener);
|
|
2033
|
+
// Clear timeouts for all potentially pending timeouts to prevent the leaks.
|
|
2034
|
+
clearTimeout(this._windowFocusTimeoutId);
|
|
2035
|
+
clearTimeout(this._touchTimeoutId);
|
|
2036
|
+
clearTimeout(this._originTimeoutId);
|
|
2002
2037
|
}
|
|
2003
2038
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
]
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
]
|
|
2014
|
-
|
|
2015
|
-
})();
|
|
2039
|
+
}
|
|
2040
|
+
FocusMonitor.ɵprov = ɵɵdefineInjectable({ factory: function FocusMonitor_Factory() { return new FocusMonitor(ɵɵinject(NgZone), ɵɵinject(Platform), ɵɵinject(DOCUMENT, 8), ɵɵinject(FOCUS_MONITOR_DEFAULT_OPTIONS, 8)); }, token: FocusMonitor, providedIn: "root" });
|
|
2041
|
+
FocusMonitor.decorators = [
|
|
2042
|
+
{ type: Injectable, args: [{ providedIn: 'root' },] }
|
|
2043
|
+
];
|
|
2044
|
+
FocusMonitor.ctorParameters = () => [
|
|
2045
|
+
{ type: NgZone },
|
|
2046
|
+
{ type: Platform },
|
|
2047
|
+
{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [DOCUMENT,] }] },
|
|
2048
|
+
{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [FOCUS_MONITOR_DEFAULT_OPTIONS,] }] }
|
|
2049
|
+
];
|
|
2016
2050
|
/** Gets the target of an event, accounting for Shadow DOM. */
|
|
2017
2051
|
function getTarget(event) {
|
|
2018
2052
|
// If an event is bound outside the Shadow DOM, the `event.target` will
|
|
@@ -2028,38 +2062,35 @@ function getTarget(event) {
|
|
|
2028
2062
|
* focused.
|
|
2029
2063
|
* 2) cdkMonitorSubtreeFocus: considers an element focused if it or any of its children are focused.
|
|
2030
2064
|
*/
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
this._monitorSubscription.unsubscribe();
|
|
2046
|
-
}
|
|
2065
|
+
class CdkMonitorFocus {
|
|
2066
|
+
constructor(_elementRef, _focusMonitor) {
|
|
2067
|
+
this._elementRef = _elementRef;
|
|
2068
|
+
this._focusMonitor = _focusMonitor;
|
|
2069
|
+
this.cdkFocusChange = new EventEmitter();
|
|
2070
|
+
}
|
|
2071
|
+
ngAfterViewInit() {
|
|
2072
|
+
this._monitorSubscription = this._focusMonitor.monitor(this._elementRef, this._elementRef.nativeElement.hasAttribute('cdkMonitorSubtreeFocus'))
|
|
2073
|
+
.subscribe(origin => this.cdkFocusChange.emit(origin));
|
|
2074
|
+
}
|
|
2075
|
+
ngOnDestroy() {
|
|
2076
|
+
this._focusMonitor.stopMonitoring(this._elementRef);
|
|
2077
|
+
if (this._monitorSubscription) {
|
|
2078
|
+
this._monitorSubscription.unsubscribe();
|
|
2047
2079
|
}
|
|
2048
2080
|
}
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
})();
|
|
2081
|
+
}
|
|
2082
|
+
CdkMonitorFocus.decorators = [
|
|
2083
|
+
{ type: Directive, args: [{
|
|
2084
|
+
selector: '[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]',
|
|
2085
|
+
},] }
|
|
2086
|
+
];
|
|
2087
|
+
CdkMonitorFocus.ctorParameters = () => [
|
|
2088
|
+
{ type: ElementRef },
|
|
2089
|
+
{ type: FocusMonitor }
|
|
2090
|
+
];
|
|
2091
|
+
CdkMonitorFocus.propDecorators = {
|
|
2092
|
+
cdkFocusChange: [{ type: Output }]
|
|
2093
|
+
};
|
|
2063
2094
|
|
|
2064
2095
|
/**
|
|
2065
2096
|
* @license
|
|
@@ -2085,69 +2116,66 @@ const HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS = 'cdk-high-contrast-active';
|
|
|
2085
2116
|
* Mode. This service does not detect high-contrast mode as added by the Chrome "High Contrast"
|
|
2086
2117
|
* browser extension.
|
|
2087
2118
|
*/
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
if (!this._platform.isBrowser) {
|
|
2097
|
-
return 0 /* NONE */;
|
|
2098
|
-
}
|
|
2099
|
-
// Create a test element with an arbitrary background-color that is neither black nor
|
|
2100
|
-
// white; high-contrast mode will coerce the color to either black or white. Also ensure that
|
|
2101
|
-
// appending the test element to the DOM does not affect layout by absolutely positioning it
|
|
2102
|
-
const testElement = this._document.createElement('div');
|
|
2103
|
-
testElement.style.backgroundColor = 'rgb(1,2,3)';
|
|
2104
|
-
testElement.style.position = 'absolute';
|
|
2105
|
-
this._document.body.appendChild(testElement);
|
|
2106
|
-
// Get the computed style for the background color, collapsing spaces to normalize between
|
|
2107
|
-
// browsers. Once we get this color, we no longer need the test element. Access the `window`
|
|
2108
|
-
// via the document so we can fake it in tests. Note that we have extra null checks, because
|
|
2109
|
-
// this logic will likely run during app bootstrap and throwing can break the entire app.
|
|
2110
|
-
const documentWindow = this._document.defaultView || window;
|
|
2111
|
-
const computedStyle = (documentWindow && documentWindow.getComputedStyle) ?
|
|
2112
|
-
documentWindow.getComputedStyle(testElement) : null;
|
|
2113
|
-
const computedColor = (computedStyle && computedStyle.backgroundColor || '').replace(/ /g, '');
|
|
2114
|
-
this._document.body.removeChild(testElement);
|
|
2115
|
-
switch (computedColor) {
|
|
2116
|
-
case 'rgb(0,0,0)': return 2 /* WHITE_ON_BLACK */;
|
|
2117
|
-
case 'rgb(255,255,255)': return 1 /* BLACK_ON_WHITE */;
|
|
2118
|
-
}
|
|
2119
|
+
class HighContrastModeDetector {
|
|
2120
|
+
constructor(_platform, document) {
|
|
2121
|
+
this._platform = _platform;
|
|
2122
|
+
this._document = document;
|
|
2123
|
+
}
|
|
2124
|
+
/** Gets the current high-contrast-mode for the page. */
|
|
2125
|
+
getHighContrastMode() {
|
|
2126
|
+
if (!this._platform.isBrowser) {
|
|
2119
2127
|
return 0 /* NONE */;
|
|
2120
2128
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2129
|
+
// Create a test element with an arbitrary background-color that is neither black nor
|
|
2130
|
+
// white; high-contrast mode will coerce the color to either black or white. Also ensure that
|
|
2131
|
+
// appending the test element to the DOM does not affect layout by absolutely positioning it
|
|
2132
|
+
const testElement = this._document.createElement('div');
|
|
2133
|
+
testElement.style.backgroundColor = 'rgb(1,2,3)';
|
|
2134
|
+
testElement.style.position = 'absolute';
|
|
2135
|
+
this._document.body.appendChild(testElement);
|
|
2136
|
+
// Get the computed style for the background color, collapsing spaces to normalize between
|
|
2137
|
+
// browsers. Once we get this color, we no longer need the test element. Access the `window`
|
|
2138
|
+
// via the document so we can fake it in tests. Note that we have extra null checks, because
|
|
2139
|
+
// this logic will likely run during app bootstrap and throwing can break the entire app.
|
|
2140
|
+
const documentWindow = this._document.defaultView || window;
|
|
2141
|
+
const computedStyle = (documentWindow && documentWindow.getComputedStyle) ?
|
|
2142
|
+
documentWindow.getComputedStyle(testElement) : null;
|
|
2143
|
+
const computedColor = (computedStyle && computedStyle.backgroundColor || '').replace(/ /g, '');
|
|
2144
|
+
this._document.body.removeChild(testElement);
|
|
2145
|
+
switch (computedColor) {
|
|
2146
|
+
case 'rgb(0,0,0)': return 2 /* WHITE_ON_BLACK */;
|
|
2147
|
+
case 'rgb(255,255,255)': return 1 /* BLACK_ON_WHITE */;
|
|
2148
|
+
}
|
|
2149
|
+
return 0 /* NONE */;
|
|
2150
|
+
}
|
|
2151
|
+
/** Applies CSS classes indicating high-contrast mode to document body (browser-only). */
|
|
2152
|
+
_applyBodyHighContrastModeCssClasses() {
|
|
2153
|
+
if (this._platform.isBrowser && this._document.body) {
|
|
2154
|
+
const bodyClasses = this._document.body.classList;
|
|
2155
|
+
// IE11 doesn't support `classList` operations with multiple arguments
|
|
2156
|
+
bodyClasses.remove(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS);
|
|
2157
|
+
bodyClasses.remove(BLACK_ON_WHITE_CSS_CLASS);
|
|
2158
|
+
bodyClasses.remove(WHITE_ON_BLACK_CSS_CLASS);
|
|
2159
|
+
const mode = this.getHighContrastMode();
|
|
2160
|
+
if (mode === 1 /* BLACK_ON_WHITE */) {
|
|
2161
|
+
bodyClasses.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS);
|
|
2162
|
+
bodyClasses.add(BLACK_ON_WHITE_CSS_CLASS);
|
|
2163
|
+
}
|
|
2164
|
+
else if (mode === 2 /* WHITE_ON_BLACK */) {
|
|
2165
|
+
bodyClasses.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS);
|
|
2166
|
+
bodyClasses.add(WHITE_ON_BLACK_CSS_CLASS);
|
|
2138
2167
|
}
|
|
2139
2168
|
}
|
|
2140
2169
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
]
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
]
|
|
2149
|
-
|
|
2150
|
-
})();
|
|
2170
|
+
}
|
|
2171
|
+
HighContrastModeDetector.ɵprov = ɵɵdefineInjectable({ factory: function HighContrastModeDetector_Factory() { return new HighContrastModeDetector(ɵɵinject(Platform), ɵɵinject(DOCUMENT)); }, token: HighContrastModeDetector, providedIn: "root" });
|
|
2172
|
+
HighContrastModeDetector.decorators = [
|
|
2173
|
+
{ type: Injectable, args: [{ providedIn: 'root' },] }
|
|
2174
|
+
];
|
|
2175
|
+
HighContrastModeDetector.ctorParameters = () => [
|
|
2176
|
+
{ type: Platform },
|
|
2177
|
+
{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }
|
|
2178
|
+
];
|
|
2151
2179
|
|
|
2152
2180
|
/**
|
|
2153
2181
|
* @license
|
|
@@ -2156,24 +2184,21 @@ let HighContrastModeDetector = /** @class */ (() => {
|
|
|
2156
2184
|
* Use of this source code is governed by an MIT-style license that can be
|
|
2157
2185
|
* found in the LICENSE file at https://angular.io/license
|
|
2158
2186
|
*/
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
];
|
|
2175
|
-
return A11yModule;
|
|
2176
|
-
})();
|
|
2187
|
+
class A11yModule {
|
|
2188
|
+
constructor(highContrastModeDetector) {
|
|
2189
|
+
highContrastModeDetector._applyBodyHighContrastModeCssClasses();
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
A11yModule.decorators = [
|
|
2193
|
+
{ type: NgModule, args: [{
|
|
2194
|
+
imports: [PlatformModule, ObserversModule],
|
|
2195
|
+
declarations: [CdkAriaLive, CdkTrapFocus, CdkMonitorFocus],
|
|
2196
|
+
exports: [CdkAriaLive, CdkTrapFocus, CdkMonitorFocus],
|
|
2197
|
+
},] }
|
|
2198
|
+
];
|
|
2199
|
+
A11yModule.ctorParameters = () => [
|
|
2200
|
+
{ type: HighContrastModeDetector }
|
|
2201
|
+
];
|
|
2177
2202
|
|
|
2178
2203
|
/**
|
|
2179
2204
|
* @license
|
|
@@ -2187,5 +2212,5 @@ let A11yModule = /** @class */ (() => {
|
|
|
2187
2212
|
* Generated bundle index. Do not edit.
|
|
2188
2213
|
*/
|
|
2189
2214
|
|
|
2190
|
-
export { A11yModule, ActiveDescendantKeyManager, AriaDescriber, CDK_DESCRIBEDBY_HOST_ATTRIBUTE, CDK_DESCRIBEDBY_ID_PREFIX, CdkAriaLive, CdkMonitorFocus, CdkTrapFocus, ConfigurableFocusTrap, ConfigurableFocusTrapFactory, EventListenerFocusTrapInertStrategy, FOCUS_MONITOR_DEFAULT_OPTIONS, FOCUS_TRAP_INERT_STRATEGY, FocusKeyManager, FocusMonitor, FocusTrap, FocusTrapFactory, HighContrastModeDetector, InteractivityChecker, LIVE_ANNOUNCER_DEFAULT_OPTIONS, LIVE_ANNOUNCER_ELEMENT_TOKEN, LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY, ListKeyManager, LiveAnnouncer, MESSAGES_CONTAINER_ID, TOUCH_BUFFER_MS, isFakeMousedownFromScreenReader, FocusTrapManager as ɵangular_material_src_cdk_a11y_a11y_a, ConfigurableFocusTrapConfig as ɵangular_material_src_cdk_a11y_a11y_b };
|
|
2215
|
+
export { A11yModule, ActiveDescendantKeyManager, AriaDescriber, CDK_DESCRIBEDBY_HOST_ATTRIBUTE, CDK_DESCRIBEDBY_ID_PREFIX, CdkAriaLive, CdkMonitorFocus, CdkTrapFocus, ConfigurableFocusTrap, ConfigurableFocusTrapFactory, EventListenerFocusTrapInertStrategy, FOCUS_MONITOR_DEFAULT_OPTIONS, FOCUS_TRAP_INERT_STRATEGY, FocusKeyManager, FocusMonitor, FocusTrap, FocusTrapFactory, HighContrastModeDetector, InteractivityChecker, IsFocusableConfig, LIVE_ANNOUNCER_DEFAULT_OPTIONS, LIVE_ANNOUNCER_ELEMENT_TOKEN, LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY, ListKeyManager, LiveAnnouncer, MESSAGES_CONTAINER_ID, TOUCH_BUFFER_MS, isFakeMousedownFromScreenReader, FocusTrapManager as ɵangular_material_src_cdk_a11y_a11y_a, ConfigurableFocusTrapConfig as ɵangular_material_src_cdk_a11y_a11y_b };
|
|
2191
2216
|
//# sourceMappingURL=a11y.js.map
|