360 lines
17 KiB
JavaScript
360 lines
17 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.getElementIndexPath = exports.getFocusableByIndexPath = exports.focusAsync = exports.shouldWrapFocus = exports.doesElementContainFocus = exports.isElementFocusSubZone = exports.isElementFocusZone = exports.isElementTabbable = exports.isElementVisibleAndNotHidden = exports.isElementVisible = exports.getNextElement = exports.getPreviousElement = exports.focusFirstChild = exports.getLastTabbable = exports.getFirstTabbable = exports.getLastFocusable = exports.getFirstFocusable = void 0;
|
|
var elementContainsAttribute_1 = require("./dom/elementContainsAttribute");
|
|
var elementContains_1 = require("./dom/elementContains");
|
|
var getParent_1 = require("./dom/getParent");
|
|
var getWindow_1 = require("./dom/getWindow");
|
|
var getDocument_1 = require("./dom/getDocument");
|
|
var IS_FOCUSABLE_ATTRIBUTE = 'data-is-focusable';
|
|
var IS_VISIBLE_ATTRIBUTE = 'data-is-visible';
|
|
var FOCUSZONE_ID_ATTRIBUTE = 'data-focuszone-id';
|
|
var FOCUSZONE_SUB_ATTRIBUTE = 'data-is-sub-focuszone';
|
|
/**
|
|
* Gets the first focusable element.
|
|
*
|
|
* @public
|
|
*/
|
|
function getFirstFocusable(rootElement, currentElement, includeElementsInFocusZones, includeShadowRoots) {
|
|
return getNextElement(rootElement, currentElement, true /*checkNode*/, false /*suppressParentTraversal*/, false /*suppressChildTraversal*/, includeElementsInFocusZones, undefined, undefined, undefined, includeShadowRoots);
|
|
}
|
|
exports.getFirstFocusable = getFirstFocusable;
|
|
/**
|
|
* Gets the last focusable element.
|
|
*
|
|
* @public
|
|
*/
|
|
function getLastFocusable(rootElement, currentElement, includeElementsInFocusZones, includeShadowRoots) {
|
|
return getPreviousElement(rootElement, currentElement, true /*checkNode*/, false /*suppressParentTraversal*/, true /*traverseChildren*/, includeElementsInFocusZones, undefined, undefined, includeShadowRoots);
|
|
}
|
|
exports.getLastFocusable = getLastFocusable;
|
|
/**
|
|
* Gets the first tabbable element. (The difference between focusable and tabbable is that tabbable elements are
|
|
* focusable elements that also have tabIndex != -1.)
|
|
* @param rootElement - The parent element to search beneath.
|
|
* @param currentElement - The descendant of rootElement to start the search at. This element is the first one checked,
|
|
* and iteration continues forward. Typical use passes rootElement.firstChild.
|
|
* @param includeElementsInFocusZones - true if traversal should go into FocusZone descendants.
|
|
* @param checkNode - Include currentElement in search when true. Defaults to true.
|
|
* @public
|
|
*/
|
|
function getFirstTabbable(rootElement, currentElement, includeElementsInFocusZones, checkNode, includeShadowRoots) {
|
|
if (checkNode === void 0) { checkNode = true; }
|
|
return getNextElement(rootElement, currentElement, checkNode, false /*suppressParentTraversal*/, false /*suppressChildTraversal*/, includeElementsInFocusZones, false /*allowFocusRoot*/, true /*tabbable*/, undefined, includeShadowRoots);
|
|
}
|
|
exports.getFirstTabbable = getFirstTabbable;
|
|
/**
|
|
* Gets the last tabbable element. (The difference between focusable and tabbable is that tabbable elements are
|
|
* focusable elements that also have tabIndex != -1.)
|
|
* @param rootElement - The parent element to search beneath.
|
|
* @param currentElement - The descendant of rootElement to start the search at. This element is the first one checked,
|
|
* and iteration continues in reverse. Typical use passes rootElement.lastChild.
|
|
* @param includeElementsInFocusZones - true if traversal should go into FocusZone descendants.
|
|
* @param checkNode - Include currentElement in search when true. Defaults to true.
|
|
* @public
|
|
*/
|
|
function getLastTabbable(rootElement, currentElement, includeElementsInFocusZones, checkNode, includeShadowRoots) {
|
|
if (checkNode === void 0) { checkNode = true; }
|
|
return getPreviousElement(rootElement, currentElement, checkNode, false /*suppressParentTraversal*/, true /*traverseChildren*/, includeElementsInFocusZones, false /*allowFocusRoot*/, true /*tabbable*/, includeShadowRoots);
|
|
}
|
|
exports.getLastTabbable = getLastTabbable;
|
|
/**
|
|
* Attempts to focus the first focusable element that is a child or child's child of the rootElement.
|
|
*
|
|
* @public
|
|
* @param rootElement - Element to start the search for a focusable child.
|
|
* @param bypassHiddenElements - If true, focus will be not be set on hidden elements.
|
|
* @returns True if focus was set, false if it was not.
|
|
*/
|
|
function focusFirstChild(rootElement, bypassHiddenElements, includeShadowRoots) {
|
|
var element = getNextElement(rootElement, rootElement, true, false, false, true, undefined, undefined, bypassHiddenElements, includeShadowRoots);
|
|
if (element) {
|
|
focusAsync(element);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
exports.focusFirstChild = focusFirstChild;
|
|
/**
|
|
* Traverse to find the previous element.
|
|
* If tabbable is true, the element must have tabIndex != -1.
|
|
*
|
|
* @public
|
|
*/
|
|
function getPreviousElement(rootElement, currentElement, checkNode, suppressParentTraversal, traverseChildren, includeElementsInFocusZones, allowFocusRoot, tabbable, includeShadowRoots) {
|
|
var _a;
|
|
if (!currentElement || (!allowFocusRoot && currentElement === rootElement)) {
|
|
return null;
|
|
}
|
|
var isCurrentElementVisible = isElementVisible(currentElement);
|
|
// Check its children.
|
|
if (traverseChildren &&
|
|
isCurrentElementVisible &&
|
|
(includeElementsInFocusZones || !(isElementFocusZone(currentElement) || isElementFocusSubZone(currentElement)))) {
|
|
var lastElementChild = (currentElement.lastElementChild ||
|
|
(includeShadowRoots && ((_a = currentElement.shadowRoot) === null || _a === void 0 ? void 0 : _a.lastElementChild)));
|
|
var childMatch = getPreviousElement(rootElement, lastElementChild, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable, includeShadowRoots);
|
|
if (childMatch) {
|
|
if ((tabbable && isElementTabbable(childMatch, true, includeShadowRoots)) || !tabbable) {
|
|
return childMatch;
|
|
}
|
|
var childMatchSiblingMatch = getPreviousElement(rootElement, childMatch.previousElementSibling, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable, includeShadowRoots);
|
|
if (childMatchSiblingMatch) {
|
|
return childMatchSiblingMatch;
|
|
}
|
|
var childMatchParent = childMatch.parentElement;
|
|
// At this point if we have not found any potential matches
|
|
// start looking at the rest of the subtree under the currentParent.
|
|
// NOTE: We do not want to recurse here because doing so could
|
|
// cause elements to get skipped.
|
|
while (childMatchParent && childMatchParent !== currentElement) {
|
|
var childMatchParentMatch = getPreviousElement(rootElement, childMatchParent.previousElementSibling, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable, includeShadowRoots);
|
|
if (childMatchParentMatch) {
|
|
return childMatchParentMatch;
|
|
}
|
|
childMatchParent = childMatchParent.parentElement;
|
|
}
|
|
}
|
|
}
|
|
// Check the current node, if it's not the first traversal.
|
|
if (checkNode && isCurrentElementVisible && isElementTabbable(currentElement, tabbable, includeShadowRoots)) {
|
|
return currentElement;
|
|
}
|
|
// Check its previous sibling.
|
|
var siblingMatch = getPreviousElement(rootElement, currentElement.previousElementSibling, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable, includeShadowRoots);
|
|
if (siblingMatch) {
|
|
return siblingMatch;
|
|
}
|
|
// Check its parent.
|
|
if (!suppressParentTraversal) {
|
|
return getPreviousElement(rootElement, currentElement.parentElement, true, false, false, includeElementsInFocusZones, allowFocusRoot, tabbable, includeShadowRoots);
|
|
}
|
|
return null;
|
|
}
|
|
exports.getPreviousElement = getPreviousElement;
|
|
/**
|
|
* Traverse to find the next focusable element.
|
|
* If tabbable is true, the element must have tabIndex != -1.
|
|
*
|
|
* @public
|
|
* @param checkNode - Include currentElement in search when true.
|
|
*/
|
|
function getNextElement(rootElement, currentElement, checkNode, suppressParentTraversal, suppressChildTraversal, includeElementsInFocusZones, allowFocusRoot, tabbable, bypassHiddenElements, includeShadowRoots) {
|
|
var _a;
|
|
if (!currentElement || (currentElement === rootElement && suppressChildTraversal && !allowFocusRoot)) {
|
|
return null;
|
|
}
|
|
var checkElementVisibility = bypassHiddenElements ? isElementVisibleAndNotHidden : isElementVisible;
|
|
var isCurrentElementVisible = checkElementVisibility(currentElement);
|
|
// Check the current node, if it's not the first traversal.
|
|
if (checkNode && isCurrentElementVisible && isElementTabbable(currentElement, tabbable, includeShadowRoots)) {
|
|
return currentElement;
|
|
}
|
|
// Check its children.
|
|
if (!suppressChildTraversal &&
|
|
isCurrentElementVisible &&
|
|
(includeElementsInFocusZones || !(isElementFocusZone(currentElement) || isElementFocusSubZone(currentElement)))) {
|
|
var firstElementchild = (currentElement.firstElementChild ||
|
|
(includeShadowRoots && ((_a = currentElement.shadowRoot) === null || _a === void 0 ? void 0 : _a.firstElementChild)));
|
|
var childMatch = getNextElement(rootElement, firstElementchild, true, true, false, includeElementsInFocusZones, allowFocusRoot, tabbable, bypassHiddenElements, includeShadowRoots);
|
|
if (childMatch) {
|
|
return childMatch;
|
|
}
|
|
}
|
|
if (currentElement === rootElement) {
|
|
return null;
|
|
}
|
|
// Check its sibling.
|
|
var siblingMatch = getNextElement(rootElement, currentElement.nextElementSibling, true, true, false, includeElementsInFocusZones, allowFocusRoot, tabbable, bypassHiddenElements, includeShadowRoots);
|
|
if (siblingMatch) {
|
|
return siblingMatch;
|
|
}
|
|
if (!suppressParentTraversal) {
|
|
return getNextElement(rootElement, currentElement.parentElement, false, false, true, includeElementsInFocusZones, allowFocusRoot, tabbable, bypassHiddenElements, includeShadowRoots);
|
|
}
|
|
return null;
|
|
}
|
|
exports.getNextElement = getNextElement;
|
|
/**
|
|
* Determines if an element is visible.
|
|
*
|
|
* @public
|
|
*/
|
|
function isElementVisible(element) {
|
|
// If the element is not valid, return false.
|
|
if (!element || !element.getAttribute) {
|
|
return false;
|
|
}
|
|
var visibilityAttribute = element.getAttribute(IS_VISIBLE_ATTRIBUTE);
|
|
// If the element is explicitly marked with the visibility attribute, return that value as boolean.
|
|
if (visibilityAttribute !== null && visibilityAttribute !== undefined) {
|
|
return visibilityAttribute === 'true';
|
|
}
|
|
// Fallback to other methods of determining actual visibility.
|
|
return (element.offsetHeight !== 0 ||
|
|
element.offsetParent !== null ||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
element.isVisible === true); // used as a workaround for testing.
|
|
}
|
|
exports.isElementVisible = isElementVisible;
|
|
/**
|
|
* Determines if an element is visible and not hidden
|
|
* @param element - Element to check
|
|
* @returns Returns true if the given element is visible and not hidden
|
|
*
|
|
* @public
|
|
*/
|
|
function isElementVisibleAndNotHidden(element, win) {
|
|
var theWin = win !== null && win !== void 0 ? win : (0, getWindow_1.getWindow)();
|
|
return (!!element &&
|
|
isElementVisible(element) &&
|
|
!element.hidden &&
|
|
theWin.getComputedStyle(element).visibility !== 'hidden');
|
|
}
|
|
exports.isElementVisibleAndNotHidden = isElementVisibleAndNotHidden;
|
|
/**
|
|
* Determines if an element can receive focus programmatically or via a mouse click.
|
|
* If checkTabIndex is true, additionally checks to ensure the element can be focused with the tab key,
|
|
* meaning tabIndex != -1.
|
|
*
|
|
* @public
|
|
*/
|
|
function isElementTabbable(element, checkTabIndex, checkShadowRoot) {
|
|
if (checkShadowRoot === void 0) { checkShadowRoot = true; }
|
|
// If this element is null or is disabled, it is not considered tabbable.
|
|
if (!element || element.disabled) {
|
|
return false;
|
|
}
|
|
var tabIndex = 0;
|
|
var tabIndexAttributeValue = null;
|
|
if (element && element.getAttribute) {
|
|
tabIndexAttributeValue = element.getAttribute('tabIndex');
|
|
if (tabIndexAttributeValue) {
|
|
tabIndex = parseInt(tabIndexAttributeValue, 10);
|
|
}
|
|
}
|
|
var isFocusableAttribute = element.getAttribute ? element.getAttribute(IS_FOCUSABLE_ATTRIBUTE) : null;
|
|
var isTabIndexSet = tabIndexAttributeValue !== null && tabIndex >= 0;
|
|
var delegatesFocus = checkShadowRoot && element.shadowRoot ? !!element.shadowRoot.delegatesFocus : false;
|
|
var result = !!element &&
|
|
isFocusableAttribute !== 'false' &&
|
|
(element.tagName === 'A' ||
|
|
element.tagName === 'BUTTON' ||
|
|
element.tagName === 'INPUT' ||
|
|
element.tagName === 'TEXTAREA' ||
|
|
element.tagName === 'SELECT' ||
|
|
isFocusableAttribute === 'true' ||
|
|
isTabIndexSet ||
|
|
delegatesFocus);
|
|
return checkTabIndex ? tabIndex !== -1 && result : result;
|
|
}
|
|
exports.isElementTabbable = isElementTabbable;
|
|
/**
|
|
* Determines if a given element is a focus zone.
|
|
*
|
|
* @public
|
|
*/
|
|
function isElementFocusZone(element) {
|
|
return !!(element && element.getAttribute && !!element.getAttribute(FOCUSZONE_ID_ATTRIBUTE));
|
|
}
|
|
exports.isElementFocusZone = isElementFocusZone;
|
|
/**
|
|
* Determines if a given element is a focus sub zone.
|
|
*
|
|
* @public
|
|
*/
|
|
function isElementFocusSubZone(element) {
|
|
return !!(element && element.getAttribute && element.getAttribute(FOCUSZONE_SUB_ATTRIBUTE) === 'true');
|
|
}
|
|
exports.isElementFocusSubZone = isElementFocusSubZone;
|
|
/**
|
|
* Determines if an element, or any of its children, contain focus.
|
|
*
|
|
* @public
|
|
*/
|
|
function doesElementContainFocus(element) {
|
|
var doc = (0, getDocument_1.getDocument)(element);
|
|
var currentActiveElement = doc && doc.activeElement;
|
|
if (currentActiveElement && (0, elementContains_1.elementContains)(element, currentActiveElement)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
exports.doesElementContainFocus = doesElementContainFocus;
|
|
/**
|
|
* Determines if an, or any of its ancestors, sepcificies that it doesn't want focus to wrap
|
|
* @param element - element to start searching from
|
|
* @param noWrapDataAttribute - the no wrap data attribute to match (either)
|
|
* @returns true if focus should wrap, false otherwise
|
|
*/
|
|
function shouldWrapFocus(element, noWrapDataAttribute, doc) {
|
|
var theDoc = doc !== null && doc !== void 0 ? doc : (0, getDocument_1.getDocument)();
|
|
return (0, elementContainsAttribute_1.elementContainsAttribute)(element, noWrapDataAttribute, theDoc) === 'true' ? false : true;
|
|
}
|
|
exports.shouldWrapFocus = shouldWrapFocus;
|
|
var animationId = undefined;
|
|
/**
|
|
* Sets focus to an element asynchronously. The focus will be set at the next browser repaint,
|
|
* meaning it won't cause any extra recalculations. If more than one focusAsync is called during one frame,
|
|
* only the latest called focusAsync element will actually be focused
|
|
* @param element - The element to focus
|
|
*/
|
|
function focusAsync(element) {
|
|
if (element) {
|
|
var win = (0, getWindow_1.getWindow)(element);
|
|
if (win) {
|
|
// cancel any previous focus queues
|
|
if (animationId !== undefined) {
|
|
win.cancelAnimationFrame(animationId);
|
|
}
|
|
// element.focus() is a no-op if the element is no longer in the DOM, meaning this is always safe
|
|
animationId = win.requestAnimationFrame(function () {
|
|
element && element.focus();
|
|
// We are done focusing for this frame, so reset the queued focus element
|
|
animationId = undefined;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
exports.focusAsync = focusAsync;
|
|
/**
|
|
* Finds the closest focusable element via an index path from a parent. See
|
|
* `getElementIndexPath` for getting an index path from an element to a child.
|
|
*/
|
|
function getFocusableByIndexPath(parent, path) {
|
|
var element = parent;
|
|
for (var _i = 0, path_1 = path; _i < path_1.length; _i++) {
|
|
var index = path_1[_i];
|
|
var nextChild = element.children[Math.min(index, element.children.length - 1)];
|
|
if (!nextChild) {
|
|
break;
|
|
}
|
|
element = nextChild;
|
|
}
|
|
element =
|
|
isElementTabbable(element) && isElementVisible(element)
|
|
? element
|
|
: getNextElement(parent, element, true) || getPreviousElement(parent, element);
|
|
return element;
|
|
}
|
|
exports.getFocusableByIndexPath = getFocusableByIndexPath;
|
|
/**
|
|
* Finds the element index path from a parent element to a child element.
|
|
*
|
|
* If you had this node structure: "A has children [B, C] and C has child D",
|
|
* the index path from A to D would be [1, 0], or `parent.chidren[1].children[0]`.
|
|
*/
|
|
function getElementIndexPath(fromElement, toElement) {
|
|
var path = [];
|
|
while (toElement && fromElement && toElement !== fromElement) {
|
|
var parent_1 = (0, getParent_1.getParent)(toElement, true);
|
|
if (parent_1 === null) {
|
|
return [];
|
|
}
|
|
path.unshift(Array.prototype.indexOf.call(parent_1.children, toElement));
|
|
toElement = parent_1;
|
|
}
|
|
return path;
|
|
}
|
|
exports.getElementIndexPath = getElementIndexPath;
|
|
//# sourceMappingURL=focus.js.map
|