UNPKG

32.7 kBJavaScriptView Raw
1import { ɵɵdefineInjectable, Injectable, ɵɵinject, PLATFORM_ID, Inject, NgZone, Directive, ElementRef, Input, NgModule } from '@angular/core';
2import { isPlatformBrowser, DOCUMENT, CommonModule } from '@angular/common';
3import { 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. */
13class 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}
49FocusTrapManager.ɵprov = ɵɵdefineInjectable({ factory: function FocusTrapManager_Factory() { return new FocusTrapManager(); }, token: FocusTrapManager, providedIn: "root" });
50FocusTrapManager.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.
63let 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
69try {
70 hasV8BreakIterator = (typeof Intl !== 'undefined' && Intl.v8BreakIterator);
71}
72catch (_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 */
79class 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}
120Platform.ɵprov = ɵɵdefineInjectable({ factory: function Platform_Factory() { return new Platform(ɵɵinject(PLATFORM_ID)); }, token: Platform, providedIn: "root" });
121Platform.decorators = [
122 { type: Injectable, args: [{ providedIn: 'root' },] }
123];
124Platform.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 */
138class 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 */
153class 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}
261InteractivityChecker.ɵprov = ɵɵdefineInjectable({ factory: function InteractivityChecker_Factory() { return new InteractivityChecker(ɵɵinject(Platform)); }, token: InteractivityChecker, providedIn: "root" });
262InteractivityChecker.decorators = [
263 { type: Injectable, args: [{ providedIn: 'root' },] }
264];
265InteractivityChecker.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 */
273function 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. */
282function 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 */
289function 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">`. */
297function isHiddenInput(element) {
298 return isInputElement(element) && element.type == 'hidden';
299}
300/** Gets whether an element is an anchor that has an href attribute. */
301function isAnchorWithHref(element) {
302 return isAnchorElement(element) && element.hasAttribute('href');
303}
304/** Gets whether an element is an input element. */
305function isInputElement(element) {
306 return element.nodeName.toLowerCase() == 'input';
307}
308/** Gets whether an element is an anchor element. */
309function isAnchorElement(element) {
310 return element.nodeName.toLowerCase() == 'a';
311}
312/** Gets whether an element has a valid tabindex. */
313function 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 */
328function 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 */
337function 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 */
349function 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. */
360function 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. */
373function 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 */
394class 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 */
663class 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}
680FocusTrapFactory.ɵprov = ɵɵdefineInjectable({ factory: function FocusTrapFactory_Factory() { return new FocusTrapFactory(ɵɵinject(InteractivityChecker), ɵɵinject(NgZone), ɵɵinject(DOCUMENT)); }, token: FocusTrapFactory, providedIn: "root" });
681FocusTrapFactory.decorators = [
682 { type: Injectable, args: [{ providedIn: 'root' },] }
683];
684FocusTrapFactory.ctorParameters = () => [
685 { type: InteractivityChecker },
686 { type: NgZone },
687 { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }
688];
689/** Directive for trapping focus within a region. */
690class 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}
749FocusTrapDirective.decorators = [
750 { type: Directive, args: [{
751 selector: '[focusTrap]',
752 exportAs: 'focusTrap'
753 },] }
754];
755FocusTrapDirective.ctorParameters = () => [
756 { type: ElementRef },
757 { type: FocusTrapFactory },
758 { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }
759];
760FocusTrapDirective.propDecorators = {
761 enabled: [{ type: Input, args: ['cdkTrapFocus',] }],
762 autoCapture: [{ type: Input, args: ['cdkTrapFocusAutoCapture',] }]
763};
764
765class FocusTrapModule {
766 static forRoot() {
767 return {
768 ngModule: FocusTrapModule,
769 providers: [
770 FocusTrapManager,
771 Platform,
772 InteractivityChecker
773 ]
774 };
775 }
776}
777FocusTrapModule.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
789export { FocusTrap, FocusTrapDirective, FocusTrapModule, FocusTrapFactory as ɵa, InteractivityChecker as ɵb, Platform as ɵc, FocusTrapManager as ɵd };
790//# sourceMappingURL=ngx-bootstrap-focus-trap.js.map