UNPKG

@material/dom

Version:
136 lines 6.13 kB
/** * @license * Copyright 2020 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var FOCUS_SENTINEL_CLASS = 'mdc-dom-focus-sentinel'; /** * Utility to trap focus in a given root element, e.g. for modal components such * as dialogs. The root should have at least one focusable child element, * for setting initial focus when trapping focus. * Also tracks the previously focused element, and restores focus to that * element when releasing focus. */ var FocusTrap = /** @class */ (function () { function FocusTrap(root, options) { if (options === void 0) { options = {}; } this.root = root; this.options = options; // Previously focused element before trapping focus. this.elFocusedBeforeTrapFocus = null; } /** * Traps focus in `root`. Also focuses on either `initialFocusEl` if set; * otherwises sets initial focus to the first focusable child element. */ FocusTrap.prototype.trapFocus = function () { var focusableEls = this.getFocusableElements(this.root); if (focusableEls.length === 0) { throw new Error('FocusTrap: Element must have at least one focusable child.'); } this.elFocusedBeforeTrapFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null; this.wrapTabFocus(this.root); if (!this.options.skipInitialFocus) { this.focusInitialElement(focusableEls, this.options.initialFocusEl); } }; /** * Releases focus from `root`. Also restores focus to the previously focused * element. */ FocusTrap.prototype.releaseFocus = function () { [].slice.call(this.root.querySelectorAll("." + FOCUS_SENTINEL_CLASS)) .forEach(function (sentinelEl) { sentinelEl.parentElement.removeChild(sentinelEl); }); if (!this.options.skipRestoreFocus && this.elFocusedBeforeTrapFocus) { this.elFocusedBeforeTrapFocus.focus(); } }; /** * Wraps tab focus within `el` by adding two hidden sentinel divs which are * used to mark the beginning and the end of the tabbable region. When * focused, these sentinel elements redirect focus to the first/last * children elements of the tabbable region, ensuring that focus is trapped * within that region. */ FocusTrap.prototype.wrapTabFocus = function (el) { var _this = this; var sentinelStart = this.createSentinel(); var sentinelEnd = this.createSentinel(); sentinelStart.addEventListener('focus', function () { var focusableEls = _this.getFocusableElements(el); if (focusableEls.length > 0) { focusableEls[focusableEls.length - 1].focus(); } }); sentinelEnd.addEventListener('focus', function () { var focusableEls = _this.getFocusableElements(el); if (focusableEls.length > 0) { focusableEls[0].focus(); } }); el.insertBefore(sentinelStart, el.children[0]); el.appendChild(sentinelEnd); }; /** * Focuses on `initialFocusEl` if defined and a child of the root element. * Otherwise, focuses on the first focusable child element of the root. */ FocusTrap.prototype.focusInitialElement = function (focusableEls, initialFocusEl) { var focusIndex = 0; if (initialFocusEl) { focusIndex = Math.max(focusableEls.indexOf(initialFocusEl), 0); } focusableEls[focusIndex].focus(); }; FocusTrap.prototype.getFocusableElements = function (root) { var focusableEls = [].slice.call(root.querySelectorAll('[autofocus], [tabindex], a, input, textarea, select, button')); return focusableEls.filter(function (el) { var isDisabledOrHidden = el.getAttribute('aria-disabled') === 'true' || el.getAttribute('disabled') != null || el.getAttribute('hidden') != null || el.getAttribute('aria-hidden') === 'true'; var isTabbableAndVisible = el.tabIndex >= 0 && el.getBoundingClientRect().width > 0 && !el.classList.contains(FOCUS_SENTINEL_CLASS) && !isDisabledOrHidden; var isProgrammaticallyHidden = false; if (isTabbableAndVisible) { var style = getComputedStyle(el); isProgrammaticallyHidden = style.display === 'none' || style.visibility === 'hidden'; } return isTabbableAndVisible && !isProgrammaticallyHidden; }); }; FocusTrap.prototype.createSentinel = function () { var sentinel = document.createElement('div'); sentinel.setAttribute('tabindex', '0'); // Don't announce in screen readers. sentinel.setAttribute('aria-hidden', 'true'); sentinel.classList.add(FOCUS_SENTINEL_CLASS); return sentinel; }; return FocusTrap; }()); export { FocusTrap }; //# sourceMappingURL=focus-trap.js.map