UNPKG

ngx-bootstrap

Version:
396 lines 16.3 kB
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /* eslint-disable */ import { coerceBooleanProperty } from './boolean-property'; import { DOCUMENT } from '@angular/common'; import { Directive, ElementRef, Inject, Injectable, Input, NgZone } from '@angular/core'; import { take } from 'rxjs/operators'; import { InteractivityChecker } from './interactivity-checker'; import * as i0 from "@angular/core"; import * as i1 from "./interactivity-checker"; import * as i2 from "@angular/common"; /** * Class that allows for trapping focus within a DOM element. * * This class currently uses a relatively simple approach to focus trapping. * It assumes that the tab order is the same as DOM order, which is not necessarily true. * Things like `tabIndex > 0`, flex `order`, and shadow roots can cause the two to misalign. * * @deprecated Use `ConfigurableFocusTrap` instead. * @breaking-change for 11.0.0 Remove this class. */ export class FocusTrap { constructor(_element, _checker, _ngZone, _document, deferAnchors = false) { this._element = _element; this._checker = _checker; this._ngZone = _ngZone; this._document = _document; this._hasAttached = false; // Event listeners for the anchors. Need to be regular functions so that we can unbind them later. this.startAnchorListener = () => this.focusLastTabbableElement(); this.endAnchorListener = () => this.focusFirstTabbableElement(); this._enabled = true; if (!deferAnchors) { this.attachAnchors(); } } /** Whether the focus trap is active. */ get enabled() { return this._enabled; } set enabled(value) { this._enabled = value; if (this._startAnchor && this._endAnchor) { this._toggleAnchorTabIndex(value, this._startAnchor); this._toggleAnchorTabIndex(value, this._endAnchor); } } /** Destroys the focus trap by cleaning up the anchors. */ destroy() { const startAnchor = this._startAnchor; const endAnchor = this._endAnchor; if (startAnchor) { startAnchor.removeEventListener('focus', this.startAnchorListener); if (startAnchor.parentNode) { startAnchor.parentNode.removeChild(startAnchor); } } if (endAnchor) { endAnchor.removeEventListener('focus', this.endAnchorListener); if (endAnchor.parentNode) { endAnchor.parentNode.removeChild(endAnchor); } } this._startAnchor = this._endAnchor = null; this._hasAttached = false; } /** * Inserts the anchors into the DOM. This is usually done automatically * in the constructor, but can be deferred for cases like directives with `*ngIf`. * @returns Whether the focus trap managed to attach successfuly. This may not be the case * if the target element isn't currently in the DOM. */ attachAnchors() { // If we're not on the browser, there can be no focus to trap. if (this._hasAttached) { return true; } this._ngZone.runOutsideAngular(() => { if (!this._startAnchor) { this._startAnchor = this._createAnchor(); this._startAnchor.addEventListener('focus', this.startAnchorListener); } if (!this._endAnchor) { this._endAnchor = this._createAnchor(); this._endAnchor.addEventListener('focus', this.endAnchorListener); } }); if (this._element.parentNode) { this._element.parentNode.insertBefore(this._startAnchor, this._element); this._element.parentNode.insertBefore(this._endAnchor, this._element.nextSibling); this._hasAttached = true; } return this._hasAttached; } /** * Waits for the zone to stabilize, then either focuses the first element that the * user specified, or the first tabbable element. * @returns Returns a promise that resolves with a boolean, depending * on whether focus was moved successfully. */ focusInitialElementWhenReady() { return new Promise(resolve => { this._executeOnStable(() => resolve(this.focusInitialElement())); }); } /** * Waits for the zone to stabilize, then focuses * the first tabbable element within the focus trap region. * @returns Returns a promise that resolves with a boolean, depending * on whether focus was moved successfully. */ focusFirstTabbableElementWhenReady() { return new Promise(resolve => { this._executeOnStable(() => resolve(this.focusFirstTabbableElement())); }); } /** * Waits for the zone to stabilize, then focuses * the last tabbable element within the focus trap region. * @returns Returns a promise that resolves with a boolean, depending * on whether focus was moved successfully. */ focusLastTabbableElementWhenReady() { return new Promise(resolve => { this._executeOnStable(() => resolve(this.focusLastTabbableElement())); }); } /** * Get the specified boundary element of the trapped region. * @param bound The boundary to get (start or end of trapped region). * @returns The boundary element. */ _getRegionBoundary(bound) { // Contains the deprecated version of selector, for temporary backwards comparability. let markers = this._element.querySelectorAll(`[cdk-focus-region-${bound}], ` + `[cdkFocusRegion${bound}], ` + `[cdk-focus-${bound}]`); for (let i = 0; i < markers.length; i++) { // @breaking-change 8.0.0 if (markers[i].hasAttribute(`cdk-focus-${bound}`)) { console.warn(`Found use of deprecated attribute 'cdk-focus-${bound}', ` + `use 'cdkFocusRegion${bound}' instead. The deprecated ` + `attribute will be removed in 8.0.0.`, markers[i]); } else if (markers[i].hasAttribute(`cdk-focus-region-${bound}`)) { console.warn(`Found use of deprecated attribute 'cdk-focus-region-${bound}', ` + `use 'cdkFocusRegion${bound}' instead. The deprecated attribute ` + `will be removed in 8.0.0.`, markers[i]); } } if (bound == 'start') { return markers.length ? markers[0] : this._getFirstTabbableElement(this._element); } return markers.length ? markers[markers.length - 1] : this._getLastTabbableElement(this._element); } /** * Focuses the element that should be focused when the focus trap is initialized. * @returns Whether focus was moved successfully. */ focusInitialElement() { // Contains the deprecated version of selector, for temporary backwards comparability. const redirectToElement = this._element.querySelector(`[cdk-focus-initial], ` + `[cdkFocusInitial]`); if (redirectToElement) { // @breaking-change 8.0.0 if (redirectToElement.hasAttribute(`cdk-focus-initial`)) { console.warn(`Found use of deprecated attribute 'cdk-focus-initial', ` + `use 'cdkFocusInitial' instead. The deprecated attribute ` + `will be removed in 8.0.0`, redirectToElement); } // Warn the consumer if the element they've pointed to // isn't focusable, when not in production mode. if (!this._checker.isFocusable(redirectToElement)) { const focusableChild = this._getFirstTabbableElement(redirectToElement); focusableChild === null || focusableChild === void 0 ? void 0 : focusableChild.focus(); return !!focusableChild; } redirectToElement.focus(); return true; } return this.focusFirstTabbableElement(); } /** * Focuses the first tabbable element within the focus trap region. * @returns Whether focus was moved successfully. */ focusFirstTabbableElement() { const redirectToElement = this._getRegionBoundary('start'); if (redirectToElement) { redirectToElement.focus(); } return !!redirectToElement; } /** * Focuses the last tabbable element within the focus trap region. * @returns Whether focus was moved successfully. */ focusLastTabbableElement() { const redirectToElement = this._getRegionBoundary('end'); if (redirectToElement) { redirectToElement.focus(); } return !!redirectToElement; } /** * Checks whether the focus trap has successfully been attached. */ hasAttached() { return this._hasAttached; } /** Get the first tabbable element from a DOM subtree (inclusive). */ _getFirstTabbableElement(root) { if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) { return root; } // Iterate in DOM order. Note that IE doesn't have `children` for SVG so we fall // back to `childNodes` which includes text nodes, comments etc. let children = root.children || root.childNodes; for (let i = 0; i < children.length; i++) { let tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE ? this._getFirstTabbableElement(children[i]) : null; if (tabbableChild) { return tabbableChild; } } return null; } /** Get the last tabbable element from a DOM subtree (inclusive). */ _getLastTabbableElement(root) { if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) { return root; } // Iterate in reverse DOM order. let children = root.children || root.childNodes; for (let i = children.length - 1; i >= 0; i--) { let tabbableChild = children[i].nodeType === this._document.ELEMENT_NODE ? this._getLastTabbableElement(children[i]) : null; if (tabbableChild) { return tabbableChild; } } return null; } /** Creates an anchor element. */ _createAnchor() { const anchor = this._document.createElement('div'); this._toggleAnchorTabIndex(this._enabled, anchor); anchor.classList.add('cdk-visually-hidden'); anchor.classList.add('cdk-focus-trap-anchor'); anchor.setAttribute('aria-hidden', 'true'); return anchor; } /** * Toggles the `tabindex` of an anchor, based on the enabled state of the focus trap. * @param isEnabled Whether the focus trap is enabled. * @param anchor Anchor on which to toggle the tabindex. */ _toggleAnchorTabIndex(isEnabled, anchor) { // Remove the tabindex completely, rather than setting it to -1, because if the // element has a tabindex, the user might still hit it when navigating with the arrow keys. isEnabled ? anchor.setAttribute('tabindex', '0') : anchor.removeAttribute('tabindex'); } /** * Toggles the`tabindex` of both anchors to either trap Tab focus or allow it to escape. * @param enabled: Whether the anchors should trap Tab. */ toggleAnchors(enabled) { if (this._startAnchor && this._endAnchor) { this._toggleAnchorTabIndex(enabled, this._startAnchor); this._toggleAnchorTabIndex(enabled, this._endAnchor); } } /** Executes a function when the zone is stable. */ _executeOnStable(fn) { if (this._ngZone.isStable) { fn(); } else { this._ngZone.onStable.pipe(take(1)).subscribe(fn); } } } /** * Factory that allows easy instantiation of focus traps. * @deprecated Use `ConfigurableFocusTrapFactory` instead. * @breaking-change for 11.0.0 Remove this class. */ export class FocusTrapFactory { constructor(_checker, _ngZone, _document) { this._checker = _checker; this._ngZone = _ngZone; this._document = _document; } /** * Creates a focus-trapped region around the given element. * @param element The element around which focus will be trapped. * @param deferCaptureElements Defers the creation of focus-capturing elements to be done * manually by the user. * @returns The created focus trap instance. */ create(element, deferCaptureElements = false) { return new FocusTrap(element, this._checker, this._ngZone, this._document, deferCaptureElements); } } FocusTrapFactory.ɵ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" }); FocusTrapFactory.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; FocusTrapFactory.ctorParameters = () => [ { type: InteractivityChecker }, { type: NgZone }, { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] } ]; /** Directive for trapping focus within a region. */ export class FocusTrapDirective { constructor(_elementRef, _focusTrapFactory, _document) { this._elementRef = _elementRef; this._focusTrapFactory = _focusTrapFactory; /** Previously focused element to restore focus to upon destroy when using autoCapture. */ this._previouslyFocusedElement = null; this._document = _document; this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true); } /** Whether the focus trap is active. */ get enabled() { return this.focusTrap.enabled; } set enabled(value) { this.focusTrap.enabled = coerceBooleanProperty(value); } /** * Whether the directive should automatically move focus into the trapped region upon * initialization and return focus to the previous activeElement upon destruction. */ get autoCapture() { return this._autoCapture; } set autoCapture(value) { this._autoCapture = coerceBooleanProperty(value); } ngOnDestroy() { this.focusTrap.destroy(); // If we stored a previously focused element when using autoCapture, return focus to that // element now that the trapped region is being destroyed. if (this._previouslyFocusedElement) { this._previouslyFocusedElement.focus(); this._previouslyFocusedElement = null; } } ngAfterContentInit() { this.focusTrap.attachAnchors(); if (this.autoCapture) { this._captureFocus(); } } ngDoCheck() { if (!this.focusTrap.hasAttached()) { this.focusTrap.attachAnchors(); } } ngOnChanges(changes) { const autoCaptureChange = changes['autoCapture']; if (autoCaptureChange && !autoCaptureChange.firstChange && this.autoCapture && this.focusTrap.hasAttached()) { this._captureFocus(); } } _captureFocus() { this._previouslyFocusedElement = this._document.activeElement; this.focusTrap.focusInitialElementWhenReady(); } } FocusTrapDirective.decorators = [ { type: Directive, args: [{ selector: '[focusTrap]', exportAs: 'focusTrap' },] } ]; FocusTrapDirective.ctorParameters = () => [ { type: ElementRef }, { type: FocusTrapFactory }, { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] } ]; FocusTrapDirective.propDecorators = { enabled: [{ type: Input, args: ['cdkTrapFocus',] }], autoCapture: [{ type: Input, args: ['cdkTrapFocusAutoCapture',] }] }; //# sourceMappingURL=focus-trap.js.map