UNPKG

16.3 kBJavaScriptView Raw
1/**
2 * @license
3 * Copyright Google LLC All Rights Reserved.
4 *
5 * Use of this source code is governed by an MIT-style license that can be
6 * found in the LICENSE file at https://angular.io/license
7 */
8/* eslint-disable */
9import { coerceBooleanProperty } from './boolean-property';
10import { DOCUMENT } from '@angular/common';
11import { Directive, ElementRef, Inject, Injectable, Input, NgZone } from '@angular/core';
12import { take } from 'rxjs/operators';
13import { InteractivityChecker } from './interactivity-checker';
14import * as i0 from "@angular/core";
15import * as i1 from "./interactivity-checker";
16import * as i2 from "@angular/common";
17/**
18 * Class that allows for trapping focus within a DOM element.
19 *
20 * This class currently uses a relatively simple approach to focus trapping.
21 * It assumes that the tab order is the same as DOM order, which is not necessarily true.
22 * Things like `tabIndex > 0`, flex `order`, and shadow roots can cause the two to misalign.
23 *
24 * @deprecated Use `ConfigurableFocusTrap` instead.
25 * @breaking-change for 11.0.0 Remove this class.
26 */
27export class FocusTrap {
28 constructor(_element, _checker, _ngZone, _document, deferAnchors = false) {
29 this._element = _element;
30 this._checker = _checker;
31 this._ngZone = _ngZone;
32 this._document = _document;
33 this._hasAttached = false;
34 // Event listeners for the anchors. Need to be regular functions so that we can unbind them later.
35 this.startAnchorListener = () => this.focusLastTabbableElement();
36 this.endAnchorListener = () => this.focusFirstTabbableElement();
37 this._enabled = true;
38 if (!deferAnchors) {
39 this.attachAnchors();
40 }
41 }
42 /** Whether the focus trap is active. */
43 get enabled() {
44 return this._enabled;
45 }
46 set enabled(value) {
47 this._enabled = value;
48 if (this._startAnchor && this._endAnchor) {
49 this._toggleAnchorTabIndex(value, this._startAnchor);
50 this._toggleAnchorTabIndex(value, this._endAnchor);
51 }
52 }
53 /** Destroys the focus trap by cleaning up the anchors. */
54 destroy() {
55 const startAnchor = this._startAnchor;
56 const endAnchor = this._endAnchor;
57 if (startAnchor) {
58 startAnchor.removeEventListener('focus', this.startAnchorListener);
59 if (startAnchor.parentNode) {
60 startAnchor.parentNode.removeChild(startAnchor);
61 }
62 }
63 if (endAnchor) {
64 endAnchor.removeEventListener('focus', this.endAnchorListener);
65 if (endAnchor.parentNode) {
66 endAnchor.parentNode.removeChild(endAnchor);
67 }
68 }
69 this._startAnchor = this._endAnchor = null;
70 this._hasAttached = false;
71 }
72 /**
73 * Inserts the anchors into the DOM. This is usually done automatically
74 * in the constructor, but can be deferred for cases like directives with `*ngIf`.
75 * @returns Whether the focus trap managed to attach successfuly. This may not be the case
76 * if the target element isn't currently in the DOM.
77 */
78 attachAnchors() {
79 // If we're not on the browser, there can be no focus to trap.
80 if (this._hasAttached) {
81 return true;
82 }
83 this._ngZone.runOutsideAngular(() => {
84 if (!this._startAnchor) {
85 this._startAnchor = this._createAnchor();
86 this._startAnchor.addEventListener('focus', this.startAnchorListener);
87 }
88 if (!this._endAnchor) {
89 this._endAnchor = this._createAnchor();
90 this._endAnchor.addEventListener('focus', this.endAnchorListener);
91 }
92 });
93 if (this._element.parentNode) {
94 this._element.parentNode.insertBefore(this._startAnchor, this._element);
95 this._element.parentNode.insertBefore(this._endAnchor, this._element.nextSibling);
96 this._hasAttached = true;
97 }
98 return this._hasAttached;
99 }
100 /**
101 * Waits for the zone to stabilize, then either focuses the first element that the
102 * user specified, or the first tabbable element.
103 * @returns Returns a promise that resolves with a boolean, depending
104 * on whether focus was moved successfully.
105 */
106 focusInitialElementWhenReady() {
107 return new Promise(resolve => {
108 this._executeOnStable(() => resolve(this.focusInitialElement()));
109 });
110 }
111 /**
112 * Waits for the zone to stabilize, then focuses
113 * the first tabbable element within the focus trap region.
114 * @returns Returns a promise that resolves with a boolean, depending
115 * on whether focus was moved successfully.
116 */
117 focusFirstTabbableElementWhenReady() {
118 return new Promise(resolve => {
119 this._executeOnStable(() => resolve(this.focusFirstTabbableElement()));
120 });
121 }
122 /**
123 * Waits for the zone to stabilize, then focuses
124 * the last tabbable element within the focus trap region.
125 * @returns Returns a promise that resolves with a boolean, depending
126 * on whether focus was moved successfully.
127 */
128 focusLastTabbableElementWhenReady() {
129 return new Promise(resolve => {
130 this._executeOnStable(() => resolve(this.focusLastTabbableElement()));
131 });
132 }
133 /**
134 * Get the specified boundary element of the trapped region.
135 * @param bound The boundary to get (start or end of trapped region).
136 * @returns The boundary element.
137 */
138 _getRegionBoundary(bound) {
139 // Contains the deprecated version of selector, for temporary backwards comparability.
140 let markers = this._element.querySelectorAll(`[cdk-focus-region-${bound}], ` +
141 `[cdkFocusRegion${bound}], ` +
142 `[cdk-focus-${bound}]`);
143 for (let i = 0; i < markers.length; i++) {
144 // @breaking-change 8.0.0
145 if (markers[i].hasAttribute(`cdk-focus-${bound}`)) {
146 console.warn(`Found use of deprecated attribute 'cdk-focus-${bound}', ` +
147 `use 'cdkFocusRegion${bound}' instead. The deprecated ` +
148 `attribute will be removed in 8.0.0.`, markers[i]);
149 }
150 else if (markers[i].hasAttribute(`cdk-focus-region-${bound}`)) {
151 console.warn(`Found use of deprecated attribute 'cdk-focus-region-${bound}', ` +
152 `use 'cdkFocusRegion${bound}' instead. The deprecated attribute ` +
153 `will be removed in 8.0.0.`, markers[i]);
154 }
155 }
156 if (bound == 'start') {
157 return markers.length ? markers[0] : this._getFirstTabbableElement(this._element);
158 }
159 return markers.length ?
160 markers[markers.length - 1] : this._getLastTabbableElement(this._element);
161 }
162 /**
163 * Focuses the element that should be focused when the focus trap is initialized.
164 * @returns Whether focus was moved successfully.
165 */
166 focusInitialElement() {
167 // Contains the deprecated version of selector, for temporary backwards comparability.
168 const redirectToElement = this._element.querySelector(`[cdk-focus-initial], ` +
169 `[cdkFocusInitial]`);
170 if (redirectToElement) {
171 // @breaking-change 8.0.0
172 if (redirectToElement.hasAttribute(`cdk-focus-initial`)) {
173 console.warn(`Found use of deprecated attribute 'cdk-focus-initial', ` +
174 `use 'cdkFocusInitial' instead. The deprecated attribute ` +
175 `will be removed in 8.0.0`, redirectToElement);
176 }
177 // Warn the consumer if the element they've pointed to
178 // isn't focusable, when not in production mode.
179 if (!this._checker.isFocusable(redirectToElement)) {
180 const focusableChild = this._getFirstTabbableElement(redirectToElement);
181 focusableChild === null || focusableChild === void 0 ? void 0 : focusableChild.focus();
182 return !!focusableChild;
183 }
184 redirectToElement.focus();
185 return true;
186 }
187 return this.focusFirstTabbableElement();
188 }
189 /**
190 * Focuses the first tabbable element within the focus trap region.
191 * @returns Whether focus was moved successfully.
192 */
193 focusFirstTabbableElement() {
194 const redirectToElement = this._getRegionBoundary('start');
195 if (redirectToElement) {
196 redirectToElement.focus();
197 }
198 return !!redirectToElement;
199 }
200 /**
201 * Focuses the last tabbable element within the focus trap region.
202 * @returns Whether focus was moved successfully.
203 */
204 focusLastTabbableElement() {
205 const redirectToElement = this._getRegionBoundary('end');
206 if (redirectToElement) {
207 redirectToElement.focus();
208 }
209 return !!redirectToElement;
210 }
211 /**
212 * Checks whether the focus trap has successfully been attached.
213 */
214 hasAttached() {
215 return this._hasAttached;
216 }
217 /** Get the first tabbable element from a DOM subtree (inclusive). */
218 _getFirstTabbableElement(root) {
219 if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
220 return root;
221 }
222 // Iterate in DOM order. Note that IE doesn't have `children` for SVG so we fall
223 // back to `childNodes` which includes text nodes, comments etc.
224 let children = root.children || root.childNodes;
225 for (let i = 0; i < children.length; i++) {
226 let tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE ?
227 this._getFirstTabbableElement(children[i]) :
228 null;
229 if (tabbableChild) {
230 return tabbableChild;
231 }
232 }
233 return null;
234 }
235 /** Get the last tabbable element from a DOM subtree (inclusive). */
236 _getLastTabbableElement(root) {
237 if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
238 return root;
239 }
240 // Iterate in reverse DOM order.
241 let children = root.children || root.childNodes;
242 for (let i = children.length - 1; i >= 0; i--) {
243 let tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE ?
244 this._getLastTabbableElement(children[i]) :
245 null;
246 if (tabbableChild) {
247 return tabbableChild;
248 }
249 }
250 return null;
251 }
252 /** Creates an anchor element. */
253 _createAnchor() {
254 const anchor = this._document.createElement('div');
255 this._toggleAnchorTabIndex(this._enabled, anchor);
256 anchor.classList.add('cdk-visually-hidden');
257 anchor.classList.add('cdk-focus-trap-anchor');
258 anchor.setAttribute('aria-hidden', 'true');
259 return anchor;
260 }
261 /**
262 * Toggles the `tabindex` of an anchor, based on the enabled state of the focus trap.
263 * @param isEnabled Whether the focus trap is enabled.
264 * @param anchor Anchor on which to toggle the tabindex.
265 */
266 _toggleAnchorTabIndex(isEnabled, anchor) {
267 // Remove the tabindex completely, rather than setting it to -1, because if the
268 // element has a tabindex, the user might still hit it when navigating with the arrow keys.
269 isEnabled ? anchor.setAttribute('tabindex', '0') : anchor.removeAttribute('tabindex');
270 }
271 /**
272 * Toggles the`tabindex` of both anchors to either trap Tab focus or allow it to escape.
273 * @param enabled: Whether the anchors should trap Tab.
274 */
275 toggleAnchors(enabled) {
276 if (this._startAnchor && this._endAnchor) {
277 this._toggleAnchorTabIndex(enabled, this._startAnchor);
278 this._toggleAnchorTabIndex(enabled, this._endAnchor);
279 }
280 }
281 /** Executes a function when the zone is stable. */
282 _executeOnStable(fn) {
283 if (this._ngZone.isStable) {
284 fn();
285 }
286 else {
287 this._ngZone.onStable.pipe(take(1)).subscribe(fn);
288 }
289 }
290}
291/**
292 * Factory that allows easy instantiation of focus traps.
293 * @deprecated Use `ConfigurableFocusTrapFactory` instead.
294 * @breaking-change for 11.0.0 Remove this class.
295 */
296export class FocusTrapFactory {
297 constructor(_checker, _ngZone, _document) {
298 this._checker = _checker;
299 this._ngZone = _ngZone;
300 this._document = _document;
301 }
302 /**
303 * Creates a focus-trapped region around the given element.
304 * @param element The element around which focus will be trapped.
305 * @param deferCaptureElements Defers the creation of focus-capturing elements to be done
306 * manually by the user.
307 * @returns The created focus trap instance.
308 */
309 create(element, deferCaptureElements = false) {
310 return new FocusTrap(element, this._checker, this._ngZone, this._document, deferCaptureElements);
311 }
312}
313FocusTrapFactory.ɵprov = i0.ɵɵdefineInjectable({ factory: function FocusTrapFactory_Factory() { return new FocusTrapFactory(i0.ɵɵinject(i1.InteractivityChecker), i0.ɵɵinject(i0.NgZone), i0.ɵɵinject(i2.DOCUMENT)); }, token: FocusTrapFactory, providedIn: "root" });
314FocusTrapFactory.decorators = [
315 { type: Injectable, args: [{ providedIn: 'root' },] }
316];
317FocusTrapFactory.ctorParameters = () => [
318 { type: InteractivityChecker },
319 { type: NgZone },
320 { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }
321];
322/** Directive for trapping focus within a region. */
323export class FocusTrapDirective {
324 constructor(_elementRef, _focusTrapFactory, _document) {
325 this._elementRef = _elementRef;
326 this._focusTrapFactory = _focusTrapFactory;
327 /** Previously focused element to restore focus to upon destroy when using autoCapture. */
328 this._previouslyFocusedElement = null;
329 this._autoCapture = false;
330 this._document = _document;
331 this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
332 }
333 /** Whether the focus trap is active. */
334 get enabled() {
335 return this.focusTrap.enabled;
336 }
337 set enabled(value) {
338 this.focusTrap.enabled = coerceBooleanProperty(value);
339 }
340 /**
341 * Whether the directive should automatically move focus into the trapped region upon
342 * initialization and return focus to the previous activeElement upon destruction.
343 */
344 get autoCapture() {
345 return this._autoCapture;
346 }
347 set autoCapture(value) {
348 this._autoCapture = coerceBooleanProperty(value);
349 }
350 ngOnDestroy() {
351 this.focusTrap.destroy();
352 // If we stored a previously focused element when using autoCapture, return focus to that
353 // element now that the trapped region is being destroyed.
354 if (this._previouslyFocusedElement) {
355 this._previouslyFocusedElement.focus();
356 this._previouslyFocusedElement = null;
357 }
358 }
359 ngAfterContentInit() {
360 this.focusTrap.attachAnchors();
361 if (this.autoCapture) {
362 this._captureFocus();
363 }
364 }
365 ngDoCheck() {
366 if (!this.focusTrap.hasAttached()) {
367 this.focusTrap.attachAnchors();
368 }
369 }
370 ngOnChanges(changes) {
371 const autoCaptureChange = changes['autoCapture'];
372 if (autoCaptureChange && !autoCaptureChange.firstChange && this.autoCapture &&
373 this.focusTrap.hasAttached()) {
374 this._captureFocus();
375 }
376 }
377 _captureFocus() {
378 this._previouslyFocusedElement = this._document.activeElement;
379 this.focusTrap.focusInitialElementWhenReady();
380 }
381}
382FocusTrapDirective.decorators = [
383 { type: Directive, args: [{
384 selector: '[focusTrap]',
385 exportAs: 'focusTrap'
386 },] }
387];
388FocusTrapDirective.ctorParameters = () => [
389 { type: ElementRef },
390 { type: FocusTrapFactory },
391 { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }
392];
393FocusTrapDirective.propDecorators = {
394 enabled: [{ type: Input, args: ['cdkTrapFocus',] }],
395 autoCapture: [{ type: Input, args: ['cdkTrapFocusAutoCapture',] }]
396};
397//# sourceMappingURL=focus-trap.js.map
\No newline at end of file