@angular/cdk 2.0.0-beta.11 → 2.0.0-beta.12
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.scss +23 -0
- package/_overlay.scss +93 -0
- package/a11y/index.metadata.json +2 -1
- package/a11y/typings/a11y-module.d.ts +2 -0
- package/a11y/typings/focus-monitor.d.ts +1 -1
- package/a11y/typings/index.d.ts +1 -1
- package/a11y/typings/index.metadata.json +1 -1
- package/a11y/typings/{public_api.d.ts → public-api.d.ts} +8 -2
- package/a11y-prebuilt.css +1 -0
- package/a11y.metadata.json +2 -1
- package/bidi/index.metadata.json +2 -1
- package/bidi/typings/bidi-module.d.ts +2 -0
- package/bidi/typings/index.d.ts +1 -1
- package/bidi/typings/index.metadata.json +1 -1
- package/bidi/typings/public-api.d.ts +10 -0
- package/bidi.metadata.json +2 -1
- package/bundles/cdk-a11y.umd.js +1368 -1357
- package/bundles/cdk-a11y.umd.js.map +1 -1
- package/bundles/cdk-a11y.umd.min.js +2 -2
- package/bundles/cdk-a11y.umd.min.js.map +1 -1
- package/bundles/cdk-bidi.umd.js +42 -40
- package/bundles/cdk-bidi.umd.js.map +1 -1
- package/bundles/cdk-bidi.umd.min.js +2 -2
- package/bundles/cdk-bidi.umd.min.js.map +1 -1
- package/bundles/cdk-coercion.umd.js +12 -0
- package/bundles/cdk-coercion.umd.js.map +1 -1
- package/bundles/cdk-coercion.umd.min.js +2 -2
- package/bundles/cdk-coercion.umd.min.js.map +1 -1
- package/bundles/cdk-collections.umd.js +113 -11
- package/bundles/cdk-collections.umd.js.map +1 -1
- package/bundles/cdk-collections.umd.min.js +2 -2
- package/bundles/cdk-collections.umd.min.js.map +1 -1
- package/bundles/cdk-keycodes.umd.js.map +1 -1
- package/bundles/cdk-keycodes.umd.min.js +1 -1
- package/bundles/cdk-keycodes.umd.min.js.map +1 -1
- package/bundles/cdk-layout.umd.js +235 -0
- package/bundles/cdk-layout.umd.js.map +1 -0
- package/bundles/cdk-layout.umd.min.js +9 -0
- package/bundles/cdk-layout.umd.min.js.map +1 -0
- package/bundles/cdk-observers.umd.js +41 -40
- package/bundles/cdk-observers.umd.js.map +1 -1
- package/bundles/cdk-observers.umd.min.js +2 -2
- package/bundles/cdk-observers.umd.min.js.map +1 -1
- package/bundles/cdk-overlay.umd.js +306 -265
- package/bundles/cdk-overlay.umd.js.map +1 -1
- package/bundles/cdk-overlay.umd.min.js +2 -2
- package/bundles/cdk-overlay.umd.min.js.map +1 -1
- package/bundles/cdk-platform.umd.js +19 -17
- 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-portal.umd.js +71 -37
- package/bundles/cdk-portal.umd.js.map +1 -1
- package/bundles/cdk-portal.umd.min.js +2 -2
- package/bundles/cdk-portal.umd.min.js.map +1 -1
- package/bundles/cdk-rxjs.umd.js +13 -4
- package/bundles/cdk-rxjs.umd.js.map +1 -1
- package/bundles/cdk-rxjs.umd.min.js +2 -2
- package/bundles/cdk-rxjs.umd.min.js.map +1 -1
- package/bundles/cdk-scrolling.umd.js +89 -54
- package/bundles/cdk-scrolling.umd.js.map +1 -1
- package/bundles/cdk-scrolling.umd.min.js +2 -2
- package/bundles/cdk-scrolling.umd.min.js.map +1 -1
- package/bundles/cdk-stepper.umd.js +115 -90
- package/bundles/cdk-stepper.umd.js.map +1 -1
- package/bundles/cdk-stepper.umd.min.js +2 -2
- package/bundles/cdk-stepper.umd.min.js.map +1 -1
- package/bundles/cdk-table.umd.js +261 -218
- package/bundles/cdk-table.umd.js.map +1 -1
- package/bundles/cdk-table.umd.min.js +2 -2
- package/bundles/cdk-table.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 +2 -2
- package/bundles/cdk.umd.min.js.map +1 -1
- package/cdk.metadata.json +2 -1
- package/coercion/index.metadata.json +2 -1
- package/coercion/typings/array.d.ts +9 -0
- package/coercion/typings/index.d.ts +1 -1
- package/coercion/typings/index.metadata.json +1 -1
- package/{typings/coercion/public_api.d.ts → coercion/typings/public-api.d.ts} +1 -0
- package/coercion.metadata.json +2 -1
- package/collections/index.metadata.json +2 -1
- package/collections/typings/index.d.ts +2 -1
- package/collections/typings/index.metadata.json +1 -1
- package/{typings/collections/public_api.d.ts → collections/typings/public-api.d.ts} +1 -0
- package/collections/typings/selection.d.ts +13 -3
- package/collections/typings/unique-selection-dispatcher.d.ts +40 -0
- package/collections.metadata.json +2 -1
- package/esm2015/a11y.js +1252 -1250
- package/esm2015/a11y.js.map +1 -1
- package/esm2015/bidi.js +1 -1
- package/esm2015/bidi.js.map +1 -1
- package/esm2015/cdk.js +1 -1
- package/esm2015/cdk.js.map +1 -1
- package/esm2015/coercion.js +11 -1
- package/esm2015/coercion.js.map +1 -1
- package/esm2015/collections.js +93 -8
- package/esm2015/collections.js.map +1 -1
- package/esm2015/keycodes.js.map +1 -1
- package/esm2015/layout.js +226 -0
- package/esm2015/layout.js.map +1 -0
- package/esm2015/observers.js +8 -7
- package/esm2015/observers.js.map +1 -1
- package/esm2015/overlay.js +157 -136
- package/esm2015/overlay.js.map +1 -1
- package/esm2015/platform.js +1 -1
- package/esm2015/platform.js.map +1 -1
- package/esm2015/portal.js +30 -1
- package/esm2015/portal.js.map +1 -1
- package/esm2015/rxjs.js +5 -1
- package/esm2015/rxjs.js.map +1 -1
- package/esm2015/scrolling.js +39 -8
- package/esm2015/scrolling.js.map +1 -1
- package/esm2015/stepper.js +27 -5
- package/esm2015/stepper.js.map +1 -1
- package/esm2015/table.js +68 -29
- package/esm2015/table.js.map +1 -1
- package/esm5/a11y.es5.js +1372 -1357
- package/esm5/a11y.es5.js.map +1 -1
- package/esm5/bidi.es5.js +45 -40
- package/esm5/bidi.es5.js.map +1 -1
- package/esm5/cdk.es5.js +4 -1
- package/esm5/cdk.es5.js.map +1 -1
- package/esm5/coercion.es5.js +14 -1
- package/esm5/coercion.es5.js.map +1 -1
- package/esm5/collections.es5.js +110 -8
- package/esm5/collections.es5.js.map +1 -1
- package/esm5/keycodes.es5.js +2 -0
- package/esm5/keycodes.es5.js.map +1 -1
- package/esm5/layout.es5.js +234 -0
- package/esm5/layout.es5.js.map +1 -0
- package/esm5/observers.es5.js +44 -40
- package/esm5/observers.es5.js.map +1 -1
- package/esm5/overlay.es5.js +304 -259
- package/esm5/overlay.es5.js.map +1 -1
- package/esm5/platform.es5.js +22 -17
- package/esm5/platform.es5.js.map +1 -1
- package/esm5/portal.es5.js +81 -44
- package/esm5/portal.es5.js.map +1 -1
- package/esm5/rxjs.es5.js +12 -1
- package/esm5/rxjs.es5.js.map +1 -1
- package/esm5/scrolling.es5.js +89 -51
- package/esm5/scrolling.es5.js.map +1 -1
- package/esm5/stepper.es5.js +119 -91
- package/esm5/stepper.es5.js.map +1 -1
- package/esm5/table.es5.js +265 -218
- package/esm5/table.es5.js.map +1 -1
- package/keycodes/index.metadata.json +2 -1
- package/keycodes/typings/index.d.ts +1 -1
- package/keycodes/typings/{public_api.d.ts → public-api.d.ts} +0 -0
- package/keycodes.metadata.json +2 -1
- package/layout/index.d.ts +8 -0
- package/layout/index.metadata.json +12 -0
- package/layout/package.json +7 -0
- package/layout/typings/breakpoints-observer.d.ts +37 -0
- package/layout/typings/breakpoints.d.ts +18 -0
- package/layout/typings/index.d.ts +4 -0
- package/layout/typings/index.metadata.json +1 -0
- package/layout/typings/media-matcher.d.ts +15 -0
- package/layout/typings/public-api.d.ts +5 -0
- package/layout.d.ts +8 -0
- package/layout.metadata.json +12 -0
- package/observers/index.metadata.json +2 -1
- package/observers/typings/index.d.ts +1 -1
- package/observers/typings/index.metadata.json +1 -1
- package/observers/typings/observe-content.d.ts +3 -3
- package/observers/typings/{public_api.d.ts → public-api.d.ts} +0 -0
- package/observers.metadata.json +2 -1
- package/overlay/index.metadata.json +2 -1
- package/overlay/typings/index.d.ts +2 -2
- package/overlay/typings/index.metadata.json +1 -1
- package/overlay/typings/overlay-config.d.ts +1 -1
- package/overlay/typings/overlay-directives.d.ts +3 -3
- package/overlay/typings/overlay-module.d.ts +11 -0
- package/overlay/typings/overlay-ref.d.ts +6 -6
- package/overlay/typings/overlay.d.ts +2 -2
- package/overlay/typings/position/connected-position-strategy.d.ts +5 -0
- package/overlay/typings/position/position-strategy.d.ts +2 -0
- package/overlay/typings/{public_api.d.ts → public-api.d.ts} +1 -4
- package/overlay/typings/scroll/scroll-strategy.d.ts +1 -1
- package/overlay-prebuilt.css +1 -0
- package/overlay.metadata.json +2 -1
- package/package.json +3 -3
- package/platform/index.metadata.json +2 -1
- package/platform/typings/index.d.ts +1 -1
- package/platform/typings/index.metadata.json +1 -1
- package/platform/typings/platform-module.d.ts +2 -0
- package/platform/typings/public-api.d.ts +10 -0
- package/platform.metadata.json +2 -1
- package/portal/index.metadata.json +2 -1
- package/portal/typings/index.d.ts +1 -1
- package/portal/typings/index.metadata.json +1 -1
- package/portal/typings/{public_api.d.ts → public-api.d.ts} +1 -0
- package/portal.metadata.json +2 -1
- package/rxjs/index.metadata.json +2 -1
- package/rxjs/typings/index.d.ts +1 -1
- package/rxjs/typings/index.metadata.json +1 -1
- package/rxjs/typings/{public_api.d.ts → public-api.d.ts} +0 -0
- package/rxjs/typings/rx-operators.d.ts +7 -0
- package/rxjs.metadata.json +2 -1
- package/scrolling/index.metadata.json +2 -1
- package/scrolling/typings/index.d.ts +1 -1
- package/scrolling/typings/index.metadata.json +1 -1
- package/scrolling/typings/public-api.d.ts +11 -0
- package/scrolling/typings/scrolling-module.d.ts +2 -0
- package/scrolling/typings/viewport-ruler.d.ts +20 -6
- package/scrolling.metadata.json +2 -1
- package/stepper/index.metadata.json +2 -1
- package/stepper/typings/index.d.ts +1 -1
- package/stepper/typings/index.metadata.json +1 -1
- package/stepper/typings/public-api.d.ts +11 -0
- package/stepper/typings/stepper-module.d.ts +2 -0
- package/stepper/typings/stepper.d.ts +8 -4
- package/stepper.metadata.json +2 -1
- package/table/index.metadata.json +2 -1
- package/table/typings/index.d.ts +1 -1
- package/table/typings/index.metadata.json +1 -1
- package/table/typings/public-api.d.ts +13 -0
- package/table/typings/row.d.ts +11 -3
- package/table/typings/table-errors.d.ts +10 -0
- package/table/typings/table-module.d.ts +2 -0
- package/table/typings/table.d.ts +17 -8
- package/table.metadata.json +2 -1
- package/typings/a11y/a11y-module.d.ts +2 -0
- package/typings/a11y/focus-monitor.d.ts +1 -1
- package/typings/a11y/index.d.ts +1 -1
- package/typings/a11y/index.metadata.json +1 -1
- package/typings/a11y/{public_api.d.ts → public-api.d.ts} +8 -2
- package/typings/bidi/bidi-module.d.ts +2 -0
- package/typings/bidi/index.d.ts +1 -1
- package/typings/bidi/index.metadata.json +1 -1
- package/typings/bidi/public-api.d.ts +10 -0
- package/typings/coercion/array.d.ts +9 -0
- package/typings/coercion/index.d.ts +1 -1
- package/typings/coercion/index.metadata.json +1 -1
- package/{coercion/typings/public_api.d.ts → typings/coercion/public-api.d.ts} +1 -0
- package/typings/collections/index.d.ts +2 -1
- package/typings/collections/index.metadata.json +1 -1
- package/{collections/typings/public_api.d.ts → typings/collections/public-api.d.ts} +1 -0
- package/typings/collections/selection.d.ts +13 -3
- package/typings/collections/unique-selection-dispatcher.d.ts +40 -0
- package/typings/index.d.ts +1 -1
- package/typings/index.metadata.json +1 -1
- package/typings/keycodes/index.d.ts +1 -1
- package/typings/keycodes/{public_api.d.ts → public-api.d.ts} +0 -0
- package/typings/layout/breakpoints-observer.d.ts +37 -0
- package/typings/layout/breakpoints.d.ts +18 -0
- package/typings/layout/index.d.ts +4 -0
- package/typings/layout/index.metadata.json +1 -0
- package/typings/layout/media-matcher.d.ts +15 -0
- package/typings/layout/public-api.d.ts +5 -0
- package/typings/observers/index.d.ts +1 -1
- package/typings/observers/index.metadata.json +1 -1
- package/typings/observers/observe-content.d.ts +3 -3
- package/typings/observers/{public_api.d.ts → public-api.d.ts} +0 -0
- package/typings/overlay/index.d.ts +2 -2
- package/typings/overlay/index.metadata.json +1 -1
- package/typings/overlay/overlay-config.d.ts +1 -1
- package/typings/overlay/overlay-directives.d.ts +3 -3
- package/typings/overlay/overlay-module.d.ts +11 -0
- package/typings/overlay/overlay-ref.d.ts +6 -6
- package/typings/overlay/overlay.d.ts +2 -2
- package/typings/overlay/position/connected-position-strategy.d.ts +5 -0
- package/typings/overlay/position/position-strategy.d.ts +2 -0
- package/typings/overlay/{public_api.d.ts → public-api.d.ts} +1 -4
- package/typings/overlay/scroll/scroll-strategy.d.ts +1 -1
- package/typings/platform/index.d.ts +1 -1
- package/typings/platform/index.metadata.json +1 -1
- package/typings/platform/platform-module.d.ts +2 -0
- package/typings/platform/public-api.d.ts +10 -0
- package/typings/portal/index.d.ts +1 -1
- package/typings/portal/index.metadata.json +1 -1
- package/typings/portal/{public_api.d.ts → public-api.d.ts} +1 -0
- package/typings/{public_api.d.ts → public-api.d.ts} +0 -0
- package/typings/rxjs/index.d.ts +1 -1
- package/typings/rxjs/index.metadata.json +1 -1
- package/typings/rxjs/{public_api.d.ts → public-api.d.ts} +0 -0
- package/typings/rxjs/rx-operators.d.ts +7 -0
- package/typings/scrolling/index.d.ts +1 -1
- package/typings/scrolling/index.metadata.json +1 -1
- package/typings/scrolling/public-api.d.ts +11 -0
- package/typings/scrolling/scrolling-module.d.ts +2 -0
- package/typings/scrolling/viewport-ruler.d.ts +20 -6
- package/typings/stepper/index.d.ts +1 -1
- package/typings/stepper/index.metadata.json +1 -1
- package/typings/stepper/public-api.d.ts +11 -0
- package/typings/stepper/stepper-module.d.ts +2 -0
- package/typings/stepper/stepper.d.ts +8 -4
- package/typings/table/index.d.ts +1 -1
- package/typings/table/index.metadata.json +1 -1
- package/typings/table/public-api.d.ts +13 -0
- package/typings/table/row.d.ts +11 -3
- package/typings/table/table-errors.d.ts +10 -0
- package/typings/table/table-module.d.ts +2 -0
- package/typings/table/table.d.ts +17 -8
- package/bidi/typings/public_api.d.ts +0 -4
- package/platform/typings/public_api.d.ts +0 -4
- package/scrolling/typings/public_api.d.ts +0 -5
- package/stepper/typings/public_api.d.ts +0 -5
- package/table/typings/public_api.d.ts +0 -7
- package/typings/bidi/public_api.d.ts +0 -4
- package/typings/platform/public_api.d.ts +0 -4
- package/typings/scrolling/public_api.d.ts +0 -5
- package/typings/stepper/public_api.d.ts +0 -5
- package/typings/table/public_api.d.ts +0 -7
package/esm2015/a11y.js
CHANGED
|
@@ -5,803 +5,561 @@
|
|
|
5
5
|
* Use of this source code is governed by an MIT-style license that can be
|
|
6
6
|
* found in the LICENSE file at https://angular.io/license
|
|
7
7
|
*/
|
|
8
|
-
import { Directive, ElementRef, EventEmitter, Inject, Injectable, InjectionToken, Input, NgModule, NgZone, Optional, Output, Renderer2, SkipSelf } from '@angular/core';
|
|
9
|
-
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
|
10
|
-
import { Platform, PlatformModule } from '@angular/cdk/platform';
|
|
11
|
-
import { RxChain, debounceTime, doOperator, filter, first, map } from '@angular/cdk/rxjs';
|
|
12
|
-
import { CommonModule } from '@angular/common';
|
|
13
8
|
import { Subject } from 'rxjs/Subject';
|
|
14
|
-
import { of } from 'rxjs/observable/of';
|
|
15
9
|
import { Subscription } from 'rxjs/Subscription';
|
|
16
10
|
import { A, DOWN_ARROW, NINE, TAB, UP_ARROW, Z, ZERO } from '@angular/cdk/keycodes';
|
|
11
|
+
import { RxChain, debounceTime, doOperator, filter, first, map } from '@angular/cdk/rxjs';
|
|
12
|
+
import { Directive, ElementRef, EventEmitter, Inject, Injectable, InjectionToken, Input, NgModule, NgZone, Optional, Output, Renderer2, SkipSelf } from '@angular/core';
|
|
13
|
+
import { Platform, PlatformModule } from '@angular/cdk/platform';
|
|
14
|
+
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
|
15
|
+
import { of } from 'rxjs/observable/of';
|
|
16
|
+
import { CommonModule } from '@angular/common';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* This class manages keyboard events for selectable lists. If you pass it a query list
|
|
20
|
+
* of items, it will set the active item correctly when arrow events occur.
|
|
21
21
|
*/
|
|
22
|
-
class
|
|
22
|
+
class ListKeyManager {
|
|
23
23
|
/**
|
|
24
|
-
* @param {?}
|
|
24
|
+
* @param {?} _items
|
|
25
25
|
*/
|
|
26
|
-
constructor(
|
|
27
|
-
this.
|
|
26
|
+
constructor(_items) {
|
|
27
|
+
this._items = _items;
|
|
28
|
+
this._activeItemIndex = -1;
|
|
29
|
+
this._wrap = false;
|
|
30
|
+
this._letterKeyStream = new Subject();
|
|
31
|
+
this._typeaheadSubscription = Subscription.EMPTY;
|
|
32
|
+
this._pressedLetters = [];
|
|
33
|
+
/**
|
|
34
|
+
* Stream that emits any time the TAB key is pressed, so components can react
|
|
35
|
+
* when focus is shifted off of the list.
|
|
36
|
+
*/
|
|
37
|
+
this.tabOut = new Subject();
|
|
28
38
|
}
|
|
29
39
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* @
|
|
33
|
-
* @return {?} Whether the element is disabled.
|
|
40
|
+
* Turns on wrapping mode, which ensures that the active item will wrap to
|
|
41
|
+
* the other end of list when there are no more items in the given direction.
|
|
42
|
+
* @return {?}
|
|
34
43
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return element.hasAttribute('disabled');
|
|
44
|
+
withWrap() {
|
|
45
|
+
this._wrap = true;
|
|
46
|
+
return this;
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* being clipped by an `overflow: hidden` parent or being outside the viewport.
|
|
45
|
-
*
|
|
46
|
-
* @param {?} element
|
|
47
|
-
* @return {?} Whether the element is visible.
|
|
49
|
+
* Turns on typeahead mode which allows users to set the active item by typing.
|
|
50
|
+
* @param {?=} debounceInterval Time to wait after the last keystroke before setting the active item.
|
|
51
|
+
* @return {?}
|
|
48
52
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
withTypeAhead(debounceInterval = 200) {
|
|
54
|
+
if (this._items.length && this._items.some(item => typeof item.getLabel !== 'function')) {
|
|
55
|
+
throw Error('ListKeyManager items in typeahead mode must implement the `getLabel` method.');
|
|
56
|
+
}
|
|
57
|
+
this._typeaheadSubscription.unsubscribe();
|
|
58
|
+
// Debounce the presses of non-navigational keys, collect the ones that correspond to letters
|
|
59
|
+
// and convert those letters back into a string. Afterwards find the first item that starts
|
|
60
|
+
// with that string and select it.
|
|
61
|
+
this._typeaheadSubscription = RxChain.from(this._letterKeyStream)
|
|
62
|
+
.call(doOperator, keyCode => this._pressedLetters.push(keyCode))
|
|
63
|
+
.call(debounceTime, debounceInterval)
|
|
64
|
+
.call(filter, () => this._pressedLetters.length > 0)
|
|
65
|
+
.call(map, () => this._pressedLetters.join(''))
|
|
66
|
+
.subscribe(inputString => {
|
|
67
|
+
const /** @type {?} */ items = this._items.toArray();
|
|
68
|
+
// Start at 1 because we want to start searching at the item immediately
|
|
69
|
+
// following the current active item.
|
|
70
|
+
for (let /** @type {?} */ i = 1; i < items.length + 1; i++) {
|
|
71
|
+
const /** @type {?} */ index = (this._activeItemIndex + i) % items.length;
|
|
72
|
+
const /** @type {?} */ item = items[index];
|
|
73
|
+
if (!item.disabled && ((item.getLabel))().toUpperCase().trim().indexOf(inputString) === 0) {
|
|
74
|
+
this.setActiveItem(index);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
this._pressedLetters = [];
|
|
79
|
+
});
|
|
80
|
+
return this;
|
|
51
81
|
}
|
|
52
82
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* @param {?} element Element to be checked.
|
|
57
|
-
* @return {?} Whether the element is tabbable.
|
|
83
|
+
* Sets the active item to the item at the index specified.
|
|
84
|
+
* @param {?} index The index of the item to be set as active.
|
|
85
|
+
* @return {?}
|
|
58
86
|
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// By default an <audio> element without the controls enabled is not tabbable.
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
else if (this._platform.BLINK) {
|
|
96
|
-
// In Blink <audio controls> elements are always tabbable.
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
if (nodeName === 'video') {
|
|
101
|
-
if (!element.hasAttribute('controls') && this._platform.TRIDENT) {
|
|
102
|
-
// In Trident a <video> element without the controls enabled is not tabbable.
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
else if (this._platform.BLINK || this._platform.FIREFOX) {
|
|
106
|
-
// In Chrome and Firefox <video controls> elements are always tabbable.
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (nodeName === 'object' && (this._platform.BLINK || this._platform.WEBKIT)) {
|
|
111
|
-
// In all Blink and WebKit based browsers <object> elements are never tabbable.
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
// In iOS the browser only considers some specific elements as tabbable.
|
|
115
|
-
if (this._platform.WEBKIT && this._platform.IOS && !isPotentiallyTabbableIOS(element)) {
|
|
116
|
-
return false;
|
|
87
|
+
setActiveItem(index) {
|
|
88
|
+
this._activeItemIndex = index;
|
|
89
|
+
this._activeItem = this._items.toArray()[index];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Sets the active item depending on the key event passed in.
|
|
93
|
+
* @param {?} event Keyboard event to be used for determining which element should be active.
|
|
94
|
+
* @return {?}
|
|
95
|
+
*/
|
|
96
|
+
onKeydown(event) {
|
|
97
|
+
switch (event.keyCode) {
|
|
98
|
+
case DOWN_ARROW:
|
|
99
|
+
this.setNextItemActive();
|
|
100
|
+
break;
|
|
101
|
+
case UP_ARROW:
|
|
102
|
+
this.setPreviousItemActive();
|
|
103
|
+
break;
|
|
104
|
+
case TAB:
|
|
105
|
+
this.tabOut.next();
|
|
106
|
+
return;
|
|
107
|
+
default:
|
|
108
|
+
const /** @type {?} */ keyCode = event.keyCode;
|
|
109
|
+
// Attempt to use the `event.key` which also maps it to the user's keyboard language,
|
|
110
|
+
// otherwise fall back to resolving alphanumeric characters via the keyCode.
|
|
111
|
+
if (event.key && event.key.length === 1) {
|
|
112
|
+
this._letterKeyStream.next(event.key.toLocaleUpperCase());
|
|
113
|
+
}
|
|
114
|
+
else if ((keyCode >= A && keyCode <= Z) || (keyCode >= ZERO && keyCode <= NINE)) {
|
|
115
|
+
this._letterKeyStream.next(String.fromCharCode(keyCode));
|
|
116
|
+
}
|
|
117
|
+
// Note that we return here, in order to avoid preventing
|
|
118
|
+
// the default action of non-navigational keys.
|
|
119
|
+
return;
|
|
117
120
|
}
|
|
118
|
-
|
|
121
|
+
this._pressedLetters = [];
|
|
122
|
+
event.preventDefault();
|
|
119
123
|
}
|
|
120
124
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* @param {?} element Element to be checked.
|
|
124
|
-
* @return {?} Whether the element is focusable.
|
|
125
|
+
* Index of the currently active item.
|
|
126
|
+
* @return {?}
|
|
125
127
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Again, naive approach that does not capture many edge cases and browser quirks.
|
|
129
|
-
return isPotentiallyFocusable(element) && !this.isDisabled(element) && this.isVisible(element);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
InteractivityChecker.decorators = [
|
|
133
|
-
{ type: Injectable },
|
|
134
|
-
];
|
|
135
|
-
/**
|
|
136
|
-
* @nocollapse
|
|
137
|
-
*/
|
|
138
|
-
InteractivityChecker.ctorParameters = () => [
|
|
139
|
-
{ type: Platform, },
|
|
140
|
-
];
|
|
141
|
-
/**
|
|
142
|
-
* Checks whether the specified element has any geometry / rectangles.
|
|
143
|
-
* @param {?} element
|
|
144
|
-
* @return {?}
|
|
145
|
-
*/
|
|
146
|
-
function hasGeometry(element) {
|
|
147
|
-
// Use logic from jQuery to check for an invisible element.
|
|
148
|
-
// See https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js#L12
|
|
149
|
-
return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Gets whether an element's
|
|
153
|
-
* @param {?} element
|
|
154
|
-
* @return {?}
|
|
155
|
-
*/
|
|
156
|
-
function isNativeFormElement(element) {
|
|
157
|
-
let /** @type {?} */ nodeName = element.nodeName.toLowerCase();
|
|
158
|
-
return nodeName === 'input' ||
|
|
159
|
-
nodeName === 'select' ||
|
|
160
|
-
nodeName === 'button' ||
|
|
161
|
-
nodeName === 'textarea';
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Gets whether an element is an <input type="hidden">.
|
|
165
|
-
* @param {?} element
|
|
166
|
-
* @return {?}
|
|
167
|
-
*/
|
|
168
|
-
function isHiddenInput(element) {
|
|
169
|
-
return isInputElement(element) && element.type == 'hidden';
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Gets whether an element is an anchor that has an href attribute.
|
|
173
|
-
* @param {?} element
|
|
174
|
-
* @return {?}
|
|
175
|
-
*/
|
|
176
|
-
function isAnchorWithHref(element) {
|
|
177
|
-
return isAnchorElement(element) && element.hasAttribute('href');
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Gets whether an element is an input element.
|
|
181
|
-
* @param {?} element
|
|
182
|
-
* @return {?}
|
|
183
|
-
*/
|
|
184
|
-
function isInputElement(element) {
|
|
185
|
-
return element.nodeName.toLowerCase() == 'input';
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Gets whether an element is an anchor element.
|
|
189
|
-
* @param {?} element
|
|
190
|
-
* @return {?}
|
|
191
|
-
*/
|
|
192
|
-
function isAnchorElement(element) {
|
|
193
|
-
return element.nodeName.toLowerCase() == 'a';
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Gets whether an element has a valid tabindex.
|
|
197
|
-
* @param {?} element
|
|
198
|
-
* @return {?}
|
|
199
|
-
*/
|
|
200
|
-
function hasValidTabIndex(element) {
|
|
201
|
-
if (!element.hasAttribute('tabindex') || element.tabIndex === undefined) {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
let /** @type {?} */ tabIndex = element.getAttribute('tabindex');
|
|
205
|
-
// IE11 parses tabindex="" as the value "-32768"
|
|
206
|
-
if (tabIndex == '-32768') {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
return !!(tabIndex && !isNaN(parseInt(tabIndex, 10)));
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Returns the parsed tabindex from the element attributes instead of returning the
|
|
213
|
-
* evaluated tabindex from the browsers defaults.
|
|
214
|
-
* @param {?} element
|
|
215
|
-
* @return {?}
|
|
216
|
-
*/
|
|
217
|
-
function getTabIndexValue(element) {
|
|
218
|
-
if (!hasValidTabIndex(element)) {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
// See browser issue in Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054
|
|
222
|
-
const /** @type {?} */ tabIndex = parseInt(element.getAttribute('tabindex') || '', 10);
|
|
223
|
-
return isNaN(tabIndex) ? -1 : tabIndex;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Checks whether the specified element is potentially tabbable on iOS
|
|
227
|
-
* @param {?} element
|
|
228
|
-
* @return {?}
|
|
229
|
-
*/
|
|
230
|
-
function isPotentiallyTabbableIOS(element) {
|
|
231
|
-
let /** @type {?} */ nodeName = element.nodeName.toLowerCase();
|
|
232
|
-
let /** @type {?} */ inputType = nodeName === 'input' && ((element)).type;
|
|
233
|
-
return inputType === 'text'
|
|
234
|
-
|| inputType === 'password'
|
|
235
|
-
|| nodeName === 'select'
|
|
236
|
-
|| nodeName === 'textarea';
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Gets whether an element is potentially focusable without taking current visible/disabled state
|
|
240
|
-
* into account.
|
|
241
|
-
* @param {?} element
|
|
242
|
-
* @return {?}
|
|
243
|
-
*/
|
|
244
|
-
function isPotentiallyFocusable(element) {
|
|
245
|
-
// Inputs are potentially focusable *unless* they're type="hidden".
|
|
246
|
-
if (isHiddenInput(element)) {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
return isNativeFormElement(element) ||
|
|
250
|
-
isAnchorWithHref(element) ||
|
|
251
|
-
element.hasAttribute('contenteditable') ||
|
|
252
|
-
hasValidTabIndex(element);
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Gets the parent window of a DOM node with regards of being inside of an iframe.
|
|
256
|
-
* @param {?} node
|
|
257
|
-
* @return {?}
|
|
258
|
-
*/
|
|
259
|
-
function getWindow(node) {
|
|
260
|
-
return node.ownerDocument.defaultView || window;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Class that allows for trapping focus within a DOM element.
|
|
265
|
-
*
|
|
266
|
-
* NOTE: This class currently uses a very simple (naive) approach to focus trapping.
|
|
267
|
-
* It assumes that the tab order is the same as DOM order, which is not necessarily true.
|
|
268
|
-
* Things like tabIndex > 0, flex `order`, and shadow roots can cause to two to misalign.
|
|
269
|
-
* This will be replaced with a more intelligent solution before the library is considered stable.
|
|
270
|
-
*/
|
|
271
|
-
class FocusTrap {
|
|
272
|
-
/**
|
|
273
|
-
* @param {?} _element
|
|
274
|
-
* @param {?} _platform
|
|
275
|
-
* @param {?} _checker
|
|
276
|
-
* @param {?} _ngZone
|
|
277
|
-
* @param {?=} deferAnchors
|
|
278
|
-
*/
|
|
279
|
-
constructor(_element, _platform, _checker, _ngZone, deferAnchors = false) {
|
|
280
|
-
this._element = _element;
|
|
281
|
-
this._platform = _platform;
|
|
282
|
-
this._checker = _checker;
|
|
283
|
-
this._ngZone = _ngZone;
|
|
284
|
-
this._enabled = true;
|
|
285
|
-
if (!deferAnchors) {
|
|
286
|
-
this.attachAnchors();
|
|
287
|
-
}
|
|
128
|
+
get activeItemIndex() {
|
|
129
|
+
return this._activeItemIndex;
|
|
288
130
|
}
|
|
289
131
|
/**
|
|
290
|
-
*
|
|
132
|
+
* The active item.
|
|
291
133
|
* @return {?}
|
|
292
134
|
*/
|
|
293
|
-
get
|
|
135
|
+
get activeItem() {
|
|
136
|
+
return this._activeItem;
|
|
137
|
+
}
|
|
294
138
|
/**
|
|
295
|
-
*
|
|
139
|
+
* Sets the active item to the first enabled item in the list.
|
|
296
140
|
* @return {?}
|
|
297
141
|
*/
|
|
298
|
-
|
|
299
|
-
this.
|
|
300
|
-
if (this._startAnchor && this._endAnchor) {
|
|
301
|
-
this._startAnchor.tabIndex = this._endAnchor.tabIndex = this._enabled ? 0 : -1;
|
|
302
|
-
}
|
|
142
|
+
setFirstItemActive() {
|
|
143
|
+
this._setActiveItemByIndex(0, 1);
|
|
303
144
|
}
|
|
304
145
|
/**
|
|
305
|
-
*
|
|
146
|
+
* Sets the active item to the last enabled item in the list.
|
|
306
147
|
* @return {?}
|
|
307
148
|
*/
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
this._startAnchor.parentNode.removeChild(this._startAnchor);
|
|
311
|
-
}
|
|
312
|
-
if (this._endAnchor && this._endAnchor.parentNode) {
|
|
313
|
-
this._endAnchor.parentNode.removeChild(this._endAnchor);
|
|
314
|
-
}
|
|
315
|
-
this._startAnchor = this._endAnchor = null;
|
|
149
|
+
setLastItemActive() {
|
|
150
|
+
this._setActiveItemByIndex(this._items.length - 1, -1);
|
|
316
151
|
}
|
|
317
152
|
/**
|
|
318
|
-
*
|
|
319
|
-
* in the constructor, but can be deferred for cases like directives with `*ngIf`.
|
|
153
|
+
* Sets the active item to the next enabled item in the list.
|
|
320
154
|
* @return {?}
|
|
321
155
|
*/
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (!this._platform.isBrowser) {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (!this._startAnchor) {
|
|
328
|
-
this._startAnchor = this._createAnchor();
|
|
329
|
-
}
|
|
330
|
-
if (!this._endAnchor) {
|
|
331
|
-
this._endAnchor = this._createAnchor();
|
|
332
|
-
}
|
|
333
|
-
this._ngZone.runOutsideAngular(() => {
|
|
334
|
-
((this._startAnchor)).addEventListener('focus', () => {
|
|
335
|
-
this.focusLastTabbableElement();
|
|
336
|
-
}); /** @type {?} */
|
|
337
|
-
((this._endAnchor)).addEventListener('focus', () => {
|
|
338
|
-
this.focusFirstTabbableElement();
|
|
339
|
-
});
|
|
340
|
-
if (this._element.parentNode) {
|
|
341
|
-
this._element.parentNode.insertBefore(/** @type {?} */ ((this._startAnchor)), this._element);
|
|
342
|
-
this._element.parentNode.insertBefore(/** @type {?} */ ((this._endAnchor)), this._element.nextSibling);
|
|
343
|
-
}
|
|
344
|
-
});
|
|
156
|
+
setNextItemActive() {
|
|
157
|
+
this._activeItemIndex < 0 ? this.setFirstItemActive() : this._setActiveItemByDelta(1);
|
|
345
158
|
}
|
|
346
159
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
* @return {?} Returns a promise that resolves with a boolean, depending
|
|
350
|
-
* on whether focus was moved successfuly.
|
|
160
|
+
* Sets the active item to a previous enabled item in the list.
|
|
161
|
+
* @return {?}
|
|
351
162
|
*/
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
});
|
|
163
|
+
setPreviousItemActive() {
|
|
164
|
+
this._activeItemIndex < 0 && this._wrap ? this.setLastItemActive()
|
|
165
|
+
: this._setActiveItemByDelta(-1);
|
|
356
166
|
}
|
|
357
167
|
/**
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
* @return {?}
|
|
361
|
-
* on whether focus was moved successfuly.
|
|
168
|
+
* Allows setting of the activeItemIndex without any other effects.
|
|
169
|
+
* @param {?} index The new activeItemIndex.
|
|
170
|
+
* @return {?}
|
|
362
171
|
*/
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this._executeOnStable(() => resolve(this.focusFirstTabbableElement()));
|
|
366
|
-
});
|
|
172
|
+
updateActiveItemIndex(index) {
|
|
173
|
+
this._activeItemIndex = index;
|
|
367
174
|
}
|
|
368
175
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
176
|
+
* This method sets the active item, given a list of items and the delta between the
|
|
177
|
+
* currently active item and the new active item. It will calculate differently
|
|
178
|
+
* depending on whether wrap mode is turned on.
|
|
179
|
+
* @param {?} delta
|
|
180
|
+
* @param {?=} items
|
|
181
|
+
* @return {?}
|
|
373
182
|
*/
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
this.
|
|
377
|
-
});
|
|
183
|
+
_setActiveItemByDelta(delta, items = this._items.toArray()) {
|
|
184
|
+
this._wrap ? this._setActiveInWrapMode(delta, items)
|
|
185
|
+
: this._setActiveInDefaultMode(delta, items);
|
|
378
186
|
}
|
|
379
187
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
188
|
+
* Sets the active item properly given "wrap" mode. In other words, it will continue to move
|
|
189
|
+
* down the list until it finds an item that is not disabled, and it will wrap if it
|
|
190
|
+
* encounters either end of the list.
|
|
191
|
+
* @param {?} delta
|
|
192
|
+
* @param {?} items
|
|
193
|
+
* @return {?}
|
|
383
194
|
*/
|
|
384
|
-
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
` use 'cdk-focus-region-${bound}' instead.`, markers[i]);
|
|
392
|
-
}
|
|
195
|
+
_setActiveInWrapMode(delta, items) {
|
|
196
|
+
// when active item would leave menu, wrap to beginning or end
|
|
197
|
+
this._activeItemIndex =
|
|
198
|
+
(this._activeItemIndex + delta + items.length) % items.length;
|
|
199
|
+
// skip all disabled menu items recursively until an enabled one is reached
|
|
200
|
+
if (items[this._activeItemIndex].disabled) {
|
|
201
|
+
this._setActiveInWrapMode(delta, items);
|
|
393
202
|
}
|
|
394
|
-
|
|
395
|
-
|
|
203
|
+
else {
|
|
204
|
+
this.setActiveItem(this._activeItemIndex);
|
|
396
205
|
}
|
|
397
|
-
return markers.length ?
|
|
398
|
-
markers[markers.length - 1] : this._getLastTabbableElement(this._element);
|
|
399
206
|
}
|
|
400
207
|
/**
|
|
401
|
-
*
|
|
402
|
-
*
|
|
208
|
+
* Sets the active item properly given the default mode. In other words, it will
|
|
209
|
+
* continue to move down the list until it finds an item that is not disabled. If
|
|
210
|
+
* it encounters either end of the list, it will stop and not wrap.
|
|
211
|
+
* @param {?} delta
|
|
212
|
+
* @param {?} items
|
|
213
|
+
* @return {?}
|
|
403
214
|
*/
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (redirectToElement) {
|
|
407
|
-
redirectToElement.focus();
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
410
|
-
return this.focusFirstTabbableElement();
|
|
215
|
+
_setActiveInDefaultMode(delta, items) {
|
|
216
|
+
this._setActiveItemByIndex(this._activeItemIndex + delta, delta, items);
|
|
411
217
|
}
|
|
412
218
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
219
|
+
* Sets the active item to the first enabled item starting at the index specified. If the
|
|
220
|
+
* item is disabled, it will move in the fallbackDelta direction until it either
|
|
221
|
+
* finds an enabled item or encounters the end of the list.
|
|
222
|
+
* @param {?} index
|
|
223
|
+
* @param {?} fallbackDelta
|
|
224
|
+
* @param {?=} items
|
|
225
|
+
* @return {?}
|
|
415
226
|
*/
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
redirectToElement.focus();
|
|
420
|
-
}
|
|
421
|
-
return !!redirectToElement;
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Focuses the last tabbable element within the focus trap region.
|
|
425
|
-
* @return {?} Returns whether focus was moved successfuly.
|
|
426
|
-
*/
|
|
427
|
-
focusLastTabbableElement() {
|
|
428
|
-
const /** @type {?} */ redirectToElement = this._getRegionBoundary('end');
|
|
429
|
-
if (redirectToElement) {
|
|
430
|
-
redirectToElement.focus();
|
|
431
|
-
}
|
|
432
|
-
return !!redirectToElement;
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Get the first tabbable element from a DOM subtree (inclusive).
|
|
436
|
-
* @param {?} root
|
|
437
|
-
* @return {?}
|
|
438
|
-
*/
|
|
439
|
-
_getFirstTabbableElement(root) {
|
|
440
|
-
if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
|
|
441
|
-
return root;
|
|
227
|
+
_setActiveItemByIndex(index, fallbackDelta, items = this._items.toArray()) {
|
|
228
|
+
if (!items[index]) {
|
|
229
|
+
return;
|
|
442
230
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
let /** @type {?} */ tabbableChild = children[i].nodeType === Node.ELEMENT_NODE ?
|
|
448
|
-
this._getFirstTabbableElement(/** @type {?} */ (children[i])) :
|
|
449
|
-
null;
|
|
450
|
-
if (tabbableChild) {
|
|
451
|
-
return tabbableChild;
|
|
231
|
+
while (items[index].disabled) {
|
|
232
|
+
index += fallbackDelta;
|
|
233
|
+
if (!items[index]) {
|
|
234
|
+
return;
|
|
452
235
|
}
|
|
453
236
|
}
|
|
454
|
-
|
|
237
|
+
this.setActiveItem(index);
|
|
455
238
|
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
class ActiveDescendantKeyManager extends ListKeyManager {
|
|
456
242
|
/**
|
|
457
|
-
*
|
|
458
|
-
*
|
|
243
|
+
* This method sets the active item to the item at the specified index.
|
|
244
|
+
* It also adds active styles to the newly active item and removes active
|
|
245
|
+
* styles from the previously active item.
|
|
246
|
+
* @param {?} index
|
|
459
247
|
* @return {?}
|
|
460
248
|
*/
|
|
461
|
-
|
|
462
|
-
if (this.
|
|
463
|
-
|
|
249
|
+
setActiveItem(index) {
|
|
250
|
+
if (this.activeItem) {
|
|
251
|
+
this.activeItem.setInactiveStyles();
|
|
464
252
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
let /** @type {?} */ tabbableChild = children[i].nodeType === Node.ELEMENT_NODE ?
|
|
469
|
-
this._getLastTabbableElement(/** @type {?} */ (children[i])) :
|
|
470
|
-
null;
|
|
471
|
-
if (tabbableChild) {
|
|
472
|
-
return tabbableChild;
|
|
473
|
-
}
|
|
253
|
+
super.setActiveItem(index);
|
|
254
|
+
if (this.activeItem) {
|
|
255
|
+
this.activeItem.setActiveStyles();
|
|
474
256
|
}
|
|
475
|
-
return null;
|
|
476
257
|
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* IDs are deliminated by an empty space, as per the spec.
|
|
262
|
+
*/
|
|
263
|
+
const ID_DELIMINATOR = ' ';
|
|
264
|
+
/**
|
|
265
|
+
* Adds the given ID to the specified ARIA attribute on an element.
|
|
266
|
+
* Used for attributes such as aria-labelledby, aria-owns, etc.
|
|
267
|
+
* @param {?} el
|
|
268
|
+
* @param {?} attr
|
|
269
|
+
* @param {?} id
|
|
270
|
+
* @return {?}
|
|
271
|
+
*/
|
|
272
|
+
function addAriaReferencedId(el, attr, id) {
|
|
273
|
+
const /** @type {?} */ ids = getAriaReferenceIds(el, attr);
|
|
274
|
+
if (ids.some(existingId => existingId.trim() == id.trim())) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
ids.push(id.trim());
|
|
278
|
+
el.setAttribute(attr, ids.join(ID_DELIMINATOR));
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Removes the given ID from the specified ARIA attribute on an element.
|
|
282
|
+
* Used for attributes such as aria-labelledby, aria-owns, etc.
|
|
283
|
+
* @param {?} el
|
|
284
|
+
* @param {?} attr
|
|
285
|
+
* @param {?} id
|
|
286
|
+
* @return {?}
|
|
287
|
+
*/
|
|
288
|
+
function removeAriaReferencedId(el, attr, id) {
|
|
289
|
+
const /** @type {?} */ ids = getAriaReferenceIds(el, attr);
|
|
290
|
+
const /** @type {?} */ filteredIds = ids.filter(val => val != id.trim());
|
|
291
|
+
el.setAttribute(attr, filteredIds.join(ID_DELIMINATOR));
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Gets the list of IDs referenced by the given ARIA attribute on an element.
|
|
295
|
+
* Used for attributes such as aria-labelledby, aria-owns, etc.
|
|
296
|
+
* @param {?} el
|
|
297
|
+
* @param {?} attr
|
|
298
|
+
* @return {?}
|
|
299
|
+
*/
|
|
300
|
+
function getAriaReferenceIds(el, attr) {
|
|
301
|
+
// Get string array of all individual ids (whitespace deliminated) in the attribute value
|
|
302
|
+
return (el.getAttribute(attr) || '').match(/\S+/g) || [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* ID used for the body container where all messages are appended.
|
|
307
|
+
*/
|
|
308
|
+
const MESSAGES_CONTAINER_ID = 'cdk-describedby-message-container';
|
|
309
|
+
/**
|
|
310
|
+
* ID prefix used for each created message element.
|
|
311
|
+
*/
|
|
312
|
+
const CDK_DESCRIBEDBY_ID_PREFIX = 'cdk-describedby-message';
|
|
313
|
+
/**
|
|
314
|
+
* Attribute given to each host element that is described by a message element.
|
|
315
|
+
*/
|
|
316
|
+
const CDK_DESCRIBEDBY_HOST_ATTRIBUTE = 'cdk-describedby-host';
|
|
317
|
+
/**
|
|
318
|
+
* Global incremental identifier for each registered message element.
|
|
319
|
+
*/
|
|
320
|
+
let nextId = 0;
|
|
321
|
+
/**
|
|
322
|
+
* Global map of all registered message elements that have been placed into the document.
|
|
323
|
+
*/
|
|
324
|
+
const messageRegistry = new Map();
|
|
325
|
+
/**
|
|
326
|
+
* Container for all registered messages.
|
|
327
|
+
*/
|
|
328
|
+
let messagesContainer = null;
|
|
329
|
+
/**
|
|
330
|
+
* Utility that creates visually hidden elements with a message content. Useful for elements that
|
|
331
|
+
* want to use aria-describedby to further describe themselves without adding additional visual
|
|
332
|
+
* content.
|
|
333
|
+
* \@docs-private
|
|
334
|
+
*/
|
|
335
|
+
class AriaDescriber {
|
|
477
336
|
/**
|
|
478
|
-
*
|
|
479
|
-
* @return {?}
|
|
337
|
+
* @param {?} _platform
|
|
480
338
|
*/
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
anchor.tabIndex = this._enabled ? 0 : -1;
|
|
484
|
-
anchor.classList.add('cdk-visually-hidden');
|
|
485
|
-
anchor.classList.add('cdk-focus-trap-anchor');
|
|
486
|
-
return anchor;
|
|
339
|
+
constructor(_platform) {
|
|
340
|
+
this._platform = _platform;
|
|
487
341
|
}
|
|
488
342
|
/**
|
|
489
|
-
*
|
|
490
|
-
*
|
|
343
|
+
* Adds to the host element an aria-describedby reference to a hidden element that contains
|
|
344
|
+
* the message. If the same message has already been registered, then it will reuse the created
|
|
345
|
+
* message element.
|
|
346
|
+
* @param {?} hostElement
|
|
347
|
+
* @param {?} message
|
|
491
348
|
* @return {?}
|
|
492
349
|
*/
|
|
493
|
-
|
|
494
|
-
if (this.
|
|
495
|
-
|
|
350
|
+
describe(hostElement, message) {
|
|
351
|
+
if (!this._platform.isBrowser || !message.trim()) {
|
|
352
|
+
return;
|
|
496
353
|
}
|
|
497
|
-
|
|
498
|
-
|
|
354
|
+
if (!messageRegistry.has(message)) {
|
|
355
|
+
createMessageElement(message);
|
|
356
|
+
}
|
|
357
|
+
if (!isElementDescribedByMessage(hostElement, message)) {
|
|
358
|
+
addMessageReference(hostElement, message);
|
|
499
359
|
}
|
|
500
360
|
}
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* Factory that allows easy instantiation of focus traps.
|
|
504
|
-
*/
|
|
505
|
-
class FocusTrapFactory {
|
|
506
361
|
/**
|
|
507
|
-
*
|
|
508
|
-
* @param {?}
|
|
509
|
-
* @param {?}
|
|
362
|
+
* Removes the host element's aria-describedby reference to the message element.
|
|
363
|
+
* @param {?} hostElement
|
|
364
|
+
* @param {?} message
|
|
365
|
+
* @return {?}
|
|
510
366
|
*/
|
|
511
|
-
|
|
512
|
-
this.
|
|
513
|
-
|
|
514
|
-
|
|
367
|
+
removeDescription(hostElement, message) {
|
|
368
|
+
if (!this._platform.isBrowser || !message.trim()) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (isElementDescribedByMessage(hostElement, message)) {
|
|
372
|
+
removeMessageReference(hostElement, message);
|
|
373
|
+
}
|
|
374
|
+
const /** @type {?} */ registeredMessage = messageRegistry.get(message);
|
|
375
|
+
if (registeredMessage && registeredMessage.referenceCount === 0) {
|
|
376
|
+
deleteMessageElement(message);
|
|
377
|
+
}
|
|
378
|
+
if (messagesContainer && messagesContainer.childNodes.length === 0) {
|
|
379
|
+
deleteMessagesContainer();
|
|
380
|
+
}
|
|
515
381
|
}
|
|
516
382
|
/**
|
|
517
|
-
*
|
|
518
|
-
* @param {?=} deferAnchors
|
|
383
|
+
* Unregisters all created message elements and removes the message container.
|
|
519
384
|
* @return {?}
|
|
520
385
|
*/
|
|
521
|
-
|
|
522
|
-
|
|
386
|
+
ngOnDestroy() {
|
|
387
|
+
if (!this._platform.isBrowser) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const /** @type {?} */ describedElements = document.querySelectorAll(`[${CDK_DESCRIBEDBY_HOST_ATTRIBUTE}]`);
|
|
391
|
+
for (let /** @type {?} */ i = 0; i < describedElements.length; i++) {
|
|
392
|
+
removeCdkDescribedByReferenceIds(describedElements[i]);
|
|
393
|
+
describedElements[i].removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
|
|
394
|
+
}
|
|
395
|
+
if (messagesContainer) {
|
|
396
|
+
deleteMessagesContainer();
|
|
397
|
+
}
|
|
398
|
+
messageRegistry.clear();
|
|
523
399
|
}
|
|
524
400
|
}
|
|
525
|
-
|
|
401
|
+
AriaDescriber.decorators = [
|
|
526
402
|
{ type: Injectable },
|
|
527
403
|
];
|
|
528
404
|
/**
|
|
529
405
|
* @nocollapse
|
|
530
406
|
*/
|
|
531
|
-
|
|
532
|
-
{ type: InteractivityChecker, },
|
|
407
|
+
AriaDescriber.ctorParameters = () => [
|
|
533
408
|
{ type: Platform, },
|
|
534
|
-
{ type: NgZone, },
|
|
535
409
|
];
|
|
536
410
|
/**
|
|
537
|
-
*
|
|
538
|
-
*
|
|
411
|
+
* Creates a new element in the visually hidden message container element with the message
|
|
412
|
+
* as its content and adds it to the message registry.
|
|
413
|
+
* @param {?} message
|
|
414
|
+
* @return {?}
|
|
539
415
|
*/
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
this.focusTrap.enabled = !coerceBooleanProperty(val);
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* @return {?}
|
|
564
|
-
*/
|
|
565
|
-
ngOnDestroy() {
|
|
566
|
-
this.focusTrap.destroy();
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* @return {?}
|
|
570
|
-
*/
|
|
571
|
-
ngAfterContentInit() {
|
|
572
|
-
this.focusTrap.attachAnchors();
|
|
416
|
+
function createMessageElement(message) {
|
|
417
|
+
const /** @type {?} */ messageElement = document.createElement('div');
|
|
418
|
+
messageElement.setAttribute('id', `${CDK_DESCRIBEDBY_ID_PREFIX}-${nextId++}`);
|
|
419
|
+
messageElement.appendChild(/** @type {?} */ ((document.createTextNode(message))));
|
|
420
|
+
if (!messagesContainer) {
|
|
421
|
+
createMessagesContainer();
|
|
422
|
+
} /** @type {?} */
|
|
423
|
+
((messagesContainer)).appendChild(messageElement);
|
|
424
|
+
messageRegistry.set(message, { messageElement, referenceCount: 0 });
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Deletes the message element from the global messages container.
|
|
428
|
+
* @param {?} message
|
|
429
|
+
* @return {?}
|
|
430
|
+
*/
|
|
431
|
+
function deleteMessageElement(message) {
|
|
432
|
+
const /** @type {?} */ registeredMessage = messageRegistry.get(message);
|
|
433
|
+
const /** @type {?} */ messageElement = registeredMessage && registeredMessage.messageElement;
|
|
434
|
+
if (messagesContainer && messageElement) {
|
|
435
|
+
messagesContainer.removeChild(messageElement);
|
|
573
436
|
}
|
|
437
|
+
messageRegistry.delete(message);
|
|
574
438
|
}
|
|
575
|
-
FocusTrapDeprecatedDirective.decorators = [
|
|
576
|
-
{ type: Directive, args: [{
|
|
577
|
-
selector: 'cdk-focus-trap',
|
|
578
|
-
},] },
|
|
579
|
-
];
|
|
580
439
|
/**
|
|
581
|
-
*
|
|
440
|
+
* Creates the global container for all aria-describedby messages.
|
|
441
|
+
* @return {?}
|
|
582
442
|
*/
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
443
|
+
function createMessagesContainer() {
|
|
444
|
+
messagesContainer = document.createElement('div');
|
|
445
|
+
messagesContainer.setAttribute('id', MESSAGES_CONTAINER_ID);
|
|
446
|
+
messagesContainer.setAttribute('aria-hidden', 'true');
|
|
447
|
+
messagesContainer.style.display = 'none';
|
|
448
|
+
document.body.appendChild(messagesContainer);
|
|
449
|
+
}
|
|
590
450
|
/**
|
|
591
|
-
*
|
|
451
|
+
* Deletes the global messages container.
|
|
452
|
+
* @return {?}
|
|
592
453
|
*/
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
|
|
596
|
-
* @param {?} _focusTrapFactory
|
|
597
|
-
*/
|
|
598
|
-
constructor(_elementRef, _focusTrapFactory) {
|
|
599
|
-
this._elementRef = _elementRef;
|
|
600
|
-
this._focusTrapFactory = _focusTrapFactory;
|
|
601
|
-
this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
|
|
602
|
-
}
|
|
603
|
-
/**
|
|
604
|
-
* Whether the focus trap is active.
|
|
605
|
-
* @return {?}
|
|
606
|
-
*/
|
|
607
|
-
get enabled() { return this.focusTrap.enabled; }
|
|
608
|
-
/**
|
|
609
|
-
* @param {?} value
|
|
610
|
-
* @return {?}
|
|
611
|
-
*/
|
|
612
|
-
set enabled(value) { this.focusTrap.enabled = coerceBooleanProperty(value); }
|
|
613
|
-
/**
|
|
614
|
-
* @return {?}
|
|
615
|
-
*/
|
|
616
|
-
ngOnDestroy() {
|
|
617
|
-
this.focusTrap.destroy();
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* @return {?}
|
|
621
|
-
*/
|
|
622
|
-
ngAfterContentInit() {
|
|
623
|
-
this.focusTrap.attachAnchors();
|
|
624
|
-
}
|
|
454
|
+
function deleteMessagesContainer() {
|
|
455
|
+
document.body.removeChild(/** @type {?} */ ((messagesContainer)));
|
|
456
|
+
messagesContainer = null;
|
|
625
457
|
}
|
|
626
|
-
FocusTrapDirective.decorators = [
|
|
627
|
-
{ type: Directive, args: [{
|
|
628
|
-
selector: '[cdkTrapFocus]',
|
|
629
|
-
exportAs: 'cdkTrapFocus',
|
|
630
|
-
},] },
|
|
631
|
-
];
|
|
632
458
|
/**
|
|
633
|
-
*
|
|
459
|
+
* Removes all cdk-describedby messages that are hosted through the element.
|
|
460
|
+
* @param {?} element
|
|
461
|
+
* @return {?}
|
|
634
462
|
*/
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
'enabled': [{ type: Input, args: ['cdkTrapFocus',] },],
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
const LIVE_ANNOUNCER_ELEMENT_TOKEN = new InjectionToken('liveAnnouncerElement');
|
|
644
|
-
class LiveAnnouncer {
|
|
645
|
-
/**
|
|
646
|
-
* @param {?} elementToken
|
|
647
|
-
* @param {?} platform
|
|
648
|
-
*/
|
|
649
|
-
constructor(elementToken, platform) {
|
|
650
|
-
// Only do anything if we're on the browser platform.
|
|
651
|
-
if (platform.isBrowser) {
|
|
652
|
-
// We inject the live element as `any` because the constructor signature cannot reference
|
|
653
|
-
// browser globals (HTMLElement) on non-browser environments, since having a class decorator
|
|
654
|
-
// causes TypeScript to preserve the constructor signature types.
|
|
655
|
-
this._liveElement = elementToken || this._createLiveElement();
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
/**
|
|
659
|
-
* Announces a message to screenreaders.
|
|
660
|
-
* @param {?} message Message to be announced to the screenreader
|
|
661
|
-
* @param {?=} politeness The politeness of the announcer element
|
|
662
|
-
* @return {?}
|
|
663
|
-
*/
|
|
664
|
-
announce(message, politeness = 'polite') {
|
|
665
|
-
this._liveElement.textContent = '';
|
|
666
|
-
// TODO: ensure changing the politeness works on all environments we support.
|
|
667
|
-
this._liveElement.setAttribute('aria-live', politeness);
|
|
668
|
-
// This 100ms timeout is necessary for some browser + screen-reader combinations:
|
|
669
|
-
// - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout.
|
|
670
|
-
// - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a
|
|
671
|
-
// second time without clearing and then using a non-zero delay.
|
|
672
|
-
// (using JAWS 17 at time of this writing).
|
|
673
|
-
setTimeout(() => this._liveElement.textContent = message, 100);
|
|
674
|
-
}
|
|
675
|
-
/**
|
|
676
|
-
* @return {?}
|
|
677
|
-
*/
|
|
678
|
-
ngOnDestroy() {
|
|
679
|
-
if (this._liveElement && this._liveElement.parentNode) {
|
|
680
|
-
this._liveElement.parentNode.removeChild(this._liveElement);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
/**
|
|
684
|
-
* @return {?}
|
|
685
|
-
*/
|
|
686
|
-
_createLiveElement() {
|
|
687
|
-
let /** @type {?} */ liveEl = document.createElement('div');
|
|
688
|
-
liveEl.classList.add('cdk-visually-hidden');
|
|
689
|
-
liveEl.setAttribute('aria-atomic', 'true');
|
|
690
|
-
liveEl.setAttribute('aria-live', 'polite');
|
|
691
|
-
document.body.appendChild(liveEl);
|
|
692
|
-
return liveEl;
|
|
693
|
-
}
|
|
463
|
+
function removeCdkDescribedByReferenceIds(element) {
|
|
464
|
+
// Remove all aria-describedby reference IDs that are prefixed by CDK_DESCRIBEDBY_ID_PREFIX
|
|
465
|
+
const /** @type {?} */ originalReferenceIds = getAriaReferenceIds(element, 'aria-describedby')
|
|
466
|
+
.filter(id => id.indexOf(CDK_DESCRIBEDBY_ID_PREFIX) != 0);
|
|
467
|
+
element.setAttribute('aria-describedby', originalReferenceIds.join(' '));
|
|
694
468
|
}
|
|
695
|
-
LiveAnnouncer.decorators = [
|
|
696
|
-
{ type: Injectable },
|
|
697
|
-
];
|
|
698
469
|
/**
|
|
699
|
-
*
|
|
470
|
+
* Adds a message reference to the element using aria-describedby and increments the registered
|
|
471
|
+
* message's reference count.
|
|
472
|
+
* @param {?} element
|
|
473
|
+
* @param {?} message
|
|
474
|
+
* @return {?}
|
|
700
475
|
*/
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
476
|
+
function addMessageReference(element, message) {
|
|
477
|
+
const /** @type {?} */ registeredMessage = ((messageRegistry.get(message)));
|
|
478
|
+
// Add the aria-describedby reference and set the describedby_host attribute to mark the element.
|
|
479
|
+
addAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
|
|
480
|
+
element.setAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE, '');
|
|
481
|
+
registeredMessage.referenceCount++;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Removes a message reference from the element using aria-describedby and decrements the registered
|
|
485
|
+
* message's reference count.
|
|
486
|
+
* @param {?} element
|
|
487
|
+
* @param {?} message
|
|
488
|
+
* @return {?}
|
|
489
|
+
*/
|
|
490
|
+
function removeMessageReference(element, message) {
|
|
491
|
+
const /** @type {?} */ registeredMessage = ((messageRegistry.get(message)));
|
|
492
|
+
registeredMessage.referenceCount--;
|
|
493
|
+
removeAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
|
|
494
|
+
element.removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Returns true if the element has been described by the provided message ID.
|
|
498
|
+
* @param {?} element
|
|
499
|
+
* @param {?} message
|
|
500
|
+
* @return {?}
|
|
501
|
+
*/
|
|
502
|
+
function isElementDescribedByMessage(element, message) {
|
|
503
|
+
const /** @type {?} */ referenceIds = getAriaReferenceIds(element, 'aria-describedby');
|
|
504
|
+
const /** @type {?} */ registeredMessage = messageRegistry.get(message);
|
|
505
|
+
const /** @type {?} */ messageId = registeredMessage && registeredMessage.messageElement.id;
|
|
506
|
+
return !!messageId && referenceIds.indexOf(messageId) != -1;
|
|
507
|
+
}
|
|
705
508
|
/**
|
|
706
509
|
* \@docs-private
|
|
707
510
|
* @param {?} parentDispatcher
|
|
708
|
-
* @param {?} liveElement
|
|
709
511
|
* @param {?} platform
|
|
710
512
|
* @return {?}
|
|
711
513
|
*/
|
|
712
|
-
function
|
|
713
|
-
return parentDispatcher || new
|
|
514
|
+
function ARIA_DESCRIBER_PROVIDER_FACTORY(parentDispatcher, platform) {
|
|
515
|
+
return parentDispatcher || new AriaDescriber(platform);
|
|
714
516
|
}
|
|
715
517
|
/**
|
|
716
518
|
* \@docs-private
|
|
717
519
|
*/
|
|
718
|
-
const
|
|
719
|
-
// If there is already
|
|
720
|
-
provide:
|
|
520
|
+
const ARIA_DESCRIBER_PROVIDER = {
|
|
521
|
+
// If there is already an AriaDescriber available, use that. Otherwise, provide a new one.
|
|
522
|
+
provide: AriaDescriber,
|
|
721
523
|
deps: [
|
|
722
|
-
[new Optional(), new SkipSelf(),
|
|
723
|
-
|
|
724
|
-
Platform,
|
|
524
|
+
[new Optional(), new SkipSelf(), AriaDescriber],
|
|
525
|
+
Platform
|
|
725
526
|
],
|
|
726
|
-
useFactory:
|
|
527
|
+
useFactory: ARIA_DESCRIBER_PROVIDER_FACTORY
|
|
727
528
|
};
|
|
728
529
|
|
|
729
530
|
/**
|
|
730
|
-
*
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
* @param {?} el
|
|
737
|
-
* @param {?} attr
|
|
738
|
-
* @param {?} id
|
|
531
|
+
* Screenreaders will often fire fake mousedown events when a focusable element
|
|
532
|
+
* is activated using the keyboard. We can typically distinguish between these faked
|
|
533
|
+
* mousedown events and real mousedown events using the "buttons" property. While
|
|
534
|
+
* real mousedowns will indicate the mouse button that was pressed (e.g. "1" for
|
|
535
|
+
* the left mouse button), faked mousedowns will usually set the property value to 0.
|
|
536
|
+
* @param {?} event
|
|
739
537
|
* @return {?}
|
|
740
538
|
*/
|
|
741
|
-
function
|
|
742
|
-
|
|
743
|
-
if (ids.some(existingId => existingId.trim() == id.trim())) {
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
ids.push(id.trim());
|
|
747
|
-
el.setAttribute(attr, ids.join(ID_DELIMINATOR));
|
|
539
|
+
function isFakeMousedownFromScreenReader(event) {
|
|
540
|
+
return event.buttons === 0;
|
|
748
541
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
* Gets the list of IDs referenced by the given ARIA attribute on an element.
|
|
764
|
-
* Used for attributes such as aria-labelledby, aria-owns, etc.
|
|
765
|
-
* @param {?} el
|
|
766
|
-
* @param {?} attr
|
|
767
|
-
* @return {?}
|
|
768
|
-
*/
|
|
769
|
-
function getAriaReferenceIds(el, attr) {
|
|
770
|
-
// Get string array of all individual ids (whitespace deliminated) in the attribute value
|
|
771
|
-
return (el.getAttribute(attr) || '').match(/\S+/g) || [];
|
|
542
|
+
|
|
543
|
+
class FocusKeyManager extends ListKeyManager {
|
|
544
|
+
/**
|
|
545
|
+
* This method sets the active item to the item at the specified index.
|
|
546
|
+
* It also adds focuses the newly active item.
|
|
547
|
+
* @param {?} index
|
|
548
|
+
* @return {?}
|
|
549
|
+
*/
|
|
550
|
+
setActiveItem(index) {
|
|
551
|
+
super.setActiveItem(index);
|
|
552
|
+
if (this.activeItem) {
|
|
553
|
+
this.activeItem.focus();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
772
556
|
}
|
|
773
557
|
|
|
774
558
|
/**
|
|
775
|
-
*
|
|
776
|
-
|
|
777
|
-
const MESSAGES_CONTAINER_ID = 'cdk-describedby-message-container';
|
|
778
|
-
/**
|
|
779
|
-
* ID prefix used for each created message element.
|
|
780
|
-
*/
|
|
781
|
-
const CDK_DESCRIBEDBY_ID_PREFIX = 'cdk-describedby-message';
|
|
782
|
-
/**
|
|
783
|
-
* Attribute given to each host element that is described by a message element.
|
|
784
|
-
*/
|
|
785
|
-
const CDK_DESCRIBEDBY_HOST_ATTRIBUTE = 'cdk-describedby-host';
|
|
786
|
-
/**
|
|
787
|
-
* Global incremental identifier for each registered message element.
|
|
788
|
-
*/
|
|
789
|
-
let nextId = 0;
|
|
790
|
-
/**
|
|
791
|
-
* Global map of all registered message elements that have been placed into the document.
|
|
792
|
-
*/
|
|
793
|
-
const messageRegistry = new Map();
|
|
794
|
-
/**
|
|
795
|
-
* Container for all registered messages.
|
|
796
|
-
*/
|
|
797
|
-
let messagesContainer = null;
|
|
798
|
-
/**
|
|
799
|
-
* Utility that creates visually hidden elements with a message content. Useful for elements that
|
|
800
|
-
* want to use aria-describedby to further describe themselves without adding additional visual
|
|
801
|
-
* content.
|
|
802
|
-
* \@docs-private
|
|
559
|
+
* Utility for checking the interactivity of an element, such as whether is is focusable or
|
|
560
|
+
* tabbable.
|
|
803
561
|
*/
|
|
804
|
-
class
|
|
562
|
+
class InteractivityChecker {
|
|
805
563
|
/**
|
|
806
564
|
* @param {?} _platform
|
|
807
565
|
*/
|
|
@@ -809,782 +567,1026 @@ class AriaDescriber {
|
|
|
809
567
|
this._platform = _platform;
|
|
810
568
|
}
|
|
811
569
|
/**
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
* @
|
|
816
|
-
* @param {?} message
|
|
817
|
-
* @return {?}
|
|
570
|
+
* Gets whether an element is disabled.
|
|
571
|
+
*
|
|
572
|
+
* @param {?} element Element to be checked.
|
|
573
|
+
* @return {?} Whether the element is disabled.
|
|
818
574
|
*/
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
if (!messageRegistry.has(message)) {
|
|
824
|
-
createMessageElement(message);
|
|
825
|
-
}
|
|
826
|
-
if (!isElementDescribedByMessage(hostElement, message)) {
|
|
827
|
-
addMessageReference(hostElement, message);
|
|
828
|
-
}
|
|
575
|
+
isDisabled(element) {
|
|
576
|
+
// This does not capture some cases, such as a non-form control with a disabled attribute or
|
|
577
|
+
// a form control inside of a disabled form, but should capture the most common cases.
|
|
578
|
+
return element.hasAttribute('disabled');
|
|
829
579
|
}
|
|
830
580
|
/**
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
581
|
+
* Gets whether an element is visible for the purposes of interactivity.
|
|
582
|
+
*
|
|
583
|
+
* This will capture states like `display: none` and `visibility: hidden`, but not things like
|
|
584
|
+
* being clipped by an `overflow: hidden` parent or being outside the viewport.
|
|
585
|
+
*
|
|
586
|
+
* @param {?} element
|
|
587
|
+
* @return {?} Whether the element is visible.
|
|
835
588
|
*/
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
if (isElementDescribedByMessage(hostElement, message)) {
|
|
841
|
-
removeMessageReference(hostElement, message);
|
|
842
|
-
}
|
|
843
|
-
const /** @type {?} */ registeredMessage = messageRegistry.get(message);
|
|
844
|
-
if (registeredMessage && registeredMessage.referenceCount === 0) {
|
|
845
|
-
deleteMessageElement(message);
|
|
846
|
-
}
|
|
847
|
-
if (messagesContainer && messagesContainer.childNodes.length === 0) {
|
|
848
|
-
deleteMessagesContainer();
|
|
849
|
-
}
|
|
589
|
+
isVisible(element) {
|
|
590
|
+
return hasGeometry(element) && getComputedStyle(element).visibility === 'visible';
|
|
850
591
|
}
|
|
851
592
|
/**
|
|
852
|
-
*
|
|
853
|
-
*
|
|
593
|
+
* Gets whether an element can be reached via Tab key.
|
|
594
|
+
* Assumes that the element has already been checked with isFocusable.
|
|
595
|
+
*
|
|
596
|
+
* @param {?} element Element to be checked.
|
|
597
|
+
* @return {?} Whether the element is tabbable.
|
|
854
598
|
*/
|
|
855
|
-
|
|
599
|
+
isTabbable(element) {
|
|
600
|
+
// Nothing is tabbable on the the server 😎
|
|
856
601
|
if (!this._platform.isBrowser) {
|
|
857
|
-
return;
|
|
602
|
+
return false;
|
|
858
603
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
604
|
+
let /** @type {?} */ frameElement = (getWindow(element).frameElement);
|
|
605
|
+
if (frameElement) {
|
|
606
|
+
let /** @type {?} */ frameType = frameElement && frameElement.nodeName.toLowerCase();
|
|
607
|
+
// Frame elements inherit their tabindex onto all child elements.
|
|
608
|
+
if (getTabIndexValue(frameElement) === -1) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
// Webkit and Blink consider anything inside of an <object> element as non-tabbable.
|
|
612
|
+
if ((this._platform.BLINK || this._platform.WEBKIT) && frameType === 'object') {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
// Webkit and Blink disable tabbing to an element inside of an invisible frame.
|
|
616
|
+
if ((this._platform.BLINK || this._platform.WEBKIT) && !this.isVisible(frameElement)) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
863
619
|
}
|
|
864
|
-
|
|
865
|
-
|
|
620
|
+
let /** @type {?} */ nodeName = element.nodeName.toLowerCase();
|
|
621
|
+
let /** @type {?} */ tabIndexValue = getTabIndexValue(element);
|
|
622
|
+
if (element.hasAttribute('contenteditable')) {
|
|
623
|
+
return tabIndexValue !== -1;
|
|
866
624
|
}
|
|
867
|
-
|
|
625
|
+
if (nodeName === 'iframe') {
|
|
626
|
+
// The frames may be tabbable depending on content, but it's not possibly to reliably
|
|
627
|
+
// investigate the content of the frames.
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
if (nodeName === 'audio') {
|
|
631
|
+
if (!element.hasAttribute('controls')) {
|
|
632
|
+
// By default an <audio> element without the controls enabled is not tabbable.
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
else if (this._platform.BLINK) {
|
|
636
|
+
// In Blink <audio controls> elements are always tabbable.
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (nodeName === 'video') {
|
|
641
|
+
if (!element.hasAttribute('controls') && this._platform.TRIDENT) {
|
|
642
|
+
// In Trident a <video> element without the controls enabled is not tabbable.
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
else if (this._platform.BLINK || this._platform.FIREFOX) {
|
|
646
|
+
// In Chrome and Firefox <video controls> elements are always tabbable.
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (nodeName === 'object' && (this._platform.BLINK || this._platform.WEBKIT)) {
|
|
651
|
+
// In all Blink and WebKit based browsers <object> elements are never tabbable.
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
// In iOS the browser only considers some specific elements as tabbable.
|
|
655
|
+
if (this._platform.WEBKIT && this._platform.IOS && !isPotentiallyTabbableIOS(element)) {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
return element.tabIndex >= 0;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Gets whether an element can be focused by the user.
|
|
662
|
+
*
|
|
663
|
+
* @param {?} element Element to be checked.
|
|
664
|
+
* @return {?} Whether the element is focusable.
|
|
665
|
+
*/
|
|
666
|
+
isFocusable(element) {
|
|
667
|
+
// Perform checks in order of left to most expensive.
|
|
668
|
+
// Again, naive approach that does not capture many edge cases and browser quirks.
|
|
669
|
+
return isPotentiallyFocusable(element) && !this.isDisabled(element) && this.isVisible(element);
|
|
868
670
|
}
|
|
869
671
|
}
|
|
870
|
-
|
|
672
|
+
InteractivityChecker.decorators = [
|
|
871
673
|
{ type: Injectable },
|
|
872
674
|
];
|
|
873
675
|
/**
|
|
874
676
|
* @nocollapse
|
|
875
677
|
*/
|
|
876
|
-
|
|
678
|
+
InteractivityChecker.ctorParameters = () => [
|
|
877
679
|
{ type: Platform, },
|
|
878
680
|
];
|
|
879
681
|
/**
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
* @param {?} message
|
|
682
|
+
* Checks whether the specified element has any geometry / rectangles.
|
|
683
|
+
* @param {?} element
|
|
883
684
|
* @return {?}
|
|
884
685
|
*/
|
|
885
|
-
function
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
if (!messagesContainer) {
|
|
890
|
-
createMessagesContainer();
|
|
891
|
-
} /** @type {?} */
|
|
892
|
-
((messagesContainer)).appendChild(messageElement);
|
|
893
|
-
messageRegistry.set(message, { messageElement, referenceCount: 0 });
|
|
686
|
+
function hasGeometry(element) {
|
|
687
|
+
// Use logic from jQuery to check for an invisible element.
|
|
688
|
+
// See https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js#L12
|
|
689
|
+
return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
|
894
690
|
}
|
|
895
691
|
/**
|
|
896
|
-
*
|
|
897
|
-
* @param {?}
|
|
692
|
+
* Gets whether an element's
|
|
693
|
+
* @param {?} element
|
|
898
694
|
* @return {?}
|
|
899
695
|
*/
|
|
900
|
-
function
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
messageRegistry.delete(message);
|
|
696
|
+
function isNativeFormElement(element) {
|
|
697
|
+
let /** @type {?} */ nodeName = element.nodeName.toLowerCase();
|
|
698
|
+
return nodeName === 'input' ||
|
|
699
|
+
nodeName === 'select' ||
|
|
700
|
+
nodeName === 'button' ||
|
|
701
|
+
nodeName === 'textarea';
|
|
907
702
|
}
|
|
908
703
|
/**
|
|
909
|
-
*
|
|
704
|
+
* Gets whether an element is an <input type="hidden">.
|
|
705
|
+
* @param {?} element
|
|
910
706
|
* @return {?}
|
|
911
707
|
*/
|
|
912
|
-
function
|
|
913
|
-
|
|
914
|
-
messagesContainer.setAttribute('id', MESSAGES_CONTAINER_ID);
|
|
915
|
-
messagesContainer.setAttribute('aria-hidden', 'true');
|
|
916
|
-
messagesContainer.style.display = 'none';
|
|
917
|
-
document.body.appendChild(messagesContainer);
|
|
708
|
+
function isHiddenInput(element) {
|
|
709
|
+
return isInputElement(element) && element.type == 'hidden';
|
|
918
710
|
}
|
|
919
711
|
/**
|
|
920
|
-
*
|
|
712
|
+
* Gets whether an element is an anchor that has an href attribute.
|
|
713
|
+
* @param {?} element
|
|
921
714
|
* @return {?}
|
|
922
715
|
*/
|
|
923
|
-
function
|
|
924
|
-
|
|
925
|
-
messagesContainer = null;
|
|
716
|
+
function isAnchorWithHref(element) {
|
|
717
|
+
return isAnchorElement(element) && element.hasAttribute('href');
|
|
926
718
|
}
|
|
927
719
|
/**
|
|
928
|
-
*
|
|
720
|
+
* Gets whether an element is an input element.
|
|
929
721
|
* @param {?} element
|
|
930
722
|
* @return {?}
|
|
931
723
|
*/
|
|
932
|
-
function
|
|
933
|
-
|
|
934
|
-
const /** @type {?} */ originalReferenceIds = getAriaReferenceIds(element, 'aria-describedby')
|
|
935
|
-
.filter(id => id.indexOf(CDK_DESCRIBEDBY_ID_PREFIX) != 0);
|
|
936
|
-
element.setAttribute('aria-describedby', originalReferenceIds.join(' '));
|
|
724
|
+
function isInputElement(element) {
|
|
725
|
+
return element.nodeName.toLowerCase() == 'input';
|
|
937
726
|
}
|
|
938
727
|
/**
|
|
939
|
-
*
|
|
940
|
-
* message's reference count.
|
|
728
|
+
* Gets whether an element is an anchor element.
|
|
941
729
|
* @param {?} element
|
|
942
|
-
* @param {?} message
|
|
943
730
|
* @return {?}
|
|
944
731
|
*/
|
|
945
|
-
function
|
|
946
|
-
|
|
947
|
-
// Add the aria-describedby reference and set the describedby_host attribute to mark the element.
|
|
948
|
-
addAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
|
|
949
|
-
element.setAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE, '');
|
|
950
|
-
registeredMessage.referenceCount++;
|
|
732
|
+
function isAnchorElement(element) {
|
|
733
|
+
return element.nodeName.toLowerCase() == 'a';
|
|
951
734
|
}
|
|
952
735
|
/**
|
|
953
|
-
*
|
|
954
|
-
* message's reference count.
|
|
736
|
+
* Gets whether an element has a valid tabindex.
|
|
955
737
|
* @param {?} element
|
|
956
|
-
* @param {?} message
|
|
957
738
|
* @return {?}
|
|
958
739
|
*/
|
|
959
|
-
function
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
element.
|
|
740
|
+
function hasValidTabIndex(element) {
|
|
741
|
+
if (!element.hasAttribute('tabindex') || element.tabIndex === undefined) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
let /** @type {?} */ tabIndex = element.getAttribute('tabindex');
|
|
745
|
+
// IE11 parses tabindex="" as the value "-32768"
|
|
746
|
+
if (tabIndex == '-32768') {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
return !!(tabIndex && !isNaN(parseInt(tabIndex, 10)));
|
|
964
750
|
}
|
|
965
751
|
/**
|
|
966
|
-
* Returns
|
|
752
|
+
* Returns the parsed tabindex from the element attributes instead of returning the
|
|
753
|
+
* evaluated tabindex from the browsers defaults.
|
|
967
754
|
* @param {?} element
|
|
968
|
-
* @param {?} message
|
|
969
755
|
* @return {?}
|
|
970
756
|
*/
|
|
971
|
-
function
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
757
|
+
function getTabIndexValue(element) {
|
|
758
|
+
if (!hasValidTabIndex(element)) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
// See browser issue in Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054
|
|
762
|
+
const /** @type {?} */ tabIndex = parseInt(element.getAttribute('tabindex') || '', 10);
|
|
763
|
+
return isNaN(tabIndex) ? -1 : tabIndex;
|
|
976
764
|
}
|
|
977
765
|
/**
|
|
978
|
-
*
|
|
979
|
-
* @param {?}
|
|
980
|
-
* @param {?} platform
|
|
766
|
+
* Checks whether the specified element is potentially tabbable on iOS
|
|
767
|
+
* @param {?} element
|
|
981
768
|
* @return {?}
|
|
982
769
|
*/
|
|
983
|
-
function
|
|
984
|
-
|
|
770
|
+
function isPotentiallyTabbableIOS(element) {
|
|
771
|
+
let /** @type {?} */ nodeName = element.nodeName.toLowerCase();
|
|
772
|
+
let /** @type {?} */ inputType = nodeName === 'input' && ((element)).type;
|
|
773
|
+
return inputType === 'text'
|
|
774
|
+
|| inputType === 'password'
|
|
775
|
+
|| nodeName === 'select'
|
|
776
|
+
|| nodeName === 'textarea';
|
|
985
777
|
}
|
|
986
778
|
/**
|
|
987
|
-
*
|
|
779
|
+
* Gets whether an element is potentially focusable without taking current visible/disabled state
|
|
780
|
+
* into account.
|
|
781
|
+
* @param {?} element
|
|
782
|
+
* @return {?}
|
|
988
783
|
*/
|
|
989
|
-
|
|
990
|
-
//
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
784
|
+
function isPotentiallyFocusable(element) {
|
|
785
|
+
// Inputs are potentially focusable *unless* they're type="hidden".
|
|
786
|
+
if (isHiddenInput(element)) {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
return isNativeFormElement(element) ||
|
|
790
|
+
isAnchorWithHref(element) ||
|
|
791
|
+
element.hasAttribute('contenteditable') ||
|
|
792
|
+
hasValidTabIndex(element);
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Gets the parent window of a DOM node with regards of being inside of an iframe.
|
|
796
|
+
* @param {?} node
|
|
797
|
+
* @return {?}
|
|
798
|
+
*/
|
|
799
|
+
function getWindow(node) {
|
|
800
|
+
return node.ownerDocument.defaultView || window;
|
|
801
|
+
}
|
|
998
802
|
|
|
999
|
-
// This is the value used by AngularJS Material. Through trial and error (on iPhone 6S) they found
|
|
1000
|
-
// that a value of around 650ms seems appropriate.
|
|
1001
|
-
const TOUCH_BUFFER_MS = 650;
|
|
1002
803
|
/**
|
|
1003
|
-
*
|
|
804
|
+
* Class that allows for trapping focus within a DOM element.
|
|
805
|
+
*
|
|
806
|
+
* NOTE: This class currently uses a very simple (naive) approach to focus trapping.
|
|
807
|
+
* It assumes that the tab order is the same as DOM order, which is not necessarily true.
|
|
808
|
+
* Things like tabIndex > 0, flex `order`, and shadow roots can cause to two to misalign.
|
|
809
|
+
* This will be replaced with a more intelligent solution before the library is considered stable.
|
|
1004
810
|
*/
|
|
1005
|
-
class
|
|
811
|
+
class FocusTrap {
|
|
1006
812
|
/**
|
|
1007
|
-
* @param {?}
|
|
813
|
+
* @param {?} _element
|
|
1008
814
|
* @param {?} _platform
|
|
815
|
+
* @param {?} _checker
|
|
816
|
+
* @param {?} _ngZone
|
|
817
|
+
* @param {?=} deferAnchors
|
|
1009
818
|
*/
|
|
1010
|
-
constructor(_ngZone,
|
|
1011
|
-
this.
|
|
819
|
+
constructor(_element, _platform, _checker, _ngZone, deferAnchors = false) {
|
|
820
|
+
this._element = _element;
|
|
1012
821
|
this._platform = _platform;
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
*/
|
|
1020
|
-
this._windowFocused = false;
|
|
1021
|
-
/**
|
|
1022
|
-
* Weak map of elements being monitored to their info.
|
|
1023
|
-
*/
|
|
1024
|
-
this._elementInfo = new WeakMap();
|
|
1025
|
-
this._ngZone.runOutsideAngular(() => this._registerDocumentEvents());
|
|
822
|
+
this._checker = _checker;
|
|
823
|
+
this._ngZone = _ngZone;
|
|
824
|
+
this._enabled = true;
|
|
825
|
+
if (!deferAnchors) {
|
|
826
|
+
this.attachAnchors();
|
|
827
|
+
}
|
|
1026
828
|
}
|
|
1027
829
|
/**
|
|
1028
|
-
*
|
|
1029
|
-
* @
|
|
1030
|
-
* @param {?} renderer The renderer to use to apply CSS classes to the element.
|
|
1031
|
-
* @param {?} checkChildren Whether to count the element as focused when its children are focused.
|
|
1032
|
-
* @return {?} An observable that emits when the focus state of the element changes.
|
|
1033
|
-
* When the element is blurred, null will be emitted.
|
|
830
|
+
* Whether the focus trap is active.
|
|
831
|
+
* @return {?}
|
|
1034
832
|
*/
|
|
1035
|
-
|
|
1036
|
-
|
|
833
|
+
get enabled() { return this._enabled; }
|
|
834
|
+
/**
|
|
835
|
+
* @param {?} val
|
|
836
|
+
* @return {?}
|
|
837
|
+
*/
|
|
838
|
+
set enabled(val) {
|
|
839
|
+
this._enabled = val;
|
|
840
|
+
if (this._startAnchor && this._endAnchor) {
|
|
841
|
+
this._startAnchor.tabIndex = this._endAnchor.tabIndex = this._enabled ? 0 : -1;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Destroys the focus trap by cleaning up the anchors.
|
|
846
|
+
* @return {?}
|
|
847
|
+
*/
|
|
848
|
+
destroy() {
|
|
849
|
+
if (this._startAnchor && this._startAnchor.parentNode) {
|
|
850
|
+
this._startAnchor.parentNode.removeChild(this._startAnchor);
|
|
851
|
+
}
|
|
852
|
+
if (this._endAnchor && this._endAnchor.parentNode) {
|
|
853
|
+
this._endAnchor.parentNode.removeChild(this._endAnchor);
|
|
854
|
+
}
|
|
855
|
+
this._startAnchor = this._endAnchor = null;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Inserts the anchors into the DOM. This is usually done automatically
|
|
859
|
+
* in the constructor, but can be deferred for cases like directives with `*ngIf`.
|
|
860
|
+
* @return {?}
|
|
861
|
+
*/
|
|
862
|
+
attachAnchors() {
|
|
863
|
+
// If we're not on the browser, there can be no focus to trap.
|
|
1037
864
|
if (!this._platform.isBrowser) {
|
|
1038
|
-
return
|
|
865
|
+
return;
|
|
1039
866
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
867
|
+
if (!this._startAnchor) {
|
|
868
|
+
this._startAnchor = this._createAnchor();
|
|
869
|
+
}
|
|
870
|
+
if (!this._endAnchor) {
|
|
871
|
+
this._endAnchor = this._createAnchor();
|
|
1045
872
|
}
|
|
1046
|
-
// Create monitored element info.
|
|
1047
|
-
let /** @type {?} */ info = {
|
|
1048
|
-
unlisten: () => { },
|
|
1049
|
-
checkChildren: checkChildren,
|
|
1050
|
-
renderer: renderer,
|
|
1051
|
-
subject: new Subject()
|
|
1052
|
-
};
|
|
1053
|
-
this._elementInfo.set(element, info);
|
|
1054
|
-
// Start listening. We need to listen in capture phase since focus events don't bubble.
|
|
1055
|
-
let /** @type {?} */ focusListener = (event) => this._onFocus(event, element);
|
|
1056
|
-
let /** @type {?} */ blurListener = (event) => this._onBlur(event, element);
|
|
1057
873
|
this._ngZone.runOutsideAngular(() => {
|
|
1058
|
-
|
|
1059
|
-
|
|
874
|
+
((this._startAnchor)).addEventListener('focus', () => {
|
|
875
|
+
this.focusLastTabbableElement();
|
|
876
|
+
}); /** @type {?} */
|
|
877
|
+
((this._endAnchor)).addEventListener('focus', () => {
|
|
878
|
+
this.focusFirstTabbableElement();
|
|
879
|
+
});
|
|
880
|
+
if (this._element.parentNode) {
|
|
881
|
+
this._element.parentNode.insertBefore(/** @type {?} */ ((this._startAnchor)), this._element);
|
|
882
|
+
this._element.parentNode.insertBefore(/** @type {?} */ ((this._endAnchor)), this._element.nextSibling);
|
|
883
|
+
}
|
|
1060
884
|
});
|
|
1061
|
-
// Create an unlisten function for later.
|
|
1062
|
-
info.unlisten = () => {
|
|
1063
|
-
element.removeEventListener('focus', focusListener, true);
|
|
1064
|
-
element.removeEventListener('blur', blurListener, true);
|
|
1065
|
-
};
|
|
1066
|
-
return info.subject.asObservable();
|
|
1067
885
|
}
|
|
1068
886
|
/**
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
887
|
+
* Waits for the zone to stabilize, then either focuses the first element that the
|
|
888
|
+
* user specified, or the first tabbable element.
|
|
889
|
+
* @return {?} Returns a promise that resolves with a boolean, depending
|
|
890
|
+
* on whether focus was moved successfuly.
|
|
891
|
+
*/
|
|
892
|
+
focusInitialElementWhenReady() {
|
|
893
|
+
return new Promise(resolve => {
|
|
894
|
+
this._executeOnStable(() => resolve(this.focusInitialElement()));
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Waits for the zone to stabilize, then focuses
|
|
899
|
+
* the first tabbable element within the focus trap region.
|
|
900
|
+
* @return {?} Returns a promise that resolves with a boolean, depending
|
|
901
|
+
* on whether focus was moved successfuly.
|
|
902
|
+
*/
|
|
903
|
+
focusFirstTabbableElementWhenReady() {
|
|
904
|
+
return new Promise(resolve => {
|
|
905
|
+
this._executeOnStable(() => resolve(this.focusFirstTabbableElement()));
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Waits for the zone to stabilize, then focuses
|
|
910
|
+
* the last tabbable element within the focus trap region.
|
|
911
|
+
* @return {?} Returns a promise that resolves with a boolean, depending
|
|
912
|
+
* on whether focus was moved successfuly.
|
|
913
|
+
*/
|
|
914
|
+
focusLastTabbableElementWhenReady() {
|
|
915
|
+
return new Promise(resolve => {
|
|
916
|
+
this._executeOnStable(() => resolve(this.focusLastTabbableElement()));
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Get the specified boundary element of the trapped region.
|
|
921
|
+
* @param {?} bound The boundary to get (start or end of trapped region).
|
|
922
|
+
* @return {?} The boundary element.
|
|
923
|
+
*/
|
|
924
|
+
_getRegionBoundary(bound) {
|
|
925
|
+
// Contains the deprecated version of selector, for temporary backwards comparability.
|
|
926
|
+
let /** @type {?} */ markers = (this._element.querySelectorAll(`[cdk-focus-region-${bound}], ` +
|
|
927
|
+
`[cdk-focus-${bound}]`));
|
|
928
|
+
for (let /** @type {?} */ i = 0; i < markers.length; i++) {
|
|
929
|
+
if (markers[i].hasAttribute(`cdk-focus-${bound}`)) {
|
|
930
|
+
console.warn(`Found use of deprecated attribute 'cdk-focus-${bound}',` +
|
|
931
|
+
` use 'cdk-focus-region-${bound}' instead.`, markers[i]);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (bound == 'start') {
|
|
935
|
+
return markers.length ? markers[0] : this._getFirstTabbableElement(this._element);
|
|
936
|
+
}
|
|
937
|
+
return markers.length ?
|
|
938
|
+
markers[markers.length - 1] : this._getLastTabbableElement(this._element);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Focuses the element that should be focused when the focus trap is initialized.
|
|
942
|
+
* @return {?} Returns whether focus was moved successfuly.
|
|
943
|
+
*/
|
|
944
|
+
focusInitialElement() {
|
|
945
|
+
const /** @type {?} */ redirectToElement = (this._element.querySelector('[cdk-focus-initial]'));
|
|
946
|
+
if (redirectToElement) {
|
|
947
|
+
redirectToElement.focus();
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
return this.focusFirstTabbableElement();
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Focuses the first tabbable element within the focus trap region.
|
|
954
|
+
* @return {?} Returns whether focus was moved successfuly.
|
|
955
|
+
*/
|
|
956
|
+
focusFirstTabbableElement() {
|
|
957
|
+
const /** @type {?} */ redirectToElement = this._getRegionBoundary('start');
|
|
958
|
+
if (redirectToElement) {
|
|
959
|
+
redirectToElement.focus();
|
|
960
|
+
}
|
|
961
|
+
return !!redirectToElement;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Focuses the last tabbable element within the focus trap region.
|
|
965
|
+
* @return {?} Returns whether focus was moved successfuly.
|
|
966
|
+
*/
|
|
967
|
+
focusLastTabbableElement() {
|
|
968
|
+
const /** @type {?} */ redirectToElement = this._getRegionBoundary('end');
|
|
969
|
+
if (redirectToElement) {
|
|
970
|
+
redirectToElement.focus();
|
|
971
|
+
}
|
|
972
|
+
return !!redirectToElement;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Get the first tabbable element from a DOM subtree (inclusive).
|
|
976
|
+
* @param {?} root
|
|
1071
977
|
* @return {?}
|
|
1072
978
|
*/
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
979
|
+
_getFirstTabbableElement(root) {
|
|
980
|
+
if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
|
|
981
|
+
return root;
|
|
982
|
+
}
|
|
983
|
+
// Iterate in DOM order. Note that IE doesn't have `children` for SVG so we fall
|
|
984
|
+
// back to `childNodes` which includes text nodes, comments etc.
|
|
985
|
+
let /** @type {?} */ children = root.children || root.childNodes;
|
|
986
|
+
for (let /** @type {?} */ i = 0; i < children.length; i++) {
|
|
987
|
+
let /** @type {?} */ tabbableChild = children[i].nodeType === Node.ELEMENT_NODE ?
|
|
988
|
+
this._getFirstTabbableElement(/** @type {?} */ (children[i])) :
|
|
989
|
+
null;
|
|
990
|
+
if (tabbableChild) {
|
|
991
|
+
return tabbableChild;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Get the last tabbable element from a DOM subtree (inclusive).
|
|
998
|
+
* @param {?} root
|
|
999
|
+
* @return {?}
|
|
1000
|
+
*/
|
|
1001
|
+
_getLastTabbableElement(root) {
|
|
1002
|
+
if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
|
|
1003
|
+
return root;
|
|
1004
|
+
}
|
|
1005
|
+
// Iterate in reverse DOM order.
|
|
1006
|
+
let /** @type {?} */ children = root.children || root.childNodes;
|
|
1007
|
+
for (let /** @type {?} */ i = children.length - 1; i >= 0; i--) {
|
|
1008
|
+
let /** @type {?} */ tabbableChild = children[i].nodeType === Node.ELEMENT_NODE ?
|
|
1009
|
+
this._getLastTabbableElement(/** @type {?} */ (children[i])) :
|
|
1010
|
+
null;
|
|
1011
|
+
if (tabbableChild) {
|
|
1012
|
+
return tabbableChild;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Creates an anchor element.
|
|
1019
|
+
* @return {?}
|
|
1020
|
+
*/
|
|
1021
|
+
_createAnchor() {
|
|
1022
|
+
let /** @type {?} */ anchor = document.createElement('div');
|
|
1023
|
+
anchor.tabIndex = this._enabled ? 0 : -1;
|
|
1024
|
+
anchor.classList.add('cdk-visually-hidden');
|
|
1025
|
+
anchor.classList.add('cdk-focus-trap-anchor');
|
|
1026
|
+
return anchor;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Executes a function when the zone is stable.
|
|
1030
|
+
* @param {?} fn
|
|
1031
|
+
* @return {?}
|
|
1032
|
+
*/
|
|
1033
|
+
_executeOnStable(fn) {
|
|
1034
|
+
if (this._ngZone.isStable) {
|
|
1035
|
+
fn();
|
|
1080
1036
|
}
|
|
1037
|
+
else {
|
|
1038
|
+
first.call(this._ngZone.onStable.asObservable()).subscribe(fn);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Factory that allows easy instantiation of focus traps.
|
|
1044
|
+
*/
|
|
1045
|
+
class FocusTrapFactory {
|
|
1046
|
+
/**
|
|
1047
|
+
* @param {?} _checker
|
|
1048
|
+
* @param {?} _platform
|
|
1049
|
+
* @param {?} _ngZone
|
|
1050
|
+
*/
|
|
1051
|
+
constructor(_checker, _platform, _ngZone) {
|
|
1052
|
+
this._checker = _checker;
|
|
1053
|
+
this._platform = _platform;
|
|
1054
|
+
this._ngZone = _ngZone;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* @param {?} element
|
|
1058
|
+
* @param {?=} deferAnchors
|
|
1059
|
+
* @return {?}
|
|
1060
|
+
*/
|
|
1061
|
+
create(element, deferAnchors = false) {
|
|
1062
|
+
return new FocusTrap(element, this._platform, this._checker, this._ngZone, deferAnchors);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
FocusTrapFactory.decorators = [
|
|
1066
|
+
{ type: Injectable },
|
|
1067
|
+
];
|
|
1068
|
+
/**
|
|
1069
|
+
* @nocollapse
|
|
1070
|
+
*/
|
|
1071
|
+
FocusTrapFactory.ctorParameters = () => [
|
|
1072
|
+
{ type: InteractivityChecker, },
|
|
1073
|
+
{ type: Platform, },
|
|
1074
|
+
{ type: NgZone, },
|
|
1075
|
+
];
|
|
1076
|
+
/**
|
|
1077
|
+
* Directive for trapping focus within a region.
|
|
1078
|
+
* @deprecated
|
|
1079
|
+
*/
|
|
1080
|
+
class FocusTrapDeprecatedDirective {
|
|
1081
|
+
/**
|
|
1082
|
+
* @param {?} _elementRef
|
|
1083
|
+
* @param {?} _focusTrapFactory
|
|
1084
|
+
*/
|
|
1085
|
+
constructor(_elementRef, _focusTrapFactory) {
|
|
1086
|
+
this._elementRef = _elementRef;
|
|
1087
|
+
this._focusTrapFactory = _focusTrapFactory;
|
|
1088
|
+
this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
|
|
1081
1089
|
}
|
|
1082
1090
|
/**
|
|
1083
|
-
*
|
|
1084
|
-
* @param {?} element The element to focus.
|
|
1085
|
-
* @param {?} origin The focus origin.
|
|
1091
|
+
* Whether the focus trap is active.
|
|
1086
1092
|
* @return {?}
|
|
1087
1093
|
*/
|
|
1088
|
-
|
|
1089
|
-
this._setOriginForCurrentEventQueue(origin);
|
|
1090
|
-
element.focus();
|
|
1091
|
-
}
|
|
1094
|
+
get disabled() { return !this.focusTrap.enabled; }
|
|
1092
1095
|
/**
|
|
1093
|
-
*
|
|
1096
|
+
* @param {?} val
|
|
1094
1097
|
* @return {?}
|
|
1095
1098
|
*/
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
if (!this._platform.isBrowser) {
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
// Note: we listen to events in the capture phase so we can detect them even if the user stops
|
|
1102
|
-
// propagation.
|
|
1103
|
-
// On keydown record the origin and clear any touch event that may be in progress.
|
|
1104
|
-
document.addEventListener('keydown', () => {
|
|
1105
|
-
this._lastTouchTarget = null;
|
|
1106
|
-
this._setOriginForCurrentEventQueue('keyboard');
|
|
1107
|
-
}, true);
|
|
1108
|
-
// On mousedown record the origin only if there is not touch target, since a mousedown can
|
|
1109
|
-
// happen as a result of a touch event.
|
|
1110
|
-
document.addEventListener('mousedown', () => {
|
|
1111
|
-
if (!this._lastTouchTarget) {
|
|
1112
|
-
this._setOriginForCurrentEventQueue('mouse');
|
|
1113
|
-
}
|
|
1114
|
-
}, true);
|
|
1115
|
-
// When the touchstart event fires the focus event is not yet in the event queue. This means
|
|
1116
|
-
// we can't rely on the trick used above (setting timeout of 0ms). Instead we wait 650ms to
|
|
1117
|
-
// see if a focus happens.
|
|
1118
|
-
document.addEventListener('touchstart', (event) => {
|
|
1119
|
-
if (this._touchTimeout != null) {
|
|
1120
|
-
clearTimeout(this._touchTimeout);
|
|
1121
|
-
}
|
|
1122
|
-
this._lastTouchTarget = event.target;
|
|
1123
|
-
this._touchTimeout = setTimeout(() => this._lastTouchTarget = null, TOUCH_BUFFER_MS);
|
|
1124
|
-
}, true);
|
|
1125
|
-
// Make a note of when the window regains focus, so we can restore the origin info for the
|
|
1126
|
-
// focused element.
|
|
1127
|
-
window.addEventListener('focus', () => {
|
|
1128
|
-
this._windowFocused = true;
|
|
1129
|
-
setTimeout(() => this._windowFocused = false, 0);
|
|
1130
|
-
});
|
|
1099
|
+
set disabled(val) {
|
|
1100
|
+
this.focusTrap.enabled = !coerceBooleanProperty(val);
|
|
1131
1101
|
}
|
|
1132
1102
|
/**
|
|
1133
|
-
* Sets the focus classes on the element based on the given focus origin.
|
|
1134
|
-
* @param {?} element The element to update the classes on.
|
|
1135
|
-
* @param {?=} origin The focus origin.
|
|
1136
1103
|
* @return {?}
|
|
1137
1104
|
*/
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if (elementInfo) {
|
|
1141
|
-
const /** @type {?} */ toggleClass = (className, shouldSet) => {
|
|
1142
|
-
shouldSet ? elementInfo.renderer.addClass(element, className) :
|
|
1143
|
-
elementInfo.renderer.removeClass(element, className);
|
|
1144
|
-
};
|
|
1145
|
-
toggleClass('cdk-focused', !!origin);
|
|
1146
|
-
toggleClass('cdk-touch-focused', origin === 'touch');
|
|
1147
|
-
toggleClass('cdk-keyboard-focused', origin === 'keyboard');
|
|
1148
|
-
toggleClass('cdk-mouse-focused', origin === 'mouse');
|
|
1149
|
-
toggleClass('cdk-program-focused', origin === 'program');
|
|
1150
|
-
}
|
|
1105
|
+
ngOnDestroy() {
|
|
1106
|
+
this.focusTrap.destroy();
|
|
1151
1107
|
}
|
|
1152
1108
|
/**
|
|
1153
|
-
* Sets the origin and schedules an async function to clear it at the end of the event queue.
|
|
1154
|
-
* @param {?} origin The origin to set.
|
|
1155
1109
|
* @return {?}
|
|
1156
1110
|
*/
|
|
1157
|
-
|
|
1158
|
-
this.
|
|
1159
|
-
setTimeout(() => this._origin = null, 0);
|
|
1111
|
+
ngAfterContentInit() {
|
|
1112
|
+
this.focusTrap.attachAnchors();
|
|
1160
1113
|
}
|
|
1114
|
+
}
|
|
1115
|
+
FocusTrapDeprecatedDirective.decorators = [
|
|
1116
|
+
{ type: Directive, args: [{
|
|
1117
|
+
selector: 'cdk-focus-trap',
|
|
1118
|
+
},] },
|
|
1119
|
+
];
|
|
1120
|
+
/**
|
|
1121
|
+
* @nocollapse
|
|
1122
|
+
*/
|
|
1123
|
+
FocusTrapDeprecatedDirective.ctorParameters = () => [
|
|
1124
|
+
{ type: ElementRef, },
|
|
1125
|
+
{ type: FocusTrapFactory, },
|
|
1126
|
+
];
|
|
1127
|
+
FocusTrapDeprecatedDirective.propDecorators = {
|
|
1128
|
+
'disabled': [{ type: Input },],
|
|
1129
|
+
};
|
|
1130
|
+
/**
|
|
1131
|
+
* Directive for trapping focus within a region.
|
|
1132
|
+
*/
|
|
1133
|
+
class FocusTrapDirective {
|
|
1161
1134
|
/**
|
|
1162
|
-
*
|
|
1163
|
-
* @param {?}
|
|
1164
|
-
* @return {?} Whether the event was caused by a touch.
|
|
1135
|
+
* @param {?} _elementRef
|
|
1136
|
+
* @param {?} _focusTrapFactory
|
|
1165
1137
|
*/
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
// <div #parent tabindex="0" cdkFocusClasses>
|
|
1171
|
-
// <div #child (click)="#parent.focus()"></div>
|
|
1172
|
-
// </div>
|
|
1173
|
-
//
|
|
1174
|
-
// If the user touches the #child element and the #parent is programmatically focused as a
|
|
1175
|
-
// result, this code will still consider it to have been caused by the touch event and will
|
|
1176
|
-
// apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
|
|
1177
|
-
// relatively small edge-case that can be worked around by using
|
|
1178
|
-
// focusVia(parentEl, renderer, 'program') to focus the parent element.
|
|
1179
|
-
//
|
|
1180
|
-
// If we decide that we absolutely must handle this case correctly, we can do so by listening
|
|
1181
|
-
// for the first focus event after the touchstart, and then the first blur event after that
|
|
1182
|
-
// focus event. When that blur event fires we know that whatever follows is not a result of the
|
|
1183
|
-
// touchstart.
|
|
1184
|
-
let /** @type {?} */ focusTarget = event.target;
|
|
1185
|
-
return this._lastTouchTarget instanceof Node && focusTarget instanceof Node &&
|
|
1186
|
-
(focusTarget === this._lastTouchTarget || focusTarget.contains(this._lastTouchTarget));
|
|
1138
|
+
constructor(_elementRef, _focusTrapFactory) {
|
|
1139
|
+
this._elementRef = _elementRef;
|
|
1140
|
+
this._focusTrapFactory = _focusTrapFactory;
|
|
1141
|
+
this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
|
|
1187
1142
|
}
|
|
1188
1143
|
/**
|
|
1189
|
-
*
|
|
1190
|
-
* @param {?} event The focus event.
|
|
1191
|
-
* @param {?} element The monitored element.
|
|
1144
|
+
* Whether the focus trap is active.
|
|
1192
1145
|
* @return {?}
|
|
1193
1146
|
*/
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
// If we couldn't detect a cause for the focus event, it's due to one of three reasons:
|
|
1206
|
-
// 1) The window has just regained focus, in which case we want to restore the focused state of
|
|
1207
|
-
// the element from before the window blurred.
|
|
1208
|
-
// 2) It was caused by a touch event, in which case we mark the origin as 'touch'.
|
|
1209
|
-
// 3) The element was programmatically focused, in which case we should mark the origin as
|
|
1210
|
-
// 'program'.
|
|
1211
|
-
if (!this._origin) {
|
|
1212
|
-
if (this._windowFocused && this._lastFocusOrigin) {
|
|
1213
|
-
this._origin = this._lastFocusOrigin;
|
|
1214
|
-
}
|
|
1215
|
-
else if (this._wasCausedByTouch(event)) {
|
|
1216
|
-
this._origin = 'touch';
|
|
1217
|
-
}
|
|
1218
|
-
else {
|
|
1219
|
-
this._origin = 'program';
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
this._setClasses(element, this._origin);
|
|
1223
|
-
elementInfo.subject.next(this._origin);
|
|
1224
|
-
this._lastFocusOrigin = this._origin;
|
|
1225
|
-
this._origin = null;
|
|
1147
|
+
get enabled() { return this.focusTrap.enabled; }
|
|
1148
|
+
/**
|
|
1149
|
+
* @param {?} value
|
|
1150
|
+
* @return {?}
|
|
1151
|
+
*/
|
|
1152
|
+
set enabled(value) { this.focusTrap.enabled = coerceBooleanProperty(value); }
|
|
1153
|
+
/**
|
|
1154
|
+
* @return {?}
|
|
1155
|
+
*/
|
|
1156
|
+
ngOnDestroy() {
|
|
1157
|
+
this.focusTrap.destroy();
|
|
1226
1158
|
}
|
|
1227
1159
|
/**
|
|
1228
|
-
* Handles blur events on a registered element.
|
|
1229
|
-
* @param {?} event The blur event.
|
|
1230
|
-
* @param {?} element The monitored element.
|
|
1231
1160
|
* @return {?}
|
|
1232
1161
|
*/
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
// order to focus another child of the monitored element.
|
|
1236
|
-
const /** @type {?} */ elementInfo = this._elementInfo.get(element);
|
|
1237
|
-
if (!elementInfo || (elementInfo.checkChildren && event.relatedTarget instanceof Node &&
|
|
1238
|
-
element.contains(event.relatedTarget))) {
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
this._setClasses(element);
|
|
1242
|
-
elementInfo.subject.next(null);
|
|
1162
|
+
ngAfterContentInit() {
|
|
1163
|
+
this.focusTrap.attachAnchors();
|
|
1243
1164
|
}
|
|
1244
1165
|
}
|
|
1245
|
-
|
|
1246
|
-
{ type:
|
|
1166
|
+
FocusTrapDirective.decorators = [
|
|
1167
|
+
{ type: Directive, args: [{
|
|
1168
|
+
selector: '[cdkTrapFocus]',
|
|
1169
|
+
exportAs: 'cdkTrapFocus',
|
|
1170
|
+
},] },
|
|
1247
1171
|
];
|
|
1248
1172
|
/**
|
|
1249
1173
|
* @nocollapse
|
|
1250
1174
|
*/
|
|
1251
|
-
|
|
1252
|
-
{ type:
|
|
1253
|
-
{ type:
|
|
1175
|
+
FocusTrapDirective.ctorParameters = () => [
|
|
1176
|
+
{ type: ElementRef, },
|
|
1177
|
+
{ type: FocusTrapFactory, },
|
|
1254
1178
|
];
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
* focused.
|
|
1262
|
-
* 2) cdkMonitorSubtreeFocus: considers an element focused if it or any of its children are focused.
|
|
1263
|
-
*/
|
|
1264
|
-
class CdkMonitorFocus {
|
|
1179
|
+
FocusTrapDirective.propDecorators = {
|
|
1180
|
+
'enabled': [{ type: Input, args: ['cdkTrapFocus',] },],
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
const LIVE_ANNOUNCER_ELEMENT_TOKEN = new InjectionToken('liveAnnouncerElement');
|
|
1184
|
+
class LiveAnnouncer {
|
|
1265
1185
|
/**
|
|
1266
|
-
* @param {?}
|
|
1267
|
-
* @param {?}
|
|
1268
|
-
* @param {?} renderer
|
|
1186
|
+
* @param {?} elementToken
|
|
1187
|
+
* @param {?} platform
|
|
1269
1188
|
*/
|
|
1270
|
-
constructor(
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1189
|
+
constructor(elementToken, platform) {
|
|
1190
|
+
// Only do anything if we're on the browser platform.
|
|
1191
|
+
if (platform.isBrowser) {
|
|
1192
|
+
// We inject the live element as `any` because the constructor signature cannot reference
|
|
1193
|
+
// browser globals (HTMLElement) on non-browser environments, since having a class decorator
|
|
1194
|
+
// causes TypeScript to preserve the constructor signature types.
|
|
1195
|
+
this._liveElement = elementToken || this._createLiveElement();
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Announces a message to screenreaders.
|
|
1200
|
+
* @param {?} message Message to be announced to the screenreader
|
|
1201
|
+
* @param {?=} politeness The politeness of the announcer element
|
|
1202
|
+
* @return {?}
|
|
1203
|
+
*/
|
|
1204
|
+
announce(message, politeness = 'polite') {
|
|
1205
|
+
this._liveElement.textContent = '';
|
|
1206
|
+
// TODO: ensure changing the politeness works on all environments we support.
|
|
1207
|
+
this._liveElement.setAttribute('aria-live', politeness);
|
|
1208
|
+
// This 100ms timeout is necessary for some browser + screen-reader combinations:
|
|
1209
|
+
// - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout.
|
|
1210
|
+
// - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a
|
|
1211
|
+
// second time without clearing and then using a non-zero delay.
|
|
1212
|
+
// (using JAWS 17 at time of this writing).
|
|
1213
|
+
setTimeout(() => this._liveElement.textContent = message, 100);
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* @return {?}
|
|
1217
|
+
*/
|
|
1218
|
+
ngOnDestroy() {
|
|
1219
|
+
if (this._liveElement && this._liveElement.parentNode) {
|
|
1220
|
+
this._liveElement.parentNode.removeChild(this._liveElement);
|
|
1221
|
+
}
|
|
1276
1222
|
}
|
|
1277
1223
|
/**
|
|
1278
1224
|
* @return {?}
|
|
1279
1225
|
*/
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1226
|
+
_createLiveElement() {
|
|
1227
|
+
let /** @type {?} */ liveEl = document.createElement('div');
|
|
1228
|
+
liveEl.classList.add('cdk-visually-hidden');
|
|
1229
|
+
liveEl.setAttribute('aria-atomic', 'true');
|
|
1230
|
+
liveEl.setAttribute('aria-live', 'polite');
|
|
1231
|
+
document.body.appendChild(liveEl);
|
|
1232
|
+
return liveEl;
|
|
1283
1233
|
}
|
|
1284
1234
|
}
|
|
1285
|
-
|
|
1286
|
-
{ type:
|
|
1287
|
-
selector: '[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]',
|
|
1288
|
-
},] },
|
|
1235
|
+
LiveAnnouncer.decorators = [
|
|
1236
|
+
{ type: Injectable },
|
|
1289
1237
|
];
|
|
1290
1238
|
/**
|
|
1291
1239
|
* @nocollapse
|
|
1292
1240
|
*/
|
|
1293
|
-
|
|
1294
|
-
{ type:
|
|
1295
|
-
{ type:
|
|
1296
|
-
{ type: Renderer2, },
|
|
1241
|
+
LiveAnnouncer.ctorParameters = () => [
|
|
1242
|
+
{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [LIVE_ANNOUNCER_ELEMENT_TOKEN,] },] },
|
|
1243
|
+
{ type: Platform, },
|
|
1297
1244
|
];
|
|
1298
|
-
CdkMonitorFocus.propDecorators = {
|
|
1299
|
-
'cdkFocusChange': [{ type: Output },],
|
|
1300
|
-
};
|
|
1301
1245
|
/**
|
|
1302
1246
|
* \@docs-private
|
|
1303
1247
|
* @param {?} parentDispatcher
|
|
1304
|
-
* @param {?}
|
|
1248
|
+
* @param {?} liveElement
|
|
1305
1249
|
* @param {?} platform
|
|
1306
1250
|
* @return {?}
|
|
1307
1251
|
*/
|
|
1308
|
-
function
|
|
1309
|
-
return parentDispatcher || new
|
|
1252
|
+
function LIVE_ANNOUNCER_PROVIDER_FACTORY(parentDispatcher, liveElement, platform) {
|
|
1253
|
+
return parentDispatcher || new LiveAnnouncer(liveElement, platform);
|
|
1310
1254
|
}
|
|
1311
1255
|
/**
|
|
1312
1256
|
* \@docs-private
|
|
1313
1257
|
*/
|
|
1314
|
-
const
|
|
1315
|
-
// If there is already a
|
|
1316
|
-
provide:
|
|
1317
|
-
deps: [
|
|
1318
|
-
|
|
1258
|
+
const LIVE_ANNOUNCER_PROVIDER = {
|
|
1259
|
+
// If there is already a LiveAnnouncer available, use that. Otherwise, provide a new one.
|
|
1260
|
+
provide: LiveAnnouncer,
|
|
1261
|
+
deps: [
|
|
1262
|
+
[new Optional(), new SkipSelf(), LiveAnnouncer],
|
|
1263
|
+
[new Optional(), new Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN)],
|
|
1264
|
+
Platform,
|
|
1265
|
+
],
|
|
1266
|
+
useFactory: LIVE_ANNOUNCER_PROVIDER_FACTORY
|
|
1319
1267
|
};
|
|
1320
1268
|
|
|
1269
|
+
// This is the value used by AngularJS Material. Through trial and error (on iPhone 6S) they found
|
|
1270
|
+
// that a value of around 650ms seems appropriate.
|
|
1271
|
+
const TOUCH_BUFFER_MS = 650;
|
|
1321
1272
|
/**
|
|
1322
|
-
*
|
|
1323
|
-
* of items, it will set the active item correctly when arrow events occur.
|
|
1273
|
+
* Monitors mouse and keyboard events to determine the cause of focus events.
|
|
1324
1274
|
*/
|
|
1325
|
-
class
|
|
1275
|
+
class FocusMonitor {
|
|
1326
1276
|
/**
|
|
1327
|
-
* @param {?}
|
|
1277
|
+
* @param {?} _ngZone
|
|
1278
|
+
* @param {?} _platform
|
|
1328
1279
|
*/
|
|
1329
|
-
constructor(
|
|
1330
|
-
this.
|
|
1331
|
-
this.
|
|
1332
|
-
this._wrap = false;
|
|
1333
|
-
this._letterKeyStream = new Subject();
|
|
1334
|
-
this._typeaheadSubscription = Subscription.EMPTY;
|
|
1335
|
-
this._pressedLetters = [];
|
|
1280
|
+
constructor(_ngZone, _platform) {
|
|
1281
|
+
this._ngZone = _ngZone;
|
|
1282
|
+
this._platform = _platform;
|
|
1336
1283
|
/**
|
|
1337
|
-
*
|
|
1338
|
-
* when focus is shifted off of the list.
|
|
1284
|
+
* The focus origin that the next focus event is a result of.
|
|
1339
1285
|
*/
|
|
1340
|
-
this.
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
this.
|
|
1349
|
-
|
|
1286
|
+
this._origin = null;
|
|
1287
|
+
/**
|
|
1288
|
+
* Whether the window has just been focused.
|
|
1289
|
+
*/
|
|
1290
|
+
this._windowFocused = false;
|
|
1291
|
+
/**
|
|
1292
|
+
* Weak map of elements being monitored to their info.
|
|
1293
|
+
*/
|
|
1294
|
+
this._elementInfo = new WeakMap();
|
|
1295
|
+
this._ngZone.runOutsideAngular(() => this._registerDocumentEvents());
|
|
1350
1296
|
}
|
|
1351
1297
|
/**
|
|
1352
|
-
*
|
|
1353
|
-
* @param {
|
|
1354
|
-
* @
|
|
1298
|
+
* Monitors focus on an element and applies appropriate CSS classes.
|
|
1299
|
+
* @param {?} element The element to monitor
|
|
1300
|
+
* @param {?} renderer The renderer to use to apply CSS classes to the element.
|
|
1301
|
+
* @param {?} checkChildren Whether to count the element as focused when its children are focused.
|
|
1302
|
+
* @return {?} An observable that emits when the focus state of the element changes.
|
|
1303
|
+
* When the element is blurred, null will be emitted.
|
|
1355
1304
|
*/
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1305
|
+
monitor(element, renderer, checkChildren) {
|
|
1306
|
+
// Do nothing if we're not on the browser platform.
|
|
1307
|
+
if (!this._platform.isBrowser) {
|
|
1308
|
+
return of(null);
|
|
1359
1309
|
}
|
|
1360
|
-
this.
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1310
|
+
// Check if we're already monitoring this element.
|
|
1311
|
+
if (this._elementInfo.has(element)) {
|
|
1312
|
+
let /** @type {?} */ cachedInfo = this._elementInfo.get(element); /** @type {?} */
|
|
1313
|
+
((cachedInfo)).checkChildren = checkChildren;
|
|
1314
|
+
return ((cachedInfo)).subject.asObservable();
|
|
1315
|
+
}
|
|
1316
|
+
// Create monitored element info.
|
|
1317
|
+
let /** @type {?} */ info = {
|
|
1318
|
+
unlisten: () => { },
|
|
1319
|
+
checkChildren: checkChildren,
|
|
1320
|
+
renderer: renderer,
|
|
1321
|
+
subject: new Subject()
|
|
1322
|
+
};
|
|
1323
|
+
this._elementInfo.set(element, info);
|
|
1324
|
+
// Start listening. We need to listen in capture phase since focus events don't bubble.
|
|
1325
|
+
let /** @type {?} */ focusListener = (event) => this._onFocus(event, element);
|
|
1326
|
+
let /** @type {?} */ blurListener = (event) => this._onBlur(event, element);
|
|
1327
|
+
this._ngZone.runOutsideAngular(() => {
|
|
1328
|
+
element.addEventListener('focus', focusListener, true);
|
|
1329
|
+
element.addEventListener('blur', blurListener, true);
|
|
1378
1330
|
});
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
*/
|
|
1386
|
-
setActiveItem(index) {
|
|
1387
|
-
this._activeItemIndex = index;
|
|
1388
|
-
this._activeItem = this._items.toArray()[index];
|
|
1331
|
+
// Create an unlisten function for later.
|
|
1332
|
+
info.unlisten = () => {
|
|
1333
|
+
element.removeEventListener('focus', focusListener, true);
|
|
1334
|
+
element.removeEventListener('blur', blurListener, true);
|
|
1335
|
+
};
|
|
1336
|
+
return info.subject.asObservable();
|
|
1389
1337
|
}
|
|
1390
1338
|
/**
|
|
1391
|
-
*
|
|
1392
|
-
* @param {?}
|
|
1339
|
+
* Stops monitoring an element and removes all focus classes.
|
|
1340
|
+
* @param {?} element The element to stop monitoring.
|
|
1393
1341
|
* @return {?}
|
|
1394
1342
|
*/
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
break;
|
|
1403
|
-
case TAB:
|
|
1404
|
-
this.tabOut.next();
|
|
1405
|
-
return;
|
|
1406
|
-
default:
|
|
1407
|
-
const /** @type {?} */ keyCode = event.keyCode;
|
|
1408
|
-
// Attempt to use the `event.key` which also maps it to the user's keyboard language,
|
|
1409
|
-
// otherwise fall back to resolving alphanumeric characters via the keyCode.
|
|
1410
|
-
if (event.key && event.key.length === 1) {
|
|
1411
|
-
this._letterKeyStream.next(event.key.toLocaleUpperCase());
|
|
1412
|
-
}
|
|
1413
|
-
else if ((keyCode >= A && keyCode <= Z) || (keyCode >= ZERO && keyCode <= NINE)) {
|
|
1414
|
-
this._letterKeyStream.next(String.fromCharCode(keyCode));
|
|
1415
|
-
}
|
|
1416
|
-
// Note that we return here, in order to avoid preventing
|
|
1417
|
-
// the default action of non-navigational keys.
|
|
1418
|
-
return;
|
|
1343
|
+
stopMonitoring(element) {
|
|
1344
|
+
let /** @type {?} */ elementInfo = this._elementInfo.get(element);
|
|
1345
|
+
if (elementInfo) {
|
|
1346
|
+
elementInfo.unlisten();
|
|
1347
|
+
elementInfo.subject.complete();
|
|
1348
|
+
this._setClasses(element);
|
|
1349
|
+
this._elementInfo.delete(element);
|
|
1419
1350
|
}
|
|
1420
|
-
this._pressedLetters = [];
|
|
1421
|
-
event.preventDefault();
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* Index of the currently active item.
|
|
1425
|
-
* @return {?}
|
|
1426
|
-
*/
|
|
1427
|
-
get activeItemIndex() {
|
|
1428
|
-
return this._activeItemIndex;
|
|
1429
|
-
}
|
|
1430
|
-
/**
|
|
1431
|
-
* The active item.
|
|
1432
|
-
* @return {?}
|
|
1433
|
-
*/
|
|
1434
|
-
get activeItem() {
|
|
1435
|
-
return this._activeItem;
|
|
1436
|
-
}
|
|
1437
|
-
/**
|
|
1438
|
-
* Sets the active item to the first enabled item in the list.
|
|
1439
|
-
* @return {?}
|
|
1440
|
-
*/
|
|
1441
|
-
setFirstItemActive() {
|
|
1442
|
-
this._setActiveItemByIndex(0, 1);
|
|
1443
1351
|
}
|
|
1444
1352
|
/**
|
|
1445
|
-
*
|
|
1353
|
+
* Focuses the element via the specified focus origin.
|
|
1354
|
+
* @param {?} element The element to focus.
|
|
1355
|
+
* @param {?} origin The focus origin.
|
|
1446
1356
|
* @return {?}
|
|
1447
1357
|
*/
|
|
1448
|
-
|
|
1449
|
-
this.
|
|
1358
|
+
focusVia(element, origin) {
|
|
1359
|
+
this._setOriginForCurrentEventQueue(origin);
|
|
1360
|
+
element.focus();
|
|
1450
1361
|
}
|
|
1451
1362
|
/**
|
|
1452
|
-
*
|
|
1363
|
+
* Register necessary event listeners on the document and window.
|
|
1453
1364
|
* @return {?}
|
|
1454
1365
|
*/
|
|
1455
|
-
|
|
1456
|
-
|
|
1366
|
+
_registerDocumentEvents() {
|
|
1367
|
+
// Do nothing if we're not on the browser platform.
|
|
1368
|
+
if (!this._platform.isBrowser) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
// Note: we listen to events in the capture phase so we can detect them even if the user stops
|
|
1372
|
+
// propagation.
|
|
1373
|
+
// On keydown record the origin and clear any touch event that may be in progress.
|
|
1374
|
+
document.addEventListener('keydown', () => {
|
|
1375
|
+
this._lastTouchTarget = null;
|
|
1376
|
+
this._setOriginForCurrentEventQueue('keyboard');
|
|
1377
|
+
}, true);
|
|
1378
|
+
// On mousedown record the origin only if there is not touch target, since a mousedown can
|
|
1379
|
+
// happen as a result of a touch event.
|
|
1380
|
+
document.addEventListener('mousedown', () => {
|
|
1381
|
+
if (!this._lastTouchTarget) {
|
|
1382
|
+
this._setOriginForCurrentEventQueue('mouse');
|
|
1383
|
+
}
|
|
1384
|
+
}, true);
|
|
1385
|
+
// When the touchstart event fires the focus event is not yet in the event queue. This means
|
|
1386
|
+
// we can't rely on the trick used above (setting timeout of 0ms). Instead we wait 650ms to
|
|
1387
|
+
// see if a focus happens.
|
|
1388
|
+
document.addEventListener('touchstart', (event) => {
|
|
1389
|
+
if (this._touchTimeout != null) {
|
|
1390
|
+
clearTimeout(this._touchTimeout);
|
|
1391
|
+
}
|
|
1392
|
+
this._lastTouchTarget = event.target;
|
|
1393
|
+
this._touchTimeout = setTimeout(() => this._lastTouchTarget = null, TOUCH_BUFFER_MS);
|
|
1394
|
+
}, true);
|
|
1395
|
+
// Make a note of when the window regains focus, so we can restore the origin info for the
|
|
1396
|
+
// focused element.
|
|
1397
|
+
window.addEventListener('focus', () => {
|
|
1398
|
+
this._windowFocused = true;
|
|
1399
|
+
setTimeout(() => this._windowFocused = false, 0);
|
|
1400
|
+
});
|
|
1457
1401
|
}
|
|
1458
1402
|
/**
|
|
1459
|
-
* Sets the
|
|
1403
|
+
* Sets the focus classes on the element based on the given focus origin.
|
|
1404
|
+
* @param {?} element The element to update the classes on.
|
|
1405
|
+
* @param {?=} origin The focus origin.
|
|
1460
1406
|
* @return {?}
|
|
1461
1407
|
*/
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1408
|
+
_setClasses(element, origin) {
|
|
1409
|
+
const /** @type {?} */ elementInfo = this._elementInfo.get(element);
|
|
1410
|
+
if (elementInfo) {
|
|
1411
|
+
const /** @type {?} */ toggleClass = (className, shouldSet) => {
|
|
1412
|
+
shouldSet ? elementInfo.renderer.addClass(element, className) :
|
|
1413
|
+
elementInfo.renderer.removeClass(element, className);
|
|
1414
|
+
};
|
|
1415
|
+
toggleClass('cdk-focused', !!origin);
|
|
1416
|
+
toggleClass('cdk-touch-focused', origin === 'touch');
|
|
1417
|
+
toggleClass('cdk-keyboard-focused', origin === 'keyboard');
|
|
1418
|
+
toggleClass('cdk-mouse-focused', origin === 'mouse');
|
|
1419
|
+
toggleClass('cdk-program-focused', origin === 'program');
|
|
1420
|
+
}
|
|
1465
1421
|
}
|
|
1466
1422
|
/**
|
|
1467
|
-
*
|
|
1468
|
-
* @param {?}
|
|
1423
|
+
* Sets the origin and schedules an async function to clear it at the end of the event queue.
|
|
1424
|
+
* @param {?} origin The origin to set.
|
|
1469
1425
|
* @return {?}
|
|
1470
1426
|
*/
|
|
1471
|
-
|
|
1472
|
-
this.
|
|
1427
|
+
_setOriginForCurrentEventQueue(origin) {
|
|
1428
|
+
this._origin = origin;
|
|
1429
|
+
setTimeout(() => this._origin = null, 0);
|
|
1473
1430
|
}
|
|
1474
1431
|
/**
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1478
|
-
* @param {?} delta
|
|
1479
|
-
* @param {?=} items
|
|
1480
|
-
* @return {?}
|
|
1432
|
+
* Checks whether the given focus event was caused by a touchstart event.
|
|
1433
|
+
* @param {?} event The focus event to check.
|
|
1434
|
+
* @return {?} Whether the event was caused by a touch.
|
|
1481
1435
|
*/
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1436
|
+
_wasCausedByTouch(event) {
|
|
1437
|
+
// Note(mmalerba): This implementation is not quite perfect, there is a small edge case.
|
|
1438
|
+
// Consider the following dom structure:
|
|
1439
|
+
//
|
|
1440
|
+
// <div #parent tabindex="0" cdkFocusClasses>
|
|
1441
|
+
// <div #child (click)="#parent.focus()"></div>
|
|
1442
|
+
// </div>
|
|
1443
|
+
//
|
|
1444
|
+
// If the user touches the #child element and the #parent is programmatically focused as a
|
|
1445
|
+
// result, this code will still consider it to have been caused by the touch event and will
|
|
1446
|
+
// apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
|
|
1447
|
+
// relatively small edge-case that can be worked around by using
|
|
1448
|
+
// focusVia(parentEl, renderer, 'program') to focus the parent element.
|
|
1449
|
+
//
|
|
1450
|
+
// If we decide that we absolutely must handle this case correctly, we can do so by listening
|
|
1451
|
+
// for the first focus event after the touchstart, and then the first blur event after that
|
|
1452
|
+
// focus event. When that blur event fires we know that whatever follows is not a result of the
|
|
1453
|
+
// touchstart.
|
|
1454
|
+
let /** @type {?} */ focusTarget = event.target;
|
|
1455
|
+
return this._lastTouchTarget instanceof Node && focusTarget instanceof Node &&
|
|
1456
|
+
(focusTarget === this._lastTouchTarget || focusTarget.contains(this._lastTouchTarget));
|
|
1485
1457
|
}
|
|
1486
1458
|
/**
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1490
|
-
* @param {?} delta
|
|
1491
|
-
* @param {?} items
|
|
1459
|
+
* Handles focus events on a registered element.
|
|
1460
|
+
* @param {?} event The focus event.
|
|
1461
|
+
* @param {?} element The monitored element.
|
|
1492
1462
|
* @return {?}
|
|
1493
1463
|
*/
|
|
1494
|
-
|
|
1495
|
-
//
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
//
|
|
1499
|
-
|
|
1500
|
-
|
|
1464
|
+
_onFocus(event, element) {
|
|
1465
|
+
// NOTE(mmalerba): We currently set the classes based on the focus origin of the most recent
|
|
1466
|
+
// focus event affecting the monitored element. If we want to use the origin of the first event
|
|
1467
|
+
// instead we should check for the cdk-focused class here and return if the element already has
|
|
1468
|
+
// it. (This only matters for elements that have includesChildren = true).
|
|
1469
|
+
// If we are not counting child-element-focus as focused, make sure that the event target is the
|
|
1470
|
+
// monitored element itself.
|
|
1471
|
+
const /** @type {?} */ elementInfo = this._elementInfo.get(element);
|
|
1472
|
+
if (!elementInfo || (!elementInfo.checkChildren && element !== event.target)) {
|
|
1473
|
+
return;
|
|
1501
1474
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1475
|
+
// If we couldn't detect a cause for the focus event, it's due to one of three reasons:
|
|
1476
|
+
// 1) The window has just regained focus, in which case we want to restore the focused state of
|
|
1477
|
+
// the element from before the window blurred.
|
|
1478
|
+
// 2) It was caused by a touch event, in which case we mark the origin as 'touch'.
|
|
1479
|
+
// 3) The element was programmatically focused, in which case we should mark the origin as
|
|
1480
|
+
// 'program'.
|
|
1481
|
+
if (!this._origin) {
|
|
1482
|
+
if (this._windowFocused && this._lastFocusOrigin) {
|
|
1483
|
+
this._origin = this._lastFocusOrigin;
|
|
1484
|
+
}
|
|
1485
|
+
else if (this._wasCausedByTouch(event)) {
|
|
1486
|
+
this._origin = 'touch';
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
this._origin = 'program';
|
|
1490
|
+
}
|
|
1504
1491
|
}
|
|
1492
|
+
this._setClasses(element, this._origin);
|
|
1493
|
+
elementInfo.subject.next(this._origin);
|
|
1494
|
+
this._lastFocusOrigin = this._origin;
|
|
1495
|
+
this._origin = null;
|
|
1505
1496
|
}
|
|
1506
1497
|
/**
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1509
|
-
*
|
|
1510
|
-
* @param {?} delta
|
|
1511
|
-
* @param {?} items
|
|
1512
|
-
* @return {?}
|
|
1513
|
-
*/
|
|
1514
|
-
_setActiveInDefaultMode(delta, items) {
|
|
1515
|
-
this._setActiveItemByIndex(this._activeItemIndex + delta, delta, items);
|
|
1516
|
-
}
|
|
1517
|
-
/**
|
|
1518
|
-
* Sets the active item to the first enabled item starting at the index specified. If the
|
|
1519
|
-
* item is disabled, it will move in the fallbackDelta direction until it either
|
|
1520
|
-
* finds an enabled item or encounters the end of the list.
|
|
1521
|
-
* @param {?} index
|
|
1522
|
-
* @param {?} fallbackDelta
|
|
1523
|
-
* @param {?=} items
|
|
1498
|
+
* Handles blur events on a registered element.
|
|
1499
|
+
* @param {?} event The blur event.
|
|
1500
|
+
* @param {?} element The monitored element.
|
|
1524
1501
|
* @return {?}
|
|
1525
1502
|
*/
|
|
1526
|
-
|
|
1527
|
-
|
|
1503
|
+
_onBlur(event, element) {
|
|
1504
|
+
// If we are counting child-element-focus as focused, make sure that we aren't just blurring in
|
|
1505
|
+
// order to focus another child of the monitored element.
|
|
1506
|
+
const /** @type {?} */ elementInfo = this._elementInfo.get(element);
|
|
1507
|
+
if (!elementInfo || (elementInfo.checkChildren && event.relatedTarget instanceof Node &&
|
|
1508
|
+
element.contains(event.relatedTarget))) {
|
|
1528
1509
|
return;
|
|
1529
1510
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
if (!items[index]) {
|
|
1533
|
-
return;
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
this.setActiveItem(index);
|
|
1511
|
+
this._setClasses(element);
|
|
1512
|
+
elementInfo.subject.next(null);
|
|
1537
1513
|
}
|
|
1538
1514
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1515
|
+
FocusMonitor.decorators = [
|
|
1516
|
+
{ type: Injectable },
|
|
1517
|
+
];
|
|
1518
|
+
/**
|
|
1519
|
+
* @nocollapse
|
|
1520
|
+
*/
|
|
1521
|
+
FocusMonitor.ctorParameters = () => [
|
|
1522
|
+
{ type: NgZone, },
|
|
1523
|
+
{ type: Platform, },
|
|
1524
|
+
];
|
|
1525
|
+
/**
|
|
1526
|
+
* Directive that determines how a particular element was focused (via keyboard, mouse, touch, or
|
|
1527
|
+
* programmatically) and adds corresponding classes to the element.
|
|
1528
|
+
*
|
|
1529
|
+
* There are two variants of this directive:
|
|
1530
|
+
* 1) cdkMonitorElementFocus: does not consider an element to be focused if one of its children is
|
|
1531
|
+
* focused.
|
|
1532
|
+
* 2) cdkMonitorSubtreeFocus: considers an element focused if it or any of its children are focused.
|
|
1533
|
+
*/
|
|
1534
|
+
class CdkMonitorFocus {
|
|
1535
|
+
/**
|
|
1536
|
+
* @param {?} _elementRef
|
|
1537
|
+
* @param {?} _focusMonitor
|
|
1538
|
+
* @param {?} renderer
|
|
1539
|
+
*/
|
|
1540
|
+
constructor(_elementRef, _focusMonitor, renderer) {
|
|
1541
|
+
this._elementRef = _elementRef;
|
|
1542
|
+
this._focusMonitor = _focusMonitor;
|
|
1543
|
+
this.cdkFocusChange = new EventEmitter();
|
|
1544
|
+
this._monitorSubscription = this._focusMonitor.monitor(this._elementRef.nativeElement, renderer, this._elementRef.nativeElement.hasAttribute('cdkMonitorSubtreeFocus'))
|
|
1545
|
+
.subscribe(origin => this.cdkFocusChange.emit(origin));
|
|
1546
|
+
}
|
|
1541
1547
|
/**
|
|
1542
|
-
* This method sets the active item to the item at the specified index.
|
|
1543
|
-
* It also adds active styles to the newly active item and removes active
|
|
1544
|
-
* styles from the previously active item.
|
|
1545
|
-
* @param {?} index
|
|
1546
1548
|
* @return {?}
|
|
1547
1549
|
*/
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
this.activeItem.setInactiveStyles();
|
|
1552
|
-
}
|
|
1553
|
-
super.setActiveItem(index);
|
|
1554
|
-
if (this.activeItem) {
|
|
1555
|
-
this.activeItem.setActiveStyles();
|
|
1556
|
-
}
|
|
1557
|
-
});
|
|
1550
|
+
ngOnDestroy() {
|
|
1551
|
+
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
|
|
1552
|
+
this._monitorSubscription.unsubscribe();
|
|
1558
1553
|
}
|
|
1559
1554
|
}
|
|
1560
|
-
|
|
1555
|
+
CdkMonitorFocus.decorators = [
|
|
1556
|
+
{ type: Directive, args: [{
|
|
1557
|
+
selector: '[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]',
|
|
1558
|
+
},] },
|
|
1559
|
+
];
|
|
1561
1560
|
/**
|
|
1562
|
-
*
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1561
|
+
* @nocollapse
|
|
1562
|
+
*/
|
|
1563
|
+
CdkMonitorFocus.ctorParameters = () => [
|
|
1564
|
+
{ type: ElementRef, },
|
|
1565
|
+
{ type: FocusMonitor, },
|
|
1566
|
+
{ type: Renderer2, },
|
|
1567
|
+
];
|
|
1568
|
+
CdkMonitorFocus.propDecorators = {
|
|
1569
|
+
'cdkFocusChange': [{ type: Output },],
|
|
1570
|
+
};
|
|
1571
|
+
/**
|
|
1572
|
+
* \@docs-private
|
|
1573
|
+
* @param {?} parentDispatcher
|
|
1574
|
+
* @param {?} ngZone
|
|
1575
|
+
* @param {?} platform
|
|
1568
1576
|
* @return {?}
|
|
1569
1577
|
*/
|
|
1570
|
-
function
|
|
1571
|
-
return
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
class FocusKeyManager extends ListKeyManager {
|
|
1575
|
-
/**
|
|
1576
|
-
* This method sets the active item to the item at the specified index.
|
|
1577
|
-
* It also adds focuses the newly active item.
|
|
1578
|
-
* @param {?} index
|
|
1579
|
-
* @return {?}
|
|
1580
|
-
*/
|
|
1581
|
-
setActiveItem(index) {
|
|
1582
|
-
super.setActiveItem(index);
|
|
1583
|
-
if (this.activeItem) {
|
|
1584
|
-
this.activeItem.focus();
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1578
|
+
function FOCUS_MONITOR_PROVIDER_FACTORY(parentDispatcher, ngZone, platform) {
|
|
1579
|
+
return parentDispatcher || new FocusMonitor(ngZone, platform);
|
|
1587
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* \@docs-private
|
|
1583
|
+
*/
|
|
1584
|
+
const FOCUS_MONITOR_PROVIDER = {
|
|
1585
|
+
// If there is already a FocusMonitor available, use that. Otherwise, provide a new one.
|
|
1586
|
+
provide: FocusMonitor,
|
|
1587
|
+
deps: [[new Optional(), new SkipSelf(), FocusMonitor], NgZone, Platform],
|
|
1588
|
+
useFactory: FOCUS_MONITOR_PROVIDER_FACTORY
|
|
1589
|
+
};
|
|
1588
1590
|
|
|
1589
1591
|
class A11yModule {
|
|
1590
1592
|
}
|
|
@@ -1612,5 +1614,5 @@ A11yModule.ctorParameters = () => [];
|
|
|
1612
1614
|
* Generated bundle index. Do not edit.
|
|
1613
1615
|
*/
|
|
1614
1616
|
|
|
1615
|
-
export {
|
|
1617
|
+
export { ActiveDescendantKeyManager, MESSAGES_CONTAINER_ID, CDK_DESCRIBEDBY_ID_PREFIX, CDK_DESCRIBEDBY_HOST_ATTRIBUTE, AriaDescriber, ARIA_DESCRIBER_PROVIDER_FACTORY, ARIA_DESCRIBER_PROVIDER, isFakeMousedownFromScreenReader, FocusKeyManager, FocusTrap, FocusTrapFactory, FocusTrapDeprecatedDirective, FocusTrapDirective, InteractivityChecker, ListKeyManager, LIVE_ANNOUNCER_ELEMENT_TOKEN, LiveAnnouncer, LIVE_ANNOUNCER_PROVIDER_FACTORY, LIVE_ANNOUNCER_PROVIDER, TOUCH_BUFFER_MS, FocusMonitor, CdkMonitorFocus, FOCUS_MONITOR_PROVIDER_FACTORY, FOCUS_MONITOR_PROVIDER, A11yModule };
|
|
1616
1618
|
//# sourceMappingURL=a11y.js.map
|