@uifabric/utilities
Version:
Fluent UI React utilities for building components.
327 lines • 14.9 kB
JavaScript
import { elementContainsAttribute } from './dom/elementContainsAttribute';
import { elementContains } from './dom/elementContains';
import { getParent } from './dom/getParent';
import { getWindow } from './dom/getWindow';
import { getDocument } from './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
*/
export function getFirstFocusable(rootElement, currentElement, includeElementsInFocusZones) {
return getNextElement(rootElement, currentElement, true /*checkNode*/, false /*suppressParentTraversal*/, false /*suppressChildTraversal*/, includeElementsInFocusZones);
}
/**
* Gets the last focusable element.
*
* @public
*/
export function getLastFocusable(rootElement, currentElement, includeElementsInFocusZones) {
return getPreviousElement(rootElement, currentElement, true /*checkNode*/, false /*suppressParentTraversal*/, true /*traverseChildren*/, includeElementsInFocusZones);
}
/**
* 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
*/
export function getFirstTabbable(rootElement, currentElement, includeElementsInFocusZones, checkNode) {
if (checkNode === void 0) { checkNode = true; }
return getNextElement(rootElement, currentElement, checkNode, false /*suppressParentTraversal*/, false /*suppressChildTraversal*/, includeElementsInFocusZones, false /*allowFocusRoot*/, true /*tabbable*/);
}
/**
* 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
*/
export function getLastTabbable(rootElement, currentElement, includeElementsInFocusZones, checkNode) {
if (checkNode === void 0) { checkNode = true; }
return getPreviousElement(rootElement, currentElement, checkNode, false /*suppressParentTraversal*/, true /*traverseChildren*/, includeElementsInFocusZones, false /*allowFocusRoot*/, true /*tabbable*/);
}
/**
* 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.
* @returns True if focus was set, false if it was not.
*/
export function focusFirstChild(rootElement) {
var element = getNextElement(rootElement, rootElement, true, false, false, true);
if (element) {
focusAsync(element);
return true;
}
return false;
}
/**
* Traverse to find the previous element.
* If tabbable is true, the element must have tabIndex != -1.
*
* @public
*/
export function getPreviousElement(rootElement, currentElement, checkNode, suppressParentTraversal, traverseChildren, includeElementsInFocusZones, allowFocusRoot, tabbable) {
if (!currentElement || (!allowFocusRoot && currentElement === rootElement)) {
return null;
}
var isCurrentElementVisible = isElementVisible(currentElement);
// Check its children.
if (traverseChildren &&
isCurrentElementVisible &&
(includeElementsInFocusZones || !(isElementFocusZone(currentElement) || isElementFocusSubZone(currentElement)))) {
var childMatch = getPreviousElement(rootElement, currentElement.lastElementChild, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable);
if (childMatch) {
if ((tabbable && isElementTabbable(childMatch, true)) || !tabbable) {
return childMatch;
}
var childMatchSiblingMatch = getPreviousElement(rootElement, childMatch.previousElementSibling, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable);
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);
if (childMatchParentMatch) {
return childMatchParentMatch;
}
childMatchParent = childMatchParent.parentElement;
}
}
}
// Check the current node, if it's not the first traversal.
if (checkNode && isCurrentElementVisible && isElementTabbable(currentElement, tabbable)) {
return currentElement;
}
// Check its previous sibling.
var siblingMatch = getPreviousElement(rootElement, currentElement.previousElementSibling, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable);
if (siblingMatch) {
return siblingMatch;
}
// Check its parent.
if (!suppressParentTraversal) {
return getPreviousElement(rootElement, currentElement.parentElement, true, false, false, includeElementsInFocusZones, allowFocusRoot, tabbable);
}
return null;
}
/**
* 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.
*/
export function getNextElement(rootElement, currentElement, checkNode, suppressParentTraversal, suppressChildTraversal, includeElementsInFocusZones, allowFocusRoot, tabbable) {
if (!currentElement || (currentElement === rootElement && suppressChildTraversal && !allowFocusRoot)) {
return null;
}
var isCurrentElementVisible = isElementVisible(currentElement);
// Check the current node, if it's not the first traversal.
if (checkNode && isCurrentElementVisible && isElementTabbable(currentElement, tabbable)) {
return currentElement;
}
// Check its children.
if (!suppressChildTraversal &&
isCurrentElementVisible &&
(includeElementsInFocusZones || !(isElementFocusZone(currentElement) || isElementFocusSubZone(currentElement)))) {
var childMatch = getNextElement(rootElement, currentElement.firstElementChild, true, true, false, includeElementsInFocusZones, allowFocusRoot, tabbable);
if (childMatch) {
return childMatch;
}
}
if (currentElement === rootElement) {
return null;
}
// Check its sibling.
var siblingMatch = getNextElement(rootElement, currentElement.nextElementSibling, true, true, false, includeElementsInFocusZones, allowFocusRoot, tabbable);
if (siblingMatch) {
return siblingMatch;
}
if (!suppressParentTraversal) {
return getNextElement(rootElement, currentElement.parentElement, false, false, true, includeElementsInFocusZones, allowFocusRoot, tabbable);
}
return null;
}
/**
* Determines if an element is visible.
*
* @public
*/
export 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.
}
/**
* 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
*/
export function isElementTabbable(element, checkTabIndex) {
// 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 result = !!element &&
isFocusableAttribute !== 'false' &&
(element.tagName === 'A' ||
element.tagName === 'BUTTON' ||
element.tagName === 'INPUT' ||
element.tagName === 'TEXTAREA' ||
element.tagName === 'SELECT' ||
isFocusableAttribute === 'true' ||
isTabIndexSet);
return checkTabIndex ? tabIndex !== -1 && result : result;
}
/**
* Determines if a given element is a focus zone.
*
* @public
*/
export function isElementFocusZone(element) {
return !!(element && element.getAttribute && !!element.getAttribute(FOCUSZONE_ID_ATTRIBUTE));
}
/**
* Determines if a given element is a focus sub zone.
*
* @public
*/
export function isElementFocusSubZone(element) {
return !!(element && element.getAttribute && element.getAttribute(FOCUSZONE_SUB_ATTRIBUTE) === 'true');
}
/**
* Determines if an element, or any of its children, contain focus.
*
* @public
*/
export function doesElementContainFocus(element) {
var document = getDocument(element);
var currentActiveElement = document && document.activeElement;
if (currentActiveElement && elementContains(element, currentActiveElement)) {
return true;
}
return false;
}
/**
* 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
*/
export function shouldWrapFocus(element, noWrapDataAttribute) {
return elementContainsAttribute(element, noWrapDataAttribute) === 'true' ? false : true;
}
var targetToFocusOnNextRepaint = 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
*/
export function focusAsync(element) {
if (element) {
// An element was already queued to be focused, so replace that one with the new element
if (targetToFocusOnNextRepaint) {
targetToFocusOnNextRepaint = element;
return;
}
targetToFocusOnNextRepaint = element;
var win = getWindow(element);
if (win) {
// element.focus() is a no-op if the element is no longer in the DOM, meaning this is always safe
win.requestAnimationFrame(function () {
var focusableElement = targetToFocusOnNextRepaint;
// We are done focusing for this frame, so reset the queued focus element
targetToFocusOnNextRepaint = undefined;
if (focusableElement) {
if (focusableElement.getAttribute && focusableElement.getAttribute(IS_FOCUSABLE_ATTRIBUTE) === 'true') {
// Normally, a FocusZone would be responsible for setting the tabindex values on all its descendants.
// However, even this animation frame callback can pre-empt the rendering of a FocusZone's child elements,
// so it may be necessary to set the tabindex directly here.
if (!focusableElement.getAttribute('tabindex')) {
focusableElement.setAttribute('tabindex', '0');
}
}
focusableElement.focus();
}
});
}
}
}
/**
* 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.
*/
export 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;
}
/**
* 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]`.
*/
export function getElementIndexPath(fromElement, toElement) {
var path = [];
while (toElement && fromElement && toElement !== fromElement) {
var parent_1 = getParent(toElement, true);
if (parent_1 === null) {
return [];
}
path.unshift(Array.prototype.indexOf.call(parent_1.children, toElement));
toElement = parent_1;
}
return path;
}
//# sourceMappingURL=focus.js.map