@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.
Files changed (306) hide show
  1. package/_a11y.scss +23 -0
  2. package/_overlay.scss +93 -0
  3. package/a11y/index.metadata.json +2 -1
  4. package/a11y/typings/a11y-module.d.ts +2 -0
  5. package/a11y/typings/focus-monitor.d.ts +1 -1
  6. package/a11y/typings/index.d.ts +1 -1
  7. package/a11y/typings/index.metadata.json +1 -1
  8. package/a11y/typings/{public_api.d.ts → public-api.d.ts} +8 -2
  9. package/a11y-prebuilt.css +1 -0
  10. package/a11y.metadata.json +2 -1
  11. package/bidi/index.metadata.json +2 -1
  12. package/bidi/typings/bidi-module.d.ts +2 -0
  13. package/bidi/typings/index.d.ts +1 -1
  14. package/bidi/typings/index.metadata.json +1 -1
  15. package/bidi/typings/public-api.d.ts +10 -0
  16. package/bidi.metadata.json +2 -1
  17. package/bundles/cdk-a11y.umd.js +1368 -1357
  18. package/bundles/cdk-a11y.umd.js.map +1 -1
  19. package/bundles/cdk-a11y.umd.min.js +2 -2
  20. package/bundles/cdk-a11y.umd.min.js.map +1 -1
  21. package/bundles/cdk-bidi.umd.js +42 -40
  22. package/bundles/cdk-bidi.umd.js.map +1 -1
  23. package/bundles/cdk-bidi.umd.min.js +2 -2
  24. package/bundles/cdk-bidi.umd.min.js.map +1 -1
  25. package/bundles/cdk-coercion.umd.js +12 -0
  26. package/bundles/cdk-coercion.umd.js.map +1 -1
  27. package/bundles/cdk-coercion.umd.min.js +2 -2
  28. package/bundles/cdk-coercion.umd.min.js.map +1 -1
  29. package/bundles/cdk-collections.umd.js +113 -11
  30. package/bundles/cdk-collections.umd.js.map +1 -1
  31. package/bundles/cdk-collections.umd.min.js +2 -2
  32. package/bundles/cdk-collections.umd.min.js.map +1 -1
  33. package/bundles/cdk-keycodes.umd.js.map +1 -1
  34. package/bundles/cdk-keycodes.umd.min.js +1 -1
  35. package/bundles/cdk-keycodes.umd.min.js.map +1 -1
  36. package/bundles/cdk-layout.umd.js +235 -0
  37. package/bundles/cdk-layout.umd.js.map +1 -0
  38. package/bundles/cdk-layout.umd.min.js +9 -0
  39. package/bundles/cdk-layout.umd.min.js.map +1 -0
  40. package/bundles/cdk-observers.umd.js +41 -40
  41. package/bundles/cdk-observers.umd.js.map +1 -1
  42. package/bundles/cdk-observers.umd.min.js +2 -2
  43. package/bundles/cdk-observers.umd.min.js.map +1 -1
  44. package/bundles/cdk-overlay.umd.js +306 -265
  45. package/bundles/cdk-overlay.umd.js.map +1 -1
  46. package/bundles/cdk-overlay.umd.min.js +2 -2
  47. package/bundles/cdk-overlay.umd.min.js.map +1 -1
  48. package/bundles/cdk-platform.umd.js +19 -17
  49. package/bundles/cdk-platform.umd.js.map +1 -1
  50. package/bundles/cdk-platform.umd.min.js +2 -2
  51. package/bundles/cdk-platform.umd.min.js.map +1 -1
  52. package/bundles/cdk-portal.umd.js +71 -37
  53. package/bundles/cdk-portal.umd.js.map +1 -1
  54. package/bundles/cdk-portal.umd.min.js +2 -2
  55. package/bundles/cdk-portal.umd.min.js.map +1 -1
  56. package/bundles/cdk-rxjs.umd.js +13 -4
  57. package/bundles/cdk-rxjs.umd.js.map +1 -1
  58. package/bundles/cdk-rxjs.umd.min.js +2 -2
  59. package/bundles/cdk-rxjs.umd.min.js.map +1 -1
  60. package/bundles/cdk-scrolling.umd.js +89 -54
  61. package/bundles/cdk-scrolling.umd.js.map +1 -1
  62. package/bundles/cdk-scrolling.umd.min.js +2 -2
  63. package/bundles/cdk-scrolling.umd.min.js.map +1 -1
  64. package/bundles/cdk-stepper.umd.js +115 -90
  65. package/bundles/cdk-stepper.umd.js.map +1 -1
  66. package/bundles/cdk-stepper.umd.min.js +2 -2
  67. package/bundles/cdk-stepper.umd.min.js.map +1 -1
  68. package/bundles/cdk-table.umd.js +261 -218
  69. package/bundles/cdk-table.umd.js.map +1 -1
  70. package/bundles/cdk-table.umd.min.js +2 -2
  71. package/bundles/cdk-table.umd.min.js.map +1 -1
  72. package/bundles/cdk.umd.js +1 -1
  73. package/bundles/cdk.umd.js.map +1 -1
  74. package/bundles/cdk.umd.min.js +2 -2
  75. package/bundles/cdk.umd.min.js.map +1 -1
  76. package/cdk.metadata.json +2 -1
  77. package/coercion/index.metadata.json +2 -1
  78. package/coercion/typings/array.d.ts +9 -0
  79. package/coercion/typings/index.d.ts +1 -1
  80. package/coercion/typings/index.metadata.json +1 -1
  81. package/{typings/coercion/public_api.d.ts → coercion/typings/public-api.d.ts} +1 -0
  82. package/coercion.metadata.json +2 -1
  83. package/collections/index.metadata.json +2 -1
  84. package/collections/typings/index.d.ts +2 -1
  85. package/collections/typings/index.metadata.json +1 -1
  86. package/{typings/collections/public_api.d.ts → collections/typings/public-api.d.ts} +1 -0
  87. package/collections/typings/selection.d.ts +13 -3
  88. package/collections/typings/unique-selection-dispatcher.d.ts +40 -0
  89. package/collections.metadata.json +2 -1
  90. package/esm2015/a11y.js +1252 -1250
  91. package/esm2015/a11y.js.map +1 -1
  92. package/esm2015/bidi.js +1 -1
  93. package/esm2015/bidi.js.map +1 -1
  94. package/esm2015/cdk.js +1 -1
  95. package/esm2015/cdk.js.map +1 -1
  96. package/esm2015/coercion.js +11 -1
  97. package/esm2015/coercion.js.map +1 -1
  98. package/esm2015/collections.js +93 -8
  99. package/esm2015/collections.js.map +1 -1
  100. package/esm2015/keycodes.js.map +1 -1
  101. package/esm2015/layout.js +226 -0
  102. package/esm2015/layout.js.map +1 -0
  103. package/esm2015/observers.js +8 -7
  104. package/esm2015/observers.js.map +1 -1
  105. package/esm2015/overlay.js +157 -136
  106. package/esm2015/overlay.js.map +1 -1
  107. package/esm2015/platform.js +1 -1
  108. package/esm2015/platform.js.map +1 -1
  109. package/esm2015/portal.js +30 -1
  110. package/esm2015/portal.js.map +1 -1
  111. package/esm2015/rxjs.js +5 -1
  112. package/esm2015/rxjs.js.map +1 -1
  113. package/esm2015/scrolling.js +39 -8
  114. package/esm2015/scrolling.js.map +1 -1
  115. package/esm2015/stepper.js +27 -5
  116. package/esm2015/stepper.js.map +1 -1
  117. package/esm2015/table.js +68 -29
  118. package/esm2015/table.js.map +1 -1
  119. package/esm5/a11y.es5.js +1372 -1357
  120. package/esm5/a11y.es5.js.map +1 -1
  121. package/esm5/bidi.es5.js +45 -40
  122. package/esm5/bidi.es5.js.map +1 -1
  123. package/esm5/cdk.es5.js +4 -1
  124. package/esm5/cdk.es5.js.map +1 -1
  125. package/esm5/coercion.es5.js +14 -1
  126. package/esm5/coercion.es5.js.map +1 -1
  127. package/esm5/collections.es5.js +110 -8
  128. package/esm5/collections.es5.js.map +1 -1
  129. package/esm5/keycodes.es5.js +2 -0
  130. package/esm5/keycodes.es5.js.map +1 -1
  131. package/esm5/layout.es5.js +234 -0
  132. package/esm5/layout.es5.js.map +1 -0
  133. package/esm5/observers.es5.js +44 -40
  134. package/esm5/observers.es5.js.map +1 -1
  135. package/esm5/overlay.es5.js +304 -259
  136. package/esm5/overlay.es5.js.map +1 -1
  137. package/esm5/platform.es5.js +22 -17
  138. package/esm5/platform.es5.js.map +1 -1
  139. package/esm5/portal.es5.js +81 -44
  140. package/esm5/portal.es5.js.map +1 -1
  141. package/esm5/rxjs.es5.js +12 -1
  142. package/esm5/rxjs.es5.js.map +1 -1
  143. package/esm5/scrolling.es5.js +89 -51
  144. package/esm5/scrolling.es5.js.map +1 -1
  145. package/esm5/stepper.es5.js +119 -91
  146. package/esm5/stepper.es5.js.map +1 -1
  147. package/esm5/table.es5.js +265 -218
  148. package/esm5/table.es5.js.map +1 -1
  149. package/keycodes/index.metadata.json +2 -1
  150. package/keycodes/typings/index.d.ts +1 -1
  151. package/keycodes/typings/{public_api.d.ts → public-api.d.ts} +0 -0
  152. package/keycodes.metadata.json +2 -1
  153. package/layout/index.d.ts +8 -0
  154. package/layout/index.metadata.json +12 -0
  155. package/layout/package.json +7 -0
  156. package/layout/typings/breakpoints-observer.d.ts +37 -0
  157. package/layout/typings/breakpoints.d.ts +18 -0
  158. package/layout/typings/index.d.ts +4 -0
  159. package/layout/typings/index.metadata.json +1 -0
  160. package/layout/typings/media-matcher.d.ts +15 -0
  161. package/layout/typings/public-api.d.ts +5 -0
  162. package/layout.d.ts +8 -0
  163. package/layout.metadata.json +12 -0
  164. package/observers/index.metadata.json +2 -1
  165. package/observers/typings/index.d.ts +1 -1
  166. package/observers/typings/index.metadata.json +1 -1
  167. package/observers/typings/observe-content.d.ts +3 -3
  168. package/observers/typings/{public_api.d.ts → public-api.d.ts} +0 -0
  169. package/observers.metadata.json +2 -1
  170. package/overlay/index.metadata.json +2 -1
  171. package/overlay/typings/index.d.ts +2 -2
  172. package/overlay/typings/index.metadata.json +1 -1
  173. package/overlay/typings/overlay-config.d.ts +1 -1
  174. package/overlay/typings/overlay-directives.d.ts +3 -3
  175. package/overlay/typings/overlay-module.d.ts +11 -0
  176. package/overlay/typings/overlay-ref.d.ts +6 -6
  177. package/overlay/typings/overlay.d.ts +2 -2
  178. package/overlay/typings/position/connected-position-strategy.d.ts +5 -0
  179. package/overlay/typings/position/position-strategy.d.ts +2 -0
  180. package/overlay/typings/{public_api.d.ts → public-api.d.ts} +1 -4
  181. package/overlay/typings/scroll/scroll-strategy.d.ts +1 -1
  182. package/overlay-prebuilt.css +1 -0
  183. package/overlay.metadata.json +2 -1
  184. package/package.json +3 -3
  185. package/platform/index.metadata.json +2 -1
  186. package/platform/typings/index.d.ts +1 -1
  187. package/platform/typings/index.metadata.json +1 -1
  188. package/platform/typings/platform-module.d.ts +2 -0
  189. package/platform/typings/public-api.d.ts +10 -0
  190. package/platform.metadata.json +2 -1
  191. package/portal/index.metadata.json +2 -1
  192. package/portal/typings/index.d.ts +1 -1
  193. package/portal/typings/index.metadata.json +1 -1
  194. package/portal/typings/{public_api.d.ts → public-api.d.ts} +1 -0
  195. package/portal.metadata.json +2 -1
  196. package/rxjs/index.metadata.json +2 -1
  197. package/rxjs/typings/index.d.ts +1 -1
  198. package/rxjs/typings/index.metadata.json +1 -1
  199. package/rxjs/typings/{public_api.d.ts → public-api.d.ts} +0 -0
  200. package/rxjs/typings/rx-operators.d.ts +7 -0
  201. package/rxjs.metadata.json +2 -1
  202. package/scrolling/index.metadata.json +2 -1
  203. package/scrolling/typings/index.d.ts +1 -1
  204. package/scrolling/typings/index.metadata.json +1 -1
  205. package/scrolling/typings/public-api.d.ts +11 -0
  206. package/scrolling/typings/scrolling-module.d.ts +2 -0
  207. package/scrolling/typings/viewport-ruler.d.ts +20 -6
  208. package/scrolling.metadata.json +2 -1
  209. package/stepper/index.metadata.json +2 -1
  210. package/stepper/typings/index.d.ts +1 -1
  211. package/stepper/typings/index.metadata.json +1 -1
  212. package/stepper/typings/public-api.d.ts +11 -0
  213. package/stepper/typings/stepper-module.d.ts +2 -0
  214. package/stepper/typings/stepper.d.ts +8 -4
  215. package/stepper.metadata.json +2 -1
  216. package/table/index.metadata.json +2 -1
  217. package/table/typings/index.d.ts +1 -1
  218. package/table/typings/index.metadata.json +1 -1
  219. package/table/typings/public-api.d.ts +13 -0
  220. package/table/typings/row.d.ts +11 -3
  221. package/table/typings/table-errors.d.ts +10 -0
  222. package/table/typings/table-module.d.ts +2 -0
  223. package/table/typings/table.d.ts +17 -8
  224. package/table.metadata.json +2 -1
  225. package/typings/a11y/a11y-module.d.ts +2 -0
  226. package/typings/a11y/focus-monitor.d.ts +1 -1
  227. package/typings/a11y/index.d.ts +1 -1
  228. package/typings/a11y/index.metadata.json +1 -1
  229. package/typings/a11y/{public_api.d.ts → public-api.d.ts} +8 -2
  230. package/typings/bidi/bidi-module.d.ts +2 -0
  231. package/typings/bidi/index.d.ts +1 -1
  232. package/typings/bidi/index.metadata.json +1 -1
  233. package/typings/bidi/public-api.d.ts +10 -0
  234. package/typings/coercion/array.d.ts +9 -0
  235. package/typings/coercion/index.d.ts +1 -1
  236. package/typings/coercion/index.metadata.json +1 -1
  237. package/{coercion/typings/public_api.d.ts → typings/coercion/public-api.d.ts} +1 -0
  238. package/typings/collections/index.d.ts +2 -1
  239. package/typings/collections/index.metadata.json +1 -1
  240. package/{collections/typings/public_api.d.ts → typings/collections/public-api.d.ts} +1 -0
  241. package/typings/collections/selection.d.ts +13 -3
  242. package/typings/collections/unique-selection-dispatcher.d.ts +40 -0
  243. package/typings/index.d.ts +1 -1
  244. package/typings/index.metadata.json +1 -1
  245. package/typings/keycodes/index.d.ts +1 -1
  246. package/typings/keycodes/{public_api.d.ts → public-api.d.ts} +0 -0
  247. package/typings/layout/breakpoints-observer.d.ts +37 -0
  248. package/typings/layout/breakpoints.d.ts +18 -0
  249. package/typings/layout/index.d.ts +4 -0
  250. package/typings/layout/index.metadata.json +1 -0
  251. package/typings/layout/media-matcher.d.ts +15 -0
  252. package/typings/layout/public-api.d.ts +5 -0
  253. package/typings/observers/index.d.ts +1 -1
  254. package/typings/observers/index.metadata.json +1 -1
  255. package/typings/observers/observe-content.d.ts +3 -3
  256. package/typings/observers/{public_api.d.ts → public-api.d.ts} +0 -0
  257. package/typings/overlay/index.d.ts +2 -2
  258. package/typings/overlay/index.metadata.json +1 -1
  259. package/typings/overlay/overlay-config.d.ts +1 -1
  260. package/typings/overlay/overlay-directives.d.ts +3 -3
  261. package/typings/overlay/overlay-module.d.ts +11 -0
  262. package/typings/overlay/overlay-ref.d.ts +6 -6
  263. package/typings/overlay/overlay.d.ts +2 -2
  264. package/typings/overlay/position/connected-position-strategy.d.ts +5 -0
  265. package/typings/overlay/position/position-strategy.d.ts +2 -0
  266. package/typings/overlay/{public_api.d.ts → public-api.d.ts} +1 -4
  267. package/typings/overlay/scroll/scroll-strategy.d.ts +1 -1
  268. package/typings/platform/index.d.ts +1 -1
  269. package/typings/platform/index.metadata.json +1 -1
  270. package/typings/platform/platform-module.d.ts +2 -0
  271. package/typings/platform/public-api.d.ts +10 -0
  272. package/typings/portal/index.d.ts +1 -1
  273. package/typings/portal/index.metadata.json +1 -1
  274. package/typings/portal/{public_api.d.ts → public-api.d.ts} +1 -0
  275. package/typings/{public_api.d.ts → public-api.d.ts} +0 -0
  276. package/typings/rxjs/index.d.ts +1 -1
  277. package/typings/rxjs/index.metadata.json +1 -1
  278. package/typings/rxjs/{public_api.d.ts → public-api.d.ts} +0 -0
  279. package/typings/rxjs/rx-operators.d.ts +7 -0
  280. package/typings/scrolling/index.d.ts +1 -1
  281. package/typings/scrolling/index.metadata.json +1 -1
  282. package/typings/scrolling/public-api.d.ts +11 -0
  283. package/typings/scrolling/scrolling-module.d.ts +2 -0
  284. package/typings/scrolling/viewport-ruler.d.ts +20 -6
  285. package/typings/stepper/index.d.ts +1 -1
  286. package/typings/stepper/index.metadata.json +1 -1
  287. package/typings/stepper/public-api.d.ts +11 -0
  288. package/typings/stepper/stepper-module.d.ts +2 -0
  289. package/typings/stepper/stepper.d.ts +8 -4
  290. package/typings/table/index.d.ts +1 -1
  291. package/typings/table/index.metadata.json +1 -1
  292. package/typings/table/public-api.d.ts +13 -0
  293. package/typings/table/row.d.ts +11 -3
  294. package/typings/table/table-errors.d.ts +10 -0
  295. package/typings/table/table-module.d.ts +2 -0
  296. package/typings/table/table.d.ts +17 -8
  297. package/bidi/typings/public_api.d.ts +0 -4
  298. package/platform/typings/public_api.d.ts +0 -4
  299. package/scrolling/typings/public_api.d.ts +0 -5
  300. package/stepper/typings/public_api.d.ts +0 -5
  301. package/table/typings/public_api.d.ts +0 -7
  302. package/typings/bidi/public_api.d.ts +0 -4
  303. package/typings/platform/public_api.d.ts +0 -4
  304. package/typings/scrolling/public_api.d.ts +0 -5
  305. package/typings/stepper/public_api.d.ts +0 -5
  306. 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
- * Utility for checking the interactivity of an element, such as whether is is focusable or
20
- * tabbable.
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 InteractivityChecker {
22
+ class ListKeyManager {
23
23
  /**
24
- * @param {?} _platform
24
+ * @param {?} _items
25
25
  */
26
- constructor(_platform) {
27
- this._platform = _platform;
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
- * Gets whether an element is disabled.
31
- *
32
- * @param {?} element Element to be checked.
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
- isDisabled(element) {
36
- // This does not capture some cases, such as a non-form control with a disabled attribute or
37
- // a form control inside of a disabled form, but should capture the most common cases.
38
- return element.hasAttribute('disabled');
44
+ withWrap() {
45
+ this._wrap = true;
46
+ return this;
39
47
  }
40
48
  /**
41
- * Gets whether an element is visible for the purposes of interactivity.
42
- *
43
- * This will capture states like `display: none` and `visibility: hidden`, but not things like
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
- isVisible(element) {
50
- return hasGeometry(element) && getComputedStyle(element).visibility === 'visible';
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
- * Gets whether an element can be reached via Tab key.
54
- * Assumes that the element has already been checked with isFocusable.
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
- isTabbable(element) {
60
- // Nothing is tabbable on the the server 😎
61
- if (!this._platform.isBrowser) {
62
- return false;
63
- }
64
- let /** @type {?} */ frameElement = (getWindow(element).frameElement);
65
- if (frameElement) {
66
- let /** @type {?} */ frameType = frameElement && frameElement.nodeName.toLowerCase();
67
- // Frame elements inherit their tabindex onto all child elements.
68
- if (getTabIndexValue(frameElement) === -1) {
69
- return false;
70
- }
71
- // Webkit and Blink consider anything inside of an <object> element as non-tabbable.
72
- if ((this._platform.BLINK || this._platform.WEBKIT) && frameType === 'object') {
73
- return false;
74
- }
75
- // Webkit and Blink disable tabbing to an element inside of an invisible frame.
76
- if ((this._platform.BLINK || this._platform.WEBKIT) && !this.isVisible(frameElement)) {
77
- return false;
78
- }
79
- }
80
- let /** @type {?} */ nodeName = element.nodeName.toLowerCase();
81
- let /** @type {?} */ tabIndexValue = getTabIndexValue(element);
82
- if (element.hasAttribute('contenteditable')) {
83
- return tabIndexValue !== -1;
84
- }
85
- if (nodeName === 'iframe') {
86
- // The frames may be tabbable depending on content, but it's not possibly to reliably
87
- // investigate the content of the frames.
88
- return false;
89
- }
90
- if (nodeName === 'audio') {
91
- if (!element.hasAttribute('controls')) {
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
- return element.tabIndex >= 0;
121
+ this._pressedLetters = [];
122
+ event.preventDefault();
119
123
  }
120
124
  /**
121
- * Gets whether an element can be focused by the user.
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
- isFocusable(element) {
127
- // Perform checks in order of left to most expensive.
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
- * Whether the focus trap is active.
132
+ * The active item.
291
133
  * @return {?}
292
134
  */
293
- get enabled() { return this._enabled; }
135
+ get activeItem() {
136
+ return this._activeItem;
137
+ }
294
138
  /**
295
- * @param {?} val
139
+ * Sets the active item to the first enabled item in the list.
296
140
  * @return {?}
297
141
  */
298
- set enabled(val) {
299
- this._enabled = val;
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
- * Destroys the focus trap by cleaning up the anchors.
146
+ * Sets the active item to the last enabled item in the list.
306
147
  * @return {?}
307
148
  */
308
- destroy() {
309
- if (this._startAnchor && this._startAnchor.parentNode) {
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
- * Inserts the anchors into the DOM. This is usually done automatically
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
- attachAnchors() {
323
- // If we're not on the browser, there can be no focus to trap.
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
- * Waits for the zone to stabilize, then either focuses the first element that the
348
- * user specified, or the first tabbable element.
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
- focusInitialElementWhenReady() {
353
- return new Promise(resolve => {
354
- this._executeOnStable(() => resolve(this.focusInitialElement()));
355
- });
163
+ setPreviousItemActive() {
164
+ this._activeItemIndex < 0 && this._wrap ? this.setLastItemActive()
165
+ : this._setActiveItemByDelta(-1);
356
166
  }
357
167
  /**
358
- * Waits for the zone to stabilize, then focuses
359
- * the first tabbable element within the focus trap region.
360
- * @return {?} Returns a promise that resolves with a boolean, depending
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
- focusFirstTabbableElementWhenReady() {
364
- return new Promise(resolve => {
365
- this._executeOnStable(() => resolve(this.focusFirstTabbableElement()));
366
- });
172
+ updateActiveItemIndex(index) {
173
+ this._activeItemIndex = index;
367
174
  }
368
175
  /**
369
- * Waits for the zone to stabilize, then focuses
370
- * the last tabbable element within the focus trap region.
371
- * @return {?} Returns a promise that resolves with a boolean, depending
372
- * on whether focus was moved successfuly.
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
- focusLastTabbableElementWhenReady() {
375
- return new Promise(resolve => {
376
- this._executeOnStable(() => resolve(this.focusLastTabbableElement()));
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
- * Get the specified boundary element of the trapped region.
381
- * @param {?} bound The boundary to get (start or end of trapped region).
382
- * @return {?} The boundary element.
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
- _getRegionBoundary(bound) {
385
- // Contains the deprecated version of selector, for temporary backwards comparability.
386
- let /** @type {?} */ markers = (this._element.querySelectorAll(`[cdk-focus-region-${bound}], ` +
387
- `[cdk-focus-${bound}]`));
388
- for (let /** @type {?} */ i = 0; i < markers.length; i++) {
389
- if (markers[i].hasAttribute(`cdk-focus-${bound}`)) {
390
- console.warn(`Found use of deprecated attribute 'cdk-focus-${bound}',` +
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
- if (bound == 'start') {
395
- return markers.length ? markers[0] : this._getFirstTabbableElement(this._element);
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
- * Focuses the element that should be focused when the focus trap is initialized.
402
- * @return {?} Returns whether focus was moved successfuly.
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
- focusInitialElement() {
405
- const /** @type {?} */ redirectToElement = (this._element.querySelector('[cdk-focus-initial]'));
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
- * Focuses the first tabbable element within the focus trap region.
414
- * @return {?} Returns whether focus was moved successfuly.
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
- focusFirstTabbableElement() {
417
- const /** @type {?} */ redirectToElement = this._getRegionBoundary('start');
418
- if (redirectToElement) {
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
- // Iterate in DOM order. Note that IE doesn't have `children` for SVG so we fall
444
- // back to `childNodes` which includes text nodes, comments etc.
445
- let /** @type {?} */ children = root.children || root.childNodes;
446
- for (let /** @type {?} */ i = 0; i < children.length; i++) {
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
- return null;
237
+ this.setActiveItem(index);
455
238
  }
239
+ }
240
+
241
+ class ActiveDescendantKeyManager extends ListKeyManager {
456
242
  /**
457
- * Get the last tabbable element from a DOM subtree (inclusive).
458
- * @param {?} root
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
- _getLastTabbableElement(root) {
462
- if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
463
- return root;
249
+ setActiveItem(index) {
250
+ if (this.activeItem) {
251
+ this.activeItem.setInactiveStyles();
464
252
  }
465
- // Iterate in reverse DOM order.
466
- let /** @type {?} */ children = root.children || root.childNodes;
467
- for (let /** @type {?} */ i = children.length - 1; i >= 0; i--) {
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
- * Creates an anchor element.
479
- * @return {?}
337
+ * @param {?} _platform
480
338
  */
481
- _createAnchor() {
482
- let /** @type {?} */ anchor = document.createElement('div');
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
- * Executes a function when the zone is stable.
490
- * @param {?} fn
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
- _executeOnStable(fn) {
494
- if (this._ngZone.isStable) {
495
- fn();
350
+ describe(hostElement, message) {
351
+ if (!this._platform.isBrowser || !message.trim()) {
352
+ return;
496
353
  }
497
- else {
498
- first.call(this._ngZone.onStable.asObservable()).subscribe(fn);
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
- * @param {?} _checker
508
- * @param {?} _platform
509
- * @param {?} _ngZone
362
+ * Removes the host element's aria-describedby reference to the message element.
363
+ * @param {?} hostElement
364
+ * @param {?} message
365
+ * @return {?}
510
366
  */
511
- constructor(_checker, _platform, _ngZone) {
512
- this._checker = _checker;
513
- this._platform = _platform;
514
- this._ngZone = _ngZone;
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
- * @param {?} element
518
- * @param {?=} deferAnchors
383
+ * Unregisters all created message elements and removes the message container.
519
384
  * @return {?}
520
385
  */
521
- create(element, deferAnchors = false) {
522
- return new FocusTrap(element, this._platform, this._checker, this._ngZone, deferAnchors);
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
- FocusTrapFactory.decorators = [
401
+ AriaDescriber.decorators = [
526
402
  { type: Injectable },
527
403
  ];
528
404
  /**
529
405
  * @nocollapse
530
406
  */
531
- FocusTrapFactory.ctorParameters = () => [
532
- { type: InteractivityChecker, },
407
+ AriaDescriber.ctorParameters = () => [
533
408
  { type: Platform, },
534
- { type: NgZone, },
535
409
  ];
536
410
  /**
537
- * Directive for trapping focus within a region.
538
- * @deprecated
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
- class FocusTrapDeprecatedDirective {
541
- /**
542
- * @param {?} _elementRef
543
- * @param {?} _focusTrapFactory
544
- */
545
- constructor(_elementRef, _focusTrapFactory) {
546
- this._elementRef = _elementRef;
547
- this._focusTrapFactory = _focusTrapFactory;
548
- this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
549
- }
550
- /**
551
- * Whether the focus trap is active.
552
- * @return {?}
553
- */
554
- get disabled() { return !this.focusTrap.enabled; }
555
- /**
556
- * @param {?} val
557
- * @return {?}
558
- */
559
- set disabled(val) {
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
- * @nocollapse
440
+ * Creates the global container for all aria-describedby messages.
441
+ * @return {?}
582
442
  */
583
- FocusTrapDeprecatedDirective.ctorParameters = () => [
584
- { type: ElementRef, },
585
- { type: FocusTrapFactory, },
586
- ];
587
- FocusTrapDeprecatedDirective.propDecorators = {
588
- 'disabled': [{ type: Input },],
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
- * Directive for trapping focus within a region.
451
+ * Deletes the global messages container.
452
+ * @return {?}
592
453
  */
593
- class FocusTrapDirective {
594
- /**
595
- * @param {?} _elementRef
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
- * @nocollapse
459
+ * Removes all cdk-describedby messages that are hosted through the element.
460
+ * @param {?} element
461
+ * @return {?}
634
462
  */
635
- FocusTrapDirective.ctorParameters = () => [
636
- { type: ElementRef, },
637
- { type: FocusTrapFactory, },
638
- ];
639
- FocusTrapDirective.propDecorators = {
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
- * @nocollapse
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
- LiveAnnouncer.ctorParameters = () => [
702
- { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [LIVE_ANNOUNCER_ELEMENT_TOKEN,] },] },
703
- { type: Platform, },
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 LIVE_ANNOUNCER_PROVIDER_FACTORY(parentDispatcher, liveElement, platform) {
713
- return parentDispatcher || new LiveAnnouncer(liveElement, platform);
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 LIVE_ANNOUNCER_PROVIDER = {
719
- // If there is already a LiveAnnouncer available, use that. Otherwise, provide a new one.
720
- provide: LiveAnnouncer,
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(), LiveAnnouncer],
723
- [new Optional(), new Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN)],
724
- Platform,
524
+ [new Optional(), new SkipSelf(), AriaDescriber],
525
+ Platform
725
526
  ],
726
- useFactory: LIVE_ANNOUNCER_PROVIDER_FACTORY
527
+ useFactory: ARIA_DESCRIBER_PROVIDER_FACTORY
727
528
  };
728
529
 
729
530
  /**
730
- * IDs are deliminated by an empty space, as per the spec.
731
- */
732
- const ID_DELIMINATOR = ' ';
733
- /**
734
- * Adds the given ID to the specified ARIA attribute on an element.
735
- * Used for attributes such as aria-labelledby, aria-owns, etc.
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 addAriaReferencedId(el, attr, id) {
742
- const /** @type {?} */ ids = getAriaReferenceIds(el, attr);
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
- * Removes the given ID from the specified ARIA attribute on an element.
751
- * Used for attributes such as aria-labelledby, aria-owns, etc.
752
- * @param {?} el
753
- * @param {?} attr
754
- * @param {?} id
755
- * @return {?}
756
- */
757
- function removeAriaReferencedId(el, attr, id) {
758
- const /** @type {?} */ ids = getAriaReferenceIds(el, attr);
759
- const /** @type {?} */ filteredIds = ids.filter(val => val != id.trim());
760
- el.setAttribute(attr, filteredIds.join(ID_DELIMINATOR));
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
- * ID used for the body container where all messages are appended.
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 AriaDescriber {
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
- * Adds to the host element an aria-describedby reference to a hidden element that contains
813
- * the message. If the same message has already been registered, then it will reuse the created
814
- * message element.
815
- * @param {?} hostElement
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
- describe(hostElement, message) {
820
- if (!this._platform.isBrowser || !message.trim()) {
821
- return;
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
- * Removes the host element's aria-describedby reference to the message element.
832
- * @param {?} hostElement
833
- * @param {?} message
834
- * @return {?}
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
- removeDescription(hostElement, message) {
837
- if (!this._platform.isBrowser || !message.trim()) {
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
- * Unregisters all created message elements and removes the message container.
853
- * @return {?}
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
- ngOnDestroy() {
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
- const /** @type {?} */ describedElements = document.querySelectorAll(`[${CDK_DESCRIBEDBY_HOST_ATTRIBUTE}]`);
860
- for (let /** @type {?} */ i = 0; i < describedElements.length; i++) {
861
- removeCdkDescribedByReferenceIds(describedElements[i]);
862
- describedElements[i].removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
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
- if (messagesContainer) {
865
- deleteMessagesContainer();
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
- messageRegistry.clear();
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
- AriaDescriber.decorators = [
672
+ InteractivityChecker.decorators = [
871
673
  { type: Injectable },
872
674
  ];
873
675
  /**
874
676
  * @nocollapse
875
677
  */
876
- AriaDescriber.ctorParameters = () => [
678
+ InteractivityChecker.ctorParameters = () => [
877
679
  { type: Platform, },
878
680
  ];
879
681
  /**
880
- * Creates a new element in the visually hidden message container element with the message
881
- * as its content and adds it to the message registry.
882
- * @param {?} message
682
+ * Checks whether the specified element has any geometry / rectangles.
683
+ * @param {?} element
883
684
  * @return {?}
884
685
  */
885
- function createMessageElement(message) {
886
- const /** @type {?} */ messageElement = document.createElement('div');
887
- messageElement.setAttribute('id', `${CDK_DESCRIBEDBY_ID_PREFIX}-${nextId++}`);
888
- messageElement.appendChild(/** @type {?} */ ((document.createTextNode(message))));
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
- * Deletes the message element from the global messages container.
897
- * @param {?} message
692
+ * Gets whether an element's
693
+ * @param {?} element
898
694
  * @return {?}
899
695
  */
900
- function deleteMessageElement(message) {
901
- const /** @type {?} */ registeredMessage = messageRegistry.get(message);
902
- const /** @type {?} */ messageElement = registeredMessage && registeredMessage.messageElement;
903
- if (messagesContainer && messageElement) {
904
- messagesContainer.removeChild(messageElement);
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
- * Creates the global container for all aria-describedby messages.
704
+ * Gets whether an element is an <input type="hidden">.
705
+ * @param {?} element
910
706
  * @return {?}
911
707
  */
912
- function createMessagesContainer() {
913
- messagesContainer = document.createElement('div');
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
- * Deletes the global messages container.
712
+ * Gets whether an element is an anchor that has an href attribute.
713
+ * @param {?} element
921
714
  * @return {?}
922
715
  */
923
- function deleteMessagesContainer() {
924
- document.body.removeChild(/** @type {?} */ ((messagesContainer)));
925
- messagesContainer = null;
716
+ function isAnchorWithHref(element) {
717
+ return isAnchorElement(element) && element.hasAttribute('href');
926
718
  }
927
719
  /**
928
- * Removes all cdk-describedby messages that are hosted through the element.
720
+ * Gets whether an element is an input element.
929
721
  * @param {?} element
930
722
  * @return {?}
931
723
  */
932
- function removeCdkDescribedByReferenceIds(element) {
933
- // Remove all aria-describedby reference IDs that are prefixed by CDK_DESCRIBEDBY_ID_PREFIX
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
- * Adds a message reference to the element using aria-describedby and increments the registered
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 addMessageReference(element, message) {
946
- const /** @type {?} */ registeredMessage = ((messageRegistry.get(message)));
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
- * Removes a message reference from the element using aria-describedby and decrements the registered
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 removeMessageReference(element, message) {
960
- const /** @type {?} */ registeredMessage = ((messageRegistry.get(message)));
961
- registeredMessage.referenceCount--;
962
- removeAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
963
- element.removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
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 true if the element has been described by the provided message ID.
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 isElementDescribedByMessage(element, message) {
972
- const /** @type {?} */ referenceIds = getAriaReferenceIds(element, 'aria-describedby');
973
- const /** @type {?} */ registeredMessage = messageRegistry.get(message);
974
- const /** @type {?} */ messageId = registeredMessage && registeredMessage.messageElement.id;
975
- return !!messageId && referenceIds.indexOf(messageId) != -1;
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
- * \@docs-private
979
- * @param {?} parentDispatcher
980
- * @param {?} platform
766
+ * Checks whether the specified element is potentially tabbable on iOS
767
+ * @param {?} element
981
768
  * @return {?}
982
769
  */
983
- function ARIA_DESCRIBER_PROVIDER_FACTORY(parentDispatcher, platform) {
984
- return parentDispatcher || new AriaDescriber(platform);
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
- * \@docs-private
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
- const ARIA_DESCRIBER_PROVIDER = {
990
- // If there is already an AriaDescriber available, use that. Otherwise, provide a new one.
991
- provide: AriaDescriber,
992
- deps: [
993
- [new Optional(), new SkipSelf(), AriaDescriber],
994
- Platform
995
- ],
996
- useFactory: ARIA_DESCRIBER_PROVIDER_FACTORY
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
- * Monitors mouse and keyboard events to determine the cause of focus events.
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 FocusMonitor {
811
+ class FocusTrap {
1006
812
  /**
1007
- * @param {?} _ngZone
813
+ * @param {?} _element
1008
814
  * @param {?} _platform
815
+ * @param {?} _checker
816
+ * @param {?} _ngZone
817
+ * @param {?=} deferAnchors
1009
818
  */
1010
- constructor(_ngZone, _platform) {
1011
- this._ngZone = _ngZone;
819
+ constructor(_element, _platform, _checker, _ngZone, deferAnchors = false) {
820
+ this._element = _element;
1012
821
  this._platform = _platform;
1013
- /**
1014
- * The focus origin that the next focus event is a result of.
1015
- */
1016
- this._origin = null;
1017
- /**
1018
- * Whether the window has just been focused.
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
- * Monitors focus on an element and applies appropriate CSS classes.
1029
- * @param {?} element The element to monitor
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
- monitor(element, renderer, checkChildren) {
1036
- // Do nothing if we're not on the browser platform.
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 of(null);
865
+ return;
1039
866
  }
1040
- // Check if we're already monitoring this element.
1041
- if (this._elementInfo.has(element)) {
1042
- let /** @type {?} */ cachedInfo = this._elementInfo.get(element); /** @type {?} */
1043
- ((cachedInfo)).checkChildren = checkChildren;
1044
- return ((cachedInfo)).subject.asObservable();
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
- element.addEventListener('focus', focusListener, true);
1059
- element.addEventListener('blur', blurListener, true);
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
- * Stops monitoring an element and removes all focus classes.
1070
- * @param {?} element The element to stop monitoring.
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
- stopMonitoring(element) {
1074
- let /** @type {?} */ elementInfo = this._elementInfo.get(element);
1075
- if (elementInfo) {
1076
- elementInfo.unlisten();
1077
- elementInfo.subject.complete();
1078
- this._setClasses(element);
1079
- this._elementInfo.delete(element);
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
- * Focuses the element via the specified focus origin.
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
- focusVia(element, origin) {
1089
- this._setOriginForCurrentEventQueue(origin);
1090
- element.focus();
1091
- }
1094
+ get disabled() { return !this.focusTrap.enabled; }
1092
1095
  /**
1093
- * Register necessary event listeners on the document and window.
1096
+ * @param {?} val
1094
1097
  * @return {?}
1095
1098
  */
1096
- _registerDocumentEvents() {
1097
- // Do nothing if we're not on the browser platform.
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
- _setClasses(element, origin) {
1139
- const /** @type {?} */ elementInfo = this._elementInfo.get(element);
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
- _setOriginForCurrentEventQueue(origin) {
1158
- this._origin = origin;
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
- * Checks whether the given focus event was caused by a touchstart event.
1163
- * @param {?} event The focus event to check.
1164
- * @return {?} Whether the event was caused by a touch.
1135
+ * @param {?} _elementRef
1136
+ * @param {?} _focusTrapFactory
1165
1137
  */
1166
- _wasCausedByTouch(event) {
1167
- // Note(mmalerba): This implementation is not quite perfect, there is a small edge case.
1168
- // Consider the following dom structure:
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
- * Handles focus events on a registered element.
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
- _onFocus(event, element) {
1195
- // NOTE(mmalerba): We currently set the classes based on the focus origin of the most recent
1196
- // focus event affecting the monitored element. If we want to use the origin of the first event
1197
- // instead we should check for the cdk-focused class here and return if the element already has
1198
- // it. (This only matters for elements that have includesChildren = true).
1199
- // If we are not counting child-element-focus as focused, make sure that the event target is the
1200
- // monitored element itself.
1201
- const /** @type {?} */ elementInfo = this._elementInfo.get(element);
1202
- if (!elementInfo || (!elementInfo.checkChildren && element !== event.target)) {
1203
- return;
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
- _onBlur(event, element) {
1234
- // If we are counting child-element-focus as focused, make sure that we aren't just blurring in
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
- FocusMonitor.decorators = [
1246
- { type: Injectable },
1166
+ FocusTrapDirective.decorators = [
1167
+ { type: Directive, args: [{
1168
+ selector: '[cdkTrapFocus]',
1169
+ exportAs: 'cdkTrapFocus',
1170
+ },] },
1247
1171
  ];
1248
1172
  /**
1249
1173
  * @nocollapse
1250
1174
  */
1251
- FocusMonitor.ctorParameters = () => [
1252
- { type: NgZone, },
1253
- { type: Platform, },
1175
+ FocusTrapDirective.ctorParameters = () => [
1176
+ { type: ElementRef, },
1177
+ { type: FocusTrapFactory, },
1254
1178
  ];
1255
- /**
1256
- * Directive that determines how a particular element was focused (via keyboard, mouse, touch, or
1257
- * programmatically) and adds corresponding classes to the element.
1258
- *
1259
- * There are two variants of this directive:
1260
- * 1) cdkMonitorElementFocus: does not consider an element to be focused if one of its children is
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 {?} _elementRef
1267
- * @param {?} _focusMonitor
1268
- * @param {?} renderer
1186
+ * @param {?} elementToken
1187
+ * @param {?} platform
1269
1188
  */
1270
- constructor(_elementRef, _focusMonitor, renderer) {
1271
- this._elementRef = _elementRef;
1272
- this._focusMonitor = _focusMonitor;
1273
- this.cdkFocusChange = new EventEmitter();
1274
- this._monitorSubscription = this._focusMonitor.monitor(this._elementRef.nativeElement, renderer, this._elementRef.nativeElement.hasAttribute('cdkMonitorSubtreeFocus'))
1275
- .subscribe(origin => this.cdkFocusChange.emit(origin));
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
- ngOnDestroy() {
1281
- this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
1282
- this._monitorSubscription.unsubscribe();
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
- CdkMonitorFocus.decorators = [
1286
- { type: Directive, args: [{
1287
- selector: '[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]',
1288
- },] },
1235
+ LiveAnnouncer.decorators = [
1236
+ { type: Injectable },
1289
1237
  ];
1290
1238
  /**
1291
1239
  * @nocollapse
1292
1240
  */
1293
- CdkMonitorFocus.ctorParameters = () => [
1294
- { type: ElementRef, },
1295
- { type: FocusMonitor, },
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 {?} ngZone
1248
+ * @param {?} liveElement
1305
1249
  * @param {?} platform
1306
1250
  * @return {?}
1307
1251
  */
1308
- function FOCUS_MONITOR_PROVIDER_FACTORY(parentDispatcher, ngZone, platform) {
1309
- return parentDispatcher || new FocusMonitor(ngZone, platform);
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 FOCUS_MONITOR_PROVIDER = {
1315
- // If there is already a FocusMonitor available, use that. Otherwise, provide a new one.
1316
- provide: FocusMonitor,
1317
- deps: [[new Optional(), new SkipSelf(), FocusMonitor], NgZone, Platform],
1318
- useFactory: FOCUS_MONITOR_PROVIDER_FACTORY
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
- * This class manages keyboard events for selectable lists. If you pass it a query list
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 ListKeyManager {
1275
+ class FocusMonitor {
1326
1276
  /**
1327
- * @param {?} _items
1277
+ * @param {?} _ngZone
1278
+ * @param {?} _platform
1328
1279
  */
1329
- constructor(_items) {
1330
- this._items = _items;
1331
- this._activeItemIndex = -1;
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
- * Stream that emits any time the TAB key is pressed, so components can react
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.tabOut = new Subject();
1341
- }
1342
- /**
1343
- * Turns on wrapping mode, which ensures that the active item will wrap to
1344
- * the other end of list when there are no more items in the given direction.
1345
- * @return {?}
1346
- */
1347
- withWrap() {
1348
- this._wrap = true;
1349
- return this;
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
- * Turns on typeahead mode which allows users to set the active item by typing.
1353
- * @param {?=} debounceInterval Time to wait after the last keystroke before setting the active item.
1354
- * @return {?}
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
- withTypeAhead(debounceInterval = 200) {
1357
- if (this._items.length && this._items.some(item => typeof item.getLabel !== 'function')) {
1358
- throw Error('ListKeyManager items in typeahead mode must implement the `getLabel` method.');
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._typeaheadSubscription.unsubscribe();
1361
- // Debounce the presses of non-navigational keys, collect the ones that correspond to letters
1362
- // and convert those letters back into a string. Afterwards find the first item that starts
1363
- // with that string and select it.
1364
- this._typeaheadSubscription = RxChain.from(this._letterKeyStream)
1365
- .call(doOperator, keyCode => this._pressedLetters.push(keyCode))
1366
- .call(debounceTime, debounceInterval)
1367
- .call(filter, () => this._pressedLetters.length > 0)
1368
- .call(map, () => this._pressedLetters.join(''))
1369
- .subscribe(inputString => {
1370
- const /** @type {?} */ items = this._items.toArray();
1371
- for (let /** @type {?} */ i = 0; i < items.length; i++) {
1372
- if (((items[i].getLabel))().toUpperCase().trim().indexOf(inputString) === 0) {
1373
- this.setActiveItem(i);
1374
- break;
1375
- }
1376
- }
1377
- this._pressedLetters = [];
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
- return this;
1380
- }
1381
- /**
1382
- * Sets the active item to the item at the index specified.
1383
- * @param {?} index The index of the item to be set as active.
1384
- * @return {?}
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
- * Sets the active item depending on the key event passed in.
1392
- * @param {?} event Keyboard event to be used for determining which element should be active.
1339
+ * Stops monitoring an element and removes all focus classes.
1340
+ * @param {?} element The element to stop monitoring.
1393
1341
  * @return {?}
1394
1342
  */
1395
- onKeydown(event) {
1396
- switch (event.keyCode) {
1397
- case DOWN_ARROW:
1398
- this.setNextItemActive();
1399
- break;
1400
- case UP_ARROW:
1401
- this.setPreviousItemActive();
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
- * Sets the active item to the last enabled item in the list.
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
- setLastItemActive() {
1449
- this._setActiveItemByIndex(this._items.length - 1, -1);
1358
+ focusVia(element, origin) {
1359
+ this._setOriginForCurrentEventQueue(origin);
1360
+ element.focus();
1450
1361
  }
1451
1362
  /**
1452
- * Sets the active item to the next enabled item in the list.
1363
+ * Register necessary event listeners on the document and window.
1453
1364
  * @return {?}
1454
1365
  */
1455
- setNextItemActive() {
1456
- this._activeItemIndex < 0 ? this.setFirstItemActive() : this._setActiveItemByDelta(1);
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 active item to a previous enabled item in the list.
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
- setPreviousItemActive() {
1463
- this._activeItemIndex < 0 && this._wrap ? this.setLastItemActive()
1464
- : this._setActiveItemByDelta(-1);
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
- * Allows setting of the activeItemIndex without any other effects.
1468
- * @param {?} index The new activeItemIndex.
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
- updateActiveItemIndex(index) {
1472
- this._activeItemIndex = index;
1427
+ _setOriginForCurrentEventQueue(origin) {
1428
+ this._origin = origin;
1429
+ setTimeout(() => this._origin = null, 0);
1473
1430
  }
1474
1431
  /**
1475
- * This method sets the active item, given a list of items and the delta between the
1476
- * currently active item and the new active item. It will calculate differently
1477
- * depending on whether wrap mode is turned on.
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
- _setActiveItemByDelta(delta, items = this._items.toArray()) {
1483
- this._wrap ? this._setActiveInWrapMode(delta, items)
1484
- : this._setActiveInDefaultMode(delta, items);
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
- * Sets the active item properly given "wrap" mode. In other words, it will continue to move
1488
- * down the list until it finds an item that is not disabled, and it will wrap if it
1489
- * encounters either end of the list.
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
- _setActiveInWrapMode(delta, items) {
1495
- // when active item would leave menu, wrap to beginning or end
1496
- this._activeItemIndex =
1497
- (this._activeItemIndex + delta + items.length) % items.length;
1498
- // skip all disabled menu items recursively until an enabled one is reached
1499
- if (items[this._activeItemIndex].disabled) {
1500
- this._setActiveInWrapMode(delta, items);
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
- else {
1503
- this.setActiveItem(this._activeItemIndex);
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
- * Sets the active item properly given the default mode. In other words, it will
1508
- * continue to move down the list until it finds an item that is not disabled. If
1509
- * it encounters either end of the list, it will stop and not wrap.
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
- _setActiveItemByIndex(index, fallbackDelta, items = this._items.toArray()) {
1527
- if (!items[index]) {
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
- while (items[index].disabled) {
1531
- index += fallbackDelta;
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
- class ActiveDescendantKeyManager extends ListKeyManager {
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
- setActiveItem(index) {
1549
- Promise.resolve().then(() => {
1550
- if (this.activeItem) {
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
- * Screenreaders will often fire fake mousedown events when a focusable element
1563
- * is activated using the keyboard. We can typically distinguish between these faked
1564
- * mousedown events and real mousedown events using the "buttons" property. While
1565
- * real mousedowns will indicate the mouse button that was pressed (e.g. "1" for
1566
- * the left mouse button), faked mousedowns will usually set the property value to 0.
1567
- * @param {?} event
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 isFakeMousedownFromScreenReader(event) {
1571
- return event.buttons === 0;
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 { A11yModule, 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 };
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