1 | import { ɵɵdefineInjectable, Injectable, ɵɵinject, PLATFORM_ID, Inject, NgZone, Directive, ElementRef, Input, NgModule } from '@angular/core';
|
2 | import { isPlatformBrowser, DOCUMENT, CommonModule } from '@angular/common';
|
3 | import { take } from 'rxjs/operators';
|
4 |
|
5 | /**
|
6 | * @license
|
7 | * Copyright Google LLC All Rights Reserved.
|
8 | *
|
9 | * Use of this source code is governed by an MIT-style license that can be
|
10 | * found in the LICENSE file at https://angular.io/license
|
11 | */
|
12 | /** Injectable that ensures only the most recently enabled FocusTrap is active. */
|
13 | class FocusTrapManager {
|
14 | constructor() {
|
15 | // A stack of the FocusTraps on the page. Only the FocusTrap at the
|
16 | // top of the stack is active.
|
17 | this._focusTrapStack = [];
|
18 | }
|
19 | /**
|
20 | * Disables the FocusTrap at the top of the stack, and then pushes
|
21 | * the new FocusTrap onto the stack.
|
22 | */
|
23 | register(focusTrap) {
|
24 | // Dedupe focusTraps that register multiple times.
|
25 | this._focusTrapStack = this._focusTrapStack.filter((ft) => ft !== focusTrap);
|
26 | let stack = this._focusTrapStack;
|
27 | if (stack.length) {
|
28 | stack[stack.length - 1]._disable();
|
29 | }
|
30 | stack.push(focusTrap);
|
31 | focusTrap._enable();
|
32 | }
|
33 | /**
|
34 | * Removes the FocusTrap from the stack, and activates the
|
35 | * FocusTrap that is the new top of the stack.
|
36 | */
|
37 | deregister(focusTrap) {
|
38 | focusTrap._disable();
|
39 | const stack = this._focusTrapStack;
|
40 | const i = stack.indexOf(focusTrap);
|
41 | if (i !== -1) {
|
42 | stack.splice(i, 1);
|
43 | if (stack.length) {
|
44 | stack[stack.length - 1]._enable();
|
45 | }
|
46 | }
|
47 | }
|
48 | }
|
49 | FocusTrapManager.ɵprov = ɵɵdefineInjectable({ factory: function FocusTrapManager_Factory() { return new FocusTrapManager(); }, token: FocusTrapManager, providedIn: "root" });
|
50 | FocusTrapManager.decorators = [
|
51 | { type: Injectable, args: [{ providedIn: 'root' },] }
|
52 | ];
|
53 |
|
54 | /**
|
55 | * @license
|
56 | * Copyright Google LLC All Rights Reserved.
|
57 | *
|
58 | * Use of this source code is governed by an MIT-style license that can be
|
59 | * found in the LICENSE file at https://angular.io/license
|
60 | */
|
61 | // Whether the current platform supports the V8 Break Iterator. The V8 check
|
62 | // is necessary to detect all Blink based browsers.
|
63 | let hasV8BreakIterator;
|
64 | // We need a try/catch around the reference to `Intl`, because accessing it in some cases can
|
65 | // cause IE to throw. These cases are tied to particular versions of Windows and can happen if
|
66 | // the consumer is providing a polyfilled `Map`. See:
|
67 | // https://github.com/Microsoft/ChakraCore/issues/3189
|
68 | // https://github.com/angular/components/issues/15687
|
69 | try {
|
70 | hasV8BreakIterator = (typeof Intl !== 'undefined' && Intl.v8BreakIterator);
|
71 | }
|
72 | catch (_a) {
|
73 | hasV8BreakIterator = false;
|
74 | }
|
75 | /**
|
76 | * Service to detect the current platform by comparing the userAgent strings and
|
77 | * checking browser-specific global properties.
|
78 | */
|
79 | class Platform {
|
80 | constructor(_platformId) {
|
81 | this._platformId = _platformId;
|
82 | // We want to use the Angular platform check because if the Document is shimmed
|
83 | // without the navigator, the following checks will fail. This is preferred because
|
84 | // sometimes the Document may be shimmed without the user's knowledge or intention
|
85 | /** Whether the Angular application is being rendered in the browser. */
|
86 | this.isBrowser = this._platformId ?
|
87 | isPlatformBrowser(this._platformId) : typeof document === 'object' && !!document;
|
88 | /** Whether the current browser is Microsoft Edge. */
|
89 | this.EDGE = this.isBrowser && /(edge)/i.test(navigator.userAgent);
|
90 | /** Whether the current rendering engine is Microsoft Trident. */
|
91 | this.TRIDENT = this.isBrowser && /(msie|trident)/i.test(navigator.userAgent);
|
92 | // EdgeHTML and Trident mock Blink specific things and need to be excluded from this check.
|
93 | /** Whether the current rendering engine is Blink. */
|
94 | this.BLINK = this.isBrowser && (!!(window.chrome || hasV8BreakIterator) &&
|
95 | typeof CSS !== 'undefined' && !this.EDGE && !this.TRIDENT);
|
96 | // Webkit is part of the userAgent in EdgeHTML, Blink and Trident. Therefore we need to
|
97 | // ensure that Webkit runs standalone and is not used as another engine's base.
|
98 | /** Whether the current rendering engine is WebKit. */
|
99 | this.WEBKIT = this.isBrowser &&
|
100 | /AppleWebKit/i.test(navigator.userAgent) && !this.BLINK && !this.EDGE && !this.TRIDENT;
|
101 | /** Whether the current platform is Apple iOS. */
|
102 | this.IOS = this.isBrowser && /iPad|iPhone|iPod/.test(navigator.userAgent) &&
|
103 | !('MSStream' in window);
|
104 | // It's difficult to detect the plain Gecko engine, because most of the browsers identify
|
105 | // them self as Gecko-like browsers and modify the userAgent's according to that.
|
106 | // Since we only cover one explicit Firefox case, we can simply check for Firefox
|
107 | // instead of having an unstable check for Gecko.
|
108 | /** Whether the current browser is Firefox. */
|
109 | this.FIREFOX = this.isBrowser && /(firefox|minefield)/i.test(navigator.userAgent);
|
110 | /** Whether the current platform is Android. */
|
111 | // Trident on mobile adds the android platform to the userAgent to trick detections.
|
112 | this.ANDROID = this.isBrowser && /android/i.test(navigator.userAgent) && !this.TRIDENT;
|
113 | // Safari browsers will include the Safari keyword in their userAgent. Some browsers may fake
|
114 | // this and just place the Safari keyword in the userAgent. To be more safe about Safari every
|
115 | // Safari browser should also use Webkit as its layout engine.
|
116 | /** Whether the current browser is Safari. */
|
117 | this.SAFARI = this.isBrowser && /safari/i.test(navigator.userAgent) && this.WEBKIT;
|
118 | }
|
119 | }
|
120 | Platform.ɵprov = ɵɵdefineInjectable({ factory: function Platform_Factory() { return new Platform(ɵɵinject(PLATFORM_ID)); }, token: Platform, providedIn: "root" });
|
121 | Platform.decorators = [
|
122 | { type: Injectable, args: [{ providedIn: 'root' },] }
|
123 | ];
|
124 | Platform.ctorParameters = () => [
|
125 | { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }
|
126 | ];
|
127 |
|
128 | /**
|
129 | * @license
|
130 | * Copyright Google LLC All Rights Reserved.
|
131 | *
|
132 | * Use of this source code is governed by an MIT-style license that can be
|
133 | * found in the LICENSE file at https://angular.io/license
|
134 | */
|
135 | /**
|
136 | * Configuration for the isFocusable method.
|
137 | */
|
138 | class IsFocusableConfig {
|
139 | constructor() {
|
140 | /**
|
141 | * Whether to count an element as focusable even if it is not currently visible.
|
142 | */
|
143 | this.ignoreVisibility = false;
|
144 | }
|
145 | }
|
146 | // The InteractivityChecker leans heavily on the ally.js accessibility utilities.
|
147 | // Methods like `isTabbable` are only covering specific edge-cases for the browsers which are
|
148 | // supported.
|
149 | /**
|
150 | * Utility for checking the interactivity of an element, such as whether is is focusable or
|
151 | * tabbable.
|
152 | */
|
153 | class InteractivityChecker {
|
154 | constructor(_platform) {
|
155 | this._platform = _platform;
|
156 | }
|
157 | /**
|
158 | * Gets whether an element is disabled.
|
159 | *
|
160 | * @param element Element to be checked.
|
161 | * @returns Whether the element is disabled.
|
162 | */
|
163 | isDisabled(element) {
|
164 | // This does not capture some cases, such as a non-form control with a disabled attribute or
|
165 | // a form control inside of a disabled form, but should capture the most common cases.
|
166 | return element.hasAttribute('disabled');
|
167 | }
|
168 | /**
|
169 | * Gets whether an element is visible for the purposes of interactivity.
|
170 | *
|
171 | * This will capture states like `display: none` and `visibility: hidden`, but not things like
|
172 | * being clipped by an `overflow: hidden` parent or being outside the viewport.
|
173 | *
|
174 | * @returns Whether the element is visible.
|
175 | */
|
176 | isVisible(element) {
|
177 | return hasGeometry(element) && getComputedStyle(element).visibility === 'visible';
|
178 | }
|
179 | /**
|
180 | * Gets whether an element can be reached via Tab key.
|
181 | * Assumes that the element has already been checked with isFocusable.
|
182 | *
|
183 | * @param element Element to be checked.
|
184 | * @returns Whether the element is tabbable.
|
185 | */
|
186 | isTabbable(element) {
|
187 | // Nothing is tabbable on the server 😎
|
188 | if (!this._platform.isBrowser) {
|
189 | return false;
|
190 | }
|
191 | const frameElement = getFrameElement(getWindow(element));
|
192 | if (frameElement) {
|
193 | // Frame elements inherit their tabindex onto all child elements.
|
194 | if (getTabIndexValue(frameElement) === -1) {
|
195 | return false;
|
196 | }
|
197 | // Browsers disable tabbing to an element inside of an invisible frame.
|
198 | if (!this.isVisible(frameElement)) {
|
199 | return false;
|
200 | }
|
201 | }
|
202 | let nodeName = element.nodeName.toLowerCase();
|
203 | let tabIndexValue = getTabIndexValue(element);
|
204 | if (element.hasAttribute('contenteditable')) {
|
205 | return tabIndexValue !== -1;
|
206 | }
|
207 | if (nodeName === 'iframe' || nodeName === 'object') {
|
208 | // The frame or object's content may be tabbable depending on the content, but it's
|
209 | // not possibly to reliably detect the content of the frames. We always consider such
|
210 | // elements as non-tabbable.
|
211 | return false;
|
212 | }
|
213 | // In iOS, the browser only considers some specific elements as tabbable.
|
214 | if (this._platform.WEBKIT && this._platform.IOS && !isPotentiallyTabbableIOS(element)) {
|
215 | return false;
|
216 | }
|
217 | if (nodeName === 'audio') {
|
218 | // Audio elements without controls enabled are never tabbable, regardless
|
219 | // of the tabindex attribute explicitly being set.
|
220 | if (!element.hasAttribute('controls')) {
|
221 | return false;
|
222 | }
|
223 | // Audio elements with controls are by default tabbable unless the
|
224 | // tabindex attribute is set to `-1` explicitly.
|
225 | return tabIndexValue !== -1;
|
226 | }
|
227 | if (nodeName === 'video') {
|
228 | // For all video elements, if the tabindex attribute is set to `-1`, the video
|
229 | // is not tabbable. Note: We cannot rely on the default `HTMLElement.tabIndex`
|
230 | // property as that one is set to `-1` in Chrome, Edge and Safari v13.1. The
|
231 | // tabindex attribute is the source of truth here.
|
232 | if (tabIndexValue === -1) {
|
233 | return false;
|
234 | }
|
235 | // If the tabindex is explicitly set, and not `-1` (as per check before), the
|
236 | // video element is always tabbable (regardless of whether it has controls or not).
|
237 | if (tabIndexValue !== null) {
|
238 | return true;
|
239 | }
|
240 | // Otherwise (when no explicit tabindex is set), a video is only tabbable if it
|
241 | // has controls enabled. Firefox is special as videos are always tabbable regardless
|
242 | // of whether there are controls or not.
|
243 | return this._platform.FIREFOX || element.hasAttribute('controls');
|
244 | }
|
245 | return element.tabIndex >= 0;
|
246 | }
|
247 | /**
|
248 | * Gets whether an element can be focused by the user.
|
249 | *
|
250 | * @param element Element to be checked.
|
251 | * @param config The config object with options to customize this method's behavior
|
252 | * @returns Whether the element is focusable.
|
253 | */
|
254 | isFocusable(element, config) {
|
255 | // Perform checks in order of left to most expensive.
|
256 | // Again, naive approach that does not capture many edge cases and browser quirks.
|
257 | return isPotentiallyFocusable(element) && !this.isDisabled(element) &&
|
258 | ((config === null || config === void 0 ? void 0 : config.ignoreVisibility) || this.isVisible(element));
|
259 | }
|
260 | }
|
261 | InteractivityChecker.ɵprov = ɵɵdefineInjectable({ factory: function InteractivityChecker_Factory() { return new InteractivityChecker(ɵɵinject(Platform)); }, token: InteractivityChecker, providedIn: "root" });
|
262 | InteractivityChecker.decorators = [
|
263 | { type: Injectable, args: [{ providedIn: 'root' },] }
|
264 | ];
|
265 | InteractivityChecker.ctorParameters = () => [
|
266 | { type: Platform }
|
267 | ];
|
268 | /**
|
269 | * Returns the frame element from a window object. Since browsers like MS Edge throw errors if
|
270 | * the frameElement property is being accessed from a different host address, this property
|
271 | * should be accessed carefully.
|
272 | */
|
273 | function getFrameElement(window) {
|
274 | try {
|
275 | return window.frameElement;
|
276 | }
|
277 | catch (_a) {
|
278 | return null;
|
279 | }
|
280 | }
|
281 | /** Checks whether the specified element has any geometry / rectangles. */
|
282 | function hasGeometry(element) {
|
283 | // Use logic from jQuery to check for an invisible element.
|
284 | // See https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js#L12
|
285 | return !!(element.offsetWidth || element.offsetHeight ||
|
286 | (typeof element.getClientRects === 'function' && element.getClientRects().length));
|
287 | }
|
288 | /** Gets whether an element's */
|
289 | function isNativeFormElement(element) {
|
290 | let nodeName = element.nodeName.toLowerCase();
|
291 | return nodeName === 'input' ||
|
292 | nodeName === 'select' ||
|
293 | nodeName === 'button' ||
|
294 | nodeName === 'textarea';
|
295 | }
|
296 | /** Gets whether an element is an `<input type="hidden">`. */
|
297 | function isHiddenInput(element) {
|
298 | return isInputElement(element) && element.type == 'hidden';
|
299 | }
|
300 | /** Gets whether an element is an anchor that has an href attribute. */
|
301 | function isAnchorWithHref(element) {
|
302 | return isAnchorElement(element) && element.hasAttribute('href');
|
303 | }
|
304 | /** Gets whether an element is an input element. */
|
305 | function isInputElement(element) {
|
306 | return element.nodeName.toLowerCase() == 'input';
|
307 | }
|
308 | /** Gets whether an element is an anchor element. */
|
309 | function isAnchorElement(element) {
|
310 | return element.nodeName.toLowerCase() == 'a';
|
311 | }
|
312 | /** Gets whether an element has a valid tabindex. */
|
313 | function hasValidTabIndex(element) {
|
314 | if (!element.hasAttribute('tabindex') || element.tabIndex === undefined) {
|
315 | return false;
|
316 | }
|
317 | let tabIndex = element.getAttribute('tabindex');
|
318 | // IE11 parses tabindex="" as the value "-32768"
|
319 | if (tabIndex == '-32768') {
|
320 | return false;
|
321 | }
|
322 | return !!(tabIndex && !isNaN(parseInt(tabIndex, 10)));
|
323 | }
|
324 | /**
|
325 | * Returns the parsed tabindex from the element attributes instead of returning the
|
326 | * evaluated tabindex from the browsers defaults.
|
327 | */
|
328 | function getTabIndexValue(element) {
|
329 | if (!hasValidTabIndex(element)) {
|
330 | return null;
|
331 | }
|
332 | // See browser issue in Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054
|
333 | const tabIndex = parseInt(element.getAttribute('tabindex') || '', 10);
|
334 | return isNaN(tabIndex) ? -1 : tabIndex;
|
335 | }
|
336 | /** Checks whether the specified element is potentially tabbable on iOS */
|
337 | function isPotentiallyTabbableIOS(element) {
|
338 | let nodeName = element.nodeName.toLowerCase();
|
339 | let inputType = nodeName === 'input' && element.type;
|
340 | return inputType === 'text'
|
341 | || inputType === 'password'
|
342 | || nodeName === 'select'
|
343 | || nodeName === 'textarea';
|
344 | }
|
345 | /**
|
346 | * Gets whether an element is potentially focusable without taking current visible/disabled state
|
347 | * into account.
|
348 | */
|
349 | function isPotentiallyFocusable(element) {
|
350 | // Inputs are potentially focusable *unless* they're type="hidden".
|
351 | if (isHiddenInput(element)) {
|
352 | return false;
|
353 | }
|
354 | return isNativeFormElement(element) ||
|
355 | isAnchorWithHref(element) ||
|
356 | element.hasAttribute('contenteditable') ||
|
357 | hasValidTabIndex(element);
|
358 | }
|
359 | /** Gets the parent window of a DOM node with regards of being inside of an iframe. */
|
360 | function getWindow(node) {
|
361 | // ownerDocument is null if `node` itself *is* a document.
|
362 | return node.ownerDocument && node.ownerDocument.defaultView || window;
|
363 | }
|
364 |
|
365 | /**
|
366 | * @license
|
367 | * Copyright Google LLC All Rights Reserved.
|
368 | *
|
369 | * Use of this source code is governed by an MIT-style license that can be
|
370 | * found in the LICENSE file at https://angular.io/license
|
371 | */
|
372 | /** Coerces a data-bound value (typically a string) to a boolean. */
|
373 | function coerceBooleanProperty(value) {
|
374 | return value != null && `${value}` !== 'false';
|
375 | }
|
376 |
|
377 | /**
|
378 | * @license
|
379 | * Copyright Google LLC All Rights Reserved.
|
380 | *
|
381 | * Use of this source code is governed by an MIT-style license that can be
|
382 | * found in the LICENSE file at https://angular.io/license
|
383 | */
|
384 | /**
|
385 | * Class that allows for trapping focus within a DOM element.
|
386 | *
|
387 | * This class currently uses a relatively simple approach to focus trapping.
|
388 | * It assumes that the tab order is the same as DOM order, which is not necessarily true.
|
389 | * Things like `tabIndex > 0`, flex `order`, and shadow roots can cause the two to misalign.
|
390 | *
|
391 | * @deprecated Use `ConfigurableFocusTrap` instead.
|
392 | * @breaking-change for 11.0.0 Remove this class.
|
393 | */
|
394 | class FocusTrap {
|
395 | constructor(_element, _checker, _ngZone, _document, deferAnchors = false) {
|
396 | this._element = _element;
|
397 | this._checker = _checker;
|
398 | this._ngZone = _ngZone;
|
399 | this._document = _document;
|
400 | this._hasAttached = false;
|
401 | // Event listeners for the anchors. Need to be regular functions so that we can unbind them later.
|
402 | this.startAnchorListener = () => this.focusLastTabbableElement();
|
403 | this.endAnchorListener = () => this.focusFirstTabbableElement();
|
404 | this._enabled = true;
|
405 | if (!deferAnchors) {
|
406 | this.attachAnchors();
|
407 | }
|
408 | }
|
409 | /** Whether the focus trap is active. */
|
410 | get enabled() {
|
411 | return this._enabled;
|
412 | }
|
413 | set enabled(value) {
|
414 | this._enabled = value;
|
415 | if (this._startAnchor && this._endAnchor) {
|
416 | this._toggleAnchorTabIndex(value, this._startAnchor);
|
417 | this._toggleAnchorTabIndex(value, this._endAnchor);
|
418 | }
|
419 | }
|
420 | /** Destroys the focus trap by cleaning up the anchors. */
|
421 | destroy() {
|
422 | const startAnchor = this._startAnchor;
|
423 | const endAnchor = this._endAnchor;
|
424 | if (startAnchor) {
|
425 | startAnchor.removeEventListener('focus', this.startAnchorListener);
|
426 | if (startAnchor.parentNode) {
|
427 | startAnchor.parentNode.removeChild(startAnchor);
|
428 | }
|
429 | }
|
430 | if (endAnchor) {
|
431 | endAnchor.removeEventListener('focus', this.endAnchorListener);
|
432 | if (endAnchor.parentNode) {
|
433 | endAnchor.parentNode.removeChild(endAnchor);
|
434 | }
|
435 | }
|
436 | this._startAnchor = this._endAnchor = null;
|
437 | this._hasAttached = false;
|
438 | }
|
439 | /**
|
440 | * Inserts the anchors into the DOM. This is usually done automatically
|
441 | * in the constructor, but can be deferred for cases like directives with `*ngIf`.
|
442 | * @returns Whether the focus trap managed to attach successfuly. This may not be the case
|
443 | * if the target element isn't currently in the DOM.
|
444 | */
|
445 | attachAnchors() {
|
446 | // If we're not on the browser, there can be no focus to trap.
|
447 | if (this._hasAttached) {
|
448 | return true;
|
449 | }
|
450 | this._ngZone.runOutsideAngular(() => {
|
451 | if (!this._startAnchor) {
|
452 | this._startAnchor = this._createAnchor();
|
453 | this._startAnchor.addEventListener('focus', this.startAnchorListener);
|
454 | }
|
455 | if (!this._endAnchor) {
|
456 | this._endAnchor = this._createAnchor();
|
457 | this._endAnchor.addEventListener('focus', this.endAnchorListener);
|
458 | }
|
459 | });
|
460 | if (this._element.parentNode) {
|
461 | this._element.parentNode.insertBefore(this._startAnchor, this._element);
|
462 | this._element.parentNode.insertBefore(this._endAnchor, this._element.nextSibling);
|
463 | this._hasAttached = true;
|
464 | }
|
465 | return this._hasAttached;
|
466 | }
|
467 | /**
|
468 | * Waits for the zone to stabilize, then either focuses the first element that the
|
469 | * user specified, or the first tabbable element.
|
470 | * @returns Returns a promise that resolves with a boolean, depending
|
471 | * on whether focus was moved successfully.
|
472 | */
|
473 | focusInitialElementWhenReady() {
|
474 | return new Promise(resolve => {
|
475 | this._executeOnStable(() => resolve(this.focusInitialElement()));
|
476 | });
|
477 | }
|
478 | /**
|
479 | * Waits for the zone to stabilize, then focuses
|
480 | * the first tabbable element within the focus trap region.
|
481 | * @returns Returns a promise that resolves with a boolean, depending
|
482 | * on whether focus was moved successfully.
|
483 | */
|
484 | focusFirstTabbableElementWhenReady() {
|
485 | return new Promise(resolve => {
|
486 | this._executeOnStable(() => resolve(this.focusFirstTabbableElement()));
|
487 | });
|
488 | }
|
489 | /**
|
490 | * Waits for the zone to stabilize, then focuses
|
491 | * the last tabbable element within the focus trap region.
|
492 | * @returns Returns a promise that resolves with a boolean, depending
|
493 | * on whether focus was moved successfully.
|
494 | */
|
495 | focusLastTabbableElementWhenReady() {
|
496 | return new Promise(resolve => {
|
497 | this._executeOnStable(() => resolve(this.focusLastTabbableElement()));
|
498 | });
|
499 | }
|
500 | /**
|
501 | * Get the specified boundary element of the trapped region.
|
502 | * @param bound The boundary to get (start or end of trapped region).
|
503 | * @returns The boundary element.
|
504 | */
|
505 | _getRegionBoundary(bound) {
|
506 | // Contains the deprecated version of selector, for temporary backwards comparability.
|
507 | let markers = this._element.querySelectorAll(`[cdk-focus-region-${bound}], ` +
|
508 | `[cdkFocusRegion${bound}], ` +
|
509 | `[cdk-focus-${bound}]`);
|
510 | for (let i = 0; i < markers.length; i++) {
|
511 | // @breaking-change 8.0.0
|
512 | if (markers[i].hasAttribute(`cdk-focus-${bound}`)) {
|
513 | console.warn(`Found use of deprecated attribute 'cdk-focus-${bound}', ` +
|
514 | `use 'cdkFocusRegion${bound}' instead. The deprecated ` +
|
515 | `attribute will be removed in 8.0.0.`, markers[i]);
|
516 | }
|
517 | else if (markers[i].hasAttribute(`cdk-focus-region-${bound}`)) {
|
518 | console.warn(`Found use of deprecated attribute 'cdk-focus-region-${bound}', ` +
|
519 | `use 'cdkFocusRegion${bound}' instead. The deprecated attribute ` +
|
520 | `will be removed in 8.0.0.`, markers[i]);
|
521 | }
|
522 | }
|
523 | if (bound == 'start') {
|
524 | return markers.length ? markers[0] : this._getFirstTabbableElement(this._element);
|
525 | }
|
526 | return markers.length ?
|
527 | markers[markers.length - 1] : this._getLastTabbableElement(this._element);
|
528 | }
|
529 | /**
|
530 | * Focuses the element that should be focused when the focus trap is initialized.
|
531 | * @returns Whether focus was moved successfully.
|
532 | */
|
533 | focusInitialElement() {
|
534 | // Contains the deprecated version of selector, for temporary backwards comparability.
|
535 | const redirectToElement = this._element.querySelector(`[cdk-focus-initial], ` +
|
536 | `[cdkFocusInitial]`);
|
537 | if (redirectToElement) {
|
538 | // @breaking-change 8.0.0
|
539 | if (redirectToElement.hasAttribute(`cdk-focus-initial`)) {
|
540 | console.warn(`Found use of deprecated attribute 'cdk-focus-initial', ` +
|
541 | `use 'cdkFocusInitial' instead. The deprecated attribute ` +
|
542 | `will be removed in 8.0.0`, redirectToElement);
|
543 | }
|
544 | // Warn the consumer if the element they've pointed to
|
545 | // isn't focusable, when not in production mode.
|
546 | if (!this._checker.isFocusable(redirectToElement)) {
|
547 | const focusableChild = this._getFirstTabbableElement(redirectToElement);
|
548 | focusableChild === null || focusableChild === void 0 ? void 0 : focusableChild.focus();
|
549 | return !!focusableChild;
|
550 | }
|
551 | redirectToElement.focus();
|
552 | return true;
|
553 | }
|
554 | return this.focusFirstTabbableElement();
|
555 | }
|
556 | /**
|
557 | * Focuses the first tabbable element within the focus trap region.
|
558 | * @returns Whether focus was moved successfully.
|
559 | */
|
560 | focusFirstTabbableElement() {
|
561 | const redirectToElement = this._getRegionBoundary('start');
|
562 | if (redirectToElement) {
|
563 | redirectToElement.focus();
|
564 | }
|
565 | return !!redirectToElement;
|
566 | }
|
567 | /**
|
568 | * Focuses the last tabbable element within the focus trap region.
|
569 | * @returns Whether focus was moved successfully.
|
570 | */
|
571 | focusLastTabbableElement() {
|
572 | const redirectToElement = this._getRegionBoundary('end');
|
573 | if (redirectToElement) {
|
574 | redirectToElement.focus();
|
575 | }
|
576 | return !!redirectToElement;
|
577 | }
|
578 | /**
|
579 | * Checks whether the focus trap has successfully been attached.
|
580 | */
|
581 | hasAttached() {
|
582 | return this._hasAttached;
|
583 | }
|
584 | /** Get the first tabbable element from a DOM subtree (inclusive). */
|
585 | _getFirstTabbableElement(root) {
|
586 | if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
|
587 | return root;
|
588 | }
|
589 | // Iterate in DOM order. Note that IE doesn't have `children` for SVG so we fall
|
590 | // back to `childNodes` which includes text nodes, comments etc.
|
591 | let children = root.children || root.childNodes;
|
592 | for (let i = 0; i < children.length; i++) {
|
593 | let tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE ?
|
594 | this._getFirstTabbableElement(children[i]) :
|
595 | null;
|
596 | if (tabbableChild) {
|
597 | return tabbableChild;
|
598 | }
|
599 | }
|
600 | return null;
|
601 | }
|
602 | /** Get the last tabbable element from a DOM subtree (inclusive). */
|
603 | _getLastTabbableElement(root) {
|
604 | if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
|
605 | return root;
|
606 | }
|
607 | // Iterate in reverse DOM order.
|
608 | let children = root.children || root.childNodes;
|
609 | for (let i = children.length - 1; i >= 0; i--) {
|
610 | let tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE ?
|
611 | this._getLastTabbableElement(children[i]) :
|
612 | null;
|
613 | if (tabbableChild) {
|
614 | return tabbableChild;
|
615 | }
|
616 | }
|
617 | return null;
|
618 | }
|
619 | /** Creates an anchor element. */
|
620 | _createAnchor() {
|
621 | const anchor = this._document.createElement('div');
|
622 | this._toggleAnchorTabIndex(this._enabled, anchor);
|
623 | anchor.classList.add('cdk-visually-hidden');
|
624 | anchor.classList.add('cdk-focus-trap-anchor');
|
625 | anchor.setAttribute('aria-hidden', 'true');
|
626 | return anchor;
|
627 | }
|
628 | /**
|
629 | * Toggles the `tabindex` of an anchor, based on the enabled state of the focus trap.
|
630 | * @param isEnabled Whether the focus trap is enabled.
|
631 | * @param anchor Anchor on which to toggle the tabindex.
|
632 | */
|
633 | _toggleAnchorTabIndex(isEnabled, anchor) {
|
634 | // Remove the tabindex completely, rather than setting it to -1, because if the
|
635 | // element has a tabindex, the user might still hit it when navigating with the arrow keys.
|
636 | isEnabled ? anchor.setAttribute('tabindex', '0') : anchor.removeAttribute('tabindex');
|
637 | }
|
638 | /**
|
639 | * Toggles the`tabindex` of both anchors to either trap Tab focus or allow it to escape.
|
640 | * @param enabled: Whether the anchors should trap Tab.
|
641 | */
|
642 | toggleAnchors(enabled) {
|
643 | if (this._startAnchor && this._endAnchor) {
|
644 | this._toggleAnchorTabIndex(enabled, this._startAnchor);
|
645 | this._toggleAnchorTabIndex(enabled, this._endAnchor);
|
646 | }
|
647 | }
|
648 | /** Executes a function when the zone is stable. */
|
649 | _executeOnStable(fn) {
|
650 | if (this._ngZone.isStable) {
|
651 | fn();
|
652 | }
|
653 | else {
|
654 | this._ngZone.onStable.pipe(take(1)).subscribe(fn);
|
655 | }
|
656 | }
|
657 | }
|
658 | /**
|
659 | * Factory that allows easy instantiation of focus traps.
|
660 | * @deprecated Use `ConfigurableFocusTrapFactory` instead.
|
661 | * @breaking-change for 11.0.0 Remove this class.
|
662 | */
|
663 | class FocusTrapFactory {
|
664 | constructor(_checker, _ngZone, _document) {
|
665 | this._checker = _checker;
|
666 | this._ngZone = _ngZone;
|
667 | this._document = _document;
|
668 | }
|
669 | /**
|
670 | * Creates a focus-trapped region around the given element.
|
671 | * @param element The element around which focus will be trapped.
|
672 | * @param deferCaptureElements Defers the creation of focus-capturing elements to be done
|
673 | * manually by the user.
|
674 | * @returns The created focus trap instance.
|
675 | */
|
676 | create(element, deferCaptureElements = false) {
|
677 | return new FocusTrap(element, this._checker, this._ngZone, this._document, deferCaptureElements);
|
678 | }
|
679 | }
|
680 | FocusTrapFactory.ɵprov = ɵɵdefineInjectable({ factory: function FocusTrapFactory_Factory() { return new FocusTrapFactory(ɵɵinject(InteractivityChecker), ɵɵinject(NgZone), ɵɵinject(DOCUMENT)); }, token: FocusTrapFactory, providedIn: "root" });
|
681 | FocusTrapFactory.decorators = [
|
682 | { type: Injectable, args: [{ providedIn: 'root' },] }
|
683 | ];
|
684 | FocusTrapFactory.ctorParameters = () => [
|
685 | { type: InteractivityChecker },
|
686 | { type: NgZone },
|
687 | { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }
|
688 | ];
|
689 | /** Directive for trapping focus within a region. */
|
690 | class FocusTrapDirective {
|
691 | constructor(_elementRef, _focusTrapFactory, _document) {
|
692 | this._elementRef = _elementRef;
|
693 | this._focusTrapFactory = _focusTrapFactory;
|
694 | /** Previously focused element to restore focus to upon destroy when using autoCapture. */
|
695 | this._previouslyFocusedElement = null;
|
696 | this._autoCapture = false;
|
697 | this._document = _document;
|
698 | this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
|
699 | }
|
700 | /** Whether the focus trap is active. */
|
701 | get enabled() {
|
702 | return this.focusTrap.enabled;
|
703 | }
|
704 | set enabled(value) {
|
705 | this.focusTrap.enabled = coerceBooleanProperty(value);
|
706 | }
|
707 | /**
|
708 | * Whether the directive should automatically move focus into the trapped region upon
|
709 | * initialization and return focus to the previous activeElement upon destruction.
|
710 | */
|
711 | get autoCapture() {
|
712 | return this._autoCapture;
|
713 | }
|
714 | set autoCapture(value) {
|
715 | this._autoCapture = coerceBooleanProperty(value);
|
716 | }
|
717 | ngOnDestroy() {
|
718 | this.focusTrap.destroy();
|
719 | // If we stored a previously focused element when using autoCapture, return focus to that
|
720 | // element now that the trapped region is being destroyed.
|
721 | if (this._previouslyFocusedElement) {
|
722 | this._previouslyFocusedElement.focus();
|
723 | this._previouslyFocusedElement = null;
|
724 | }
|
725 | }
|
726 | ngAfterContentInit() {
|
727 | this.focusTrap.attachAnchors();
|
728 | if (this.autoCapture) {
|
729 | this._captureFocus();
|
730 | }
|
731 | }
|
732 | ngDoCheck() {
|
733 | if (!this.focusTrap.hasAttached()) {
|
734 | this.focusTrap.attachAnchors();
|
735 | }
|
736 | }
|
737 | ngOnChanges(changes) {
|
738 | const autoCaptureChange = changes['autoCapture'];
|
739 | if (autoCaptureChange && !autoCaptureChange.firstChange && this.autoCapture &&
|
740 | this.focusTrap.hasAttached()) {
|
741 | this._captureFocus();
|
742 | }
|
743 | }
|
744 | _captureFocus() {
|
745 | this._previouslyFocusedElement = this._document.activeElement;
|
746 | this.focusTrap.focusInitialElementWhenReady();
|
747 | }
|
748 | }
|
749 | FocusTrapDirective.decorators = [
|
750 | { type: Directive, args: [{
|
751 | selector: '[focusTrap]',
|
752 | exportAs: 'focusTrap'
|
753 | },] }
|
754 | ];
|
755 | FocusTrapDirective.ctorParameters = () => [
|
756 | { type: ElementRef },
|
757 | { type: FocusTrapFactory },
|
758 | { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }
|
759 | ];
|
760 | FocusTrapDirective.propDecorators = {
|
761 | enabled: [{ type: Input, args: ['cdkTrapFocus',] }],
|
762 | autoCapture: [{ type: Input, args: ['cdkTrapFocusAutoCapture',] }]
|
763 | };
|
764 |
|
765 | class FocusTrapModule {
|
766 | static forRoot() {
|
767 | return {
|
768 | ngModule: FocusTrapModule,
|
769 | providers: [
|
770 | FocusTrapManager,
|
771 | Platform,
|
772 | InteractivityChecker
|
773 | ]
|
774 | };
|
775 | }
|
776 | }
|
777 | FocusTrapModule.decorators = [
|
778 | { type: NgModule, args: [{
|
779 | imports: [CommonModule],
|
780 | declarations: [FocusTrapDirective],
|
781 | exports: [FocusTrapDirective]
|
782 | },] }
|
783 | ];
|
784 |
|
785 | /**
|
786 | * Generated bundle index. Do not edit.
|
787 | */
|
788 |
|
789 | export { FocusTrap, FocusTrapDirective, FocusTrapModule, FocusTrapFactory as ɵa, InteractivityChecker as ɵb, Platform as ɵc, FocusTrapManager as ɵd };
|
790 | //# sourceMappingURL=ngx-bootstrap-focus-trap.js.map
|