UNPKG

14.9 kBJavaScriptView Raw
1import { elementContainsAttribute } from './dom/elementContainsAttribute';
2import { elementContains } from './dom/elementContains';
3import { getParent } from './dom/getParent';
4import { getWindow } from './dom/getWindow';
5import { getDocument } from './dom/getDocument';
6var IS_FOCUSABLE_ATTRIBUTE = 'data-is-focusable';
7var IS_VISIBLE_ATTRIBUTE = 'data-is-visible';
8var FOCUSZONE_ID_ATTRIBUTE = 'data-focuszone-id';
9var FOCUSZONE_SUB_ATTRIBUTE = 'data-is-sub-focuszone';
10/**
11 * Gets the first focusable element.
12 *
13 * @public
14 */
15export function getFirstFocusable(rootElement, currentElement, includeElementsInFocusZones) {
16 return getNextElement(rootElement, currentElement, true /*checkNode*/, false /*suppressParentTraversal*/, false /*suppressChildTraversal*/, includeElementsInFocusZones);
17}
18/**
19 * Gets the last focusable element.
20 *
21 * @public
22 */
23export function getLastFocusable(rootElement, currentElement, includeElementsInFocusZones) {
24 return getPreviousElement(rootElement, currentElement, true /*checkNode*/, false /*suppressParentTraversal*/, true /*traverseChildren*/, includeElementsInFocusZones);
25}
26/**
27 * Gets the first tabbable element. (The difference between focusable and tabbable is that tabbable elements are
28 * focusable elements that also have tabIndex != -1.)
29 * @param rootElement - The parent element to search beneath.
30 * @param currentElement - The descendant of rootElement to start the search at. This element is the first one checked,
31 * and iteration continues forward. Typical use passes rootElement.firstChild.
32 * @param includeElementsInFocusZones - true if traversal should go into FocusZone descendants.
33 * @param checkNode - Include currentElement in search when true. Defaults to true.
34 * @public
35 */
36export function getFirstTabbable(rootElement, currentElement, includeElementsInFocusZones, checkNode) {
37 if (checkNode === void 0) { checkNode = true; }
38 return getNextElement(rootElement, currentElement, checkNode, false /*suppressParentTraversal*/, false /*suppressChildTraversal*/, includeElementsInFocusZones, false /*allowFocusRoot*/, true /*tabbable*/);
39}
40/**
41 * Gets the last tabbable element. (The difference between focusable and tabbable is that tabbable elements are
42 * focusable elements that also have tabIndex != -1.)
43 * @param rootElement - The parent element to search beneath.
44 * @param currentElement - The descendant of rootElement to start the search at. This element is the first one checked,
45 * and iteration continues in reverse. Typical use passes rootElement.lastChild.
46 * @param includeElementsInFocusZones - true if traversal should go into FocusZone descendants.
47 * @param checkNode - Include currentElement in search when true. Defaults to true.
48 * @public
49 */
50export function getLastTabbable(rootElement, currentElement, includeElementsInFocusZones, checkNode) {
51 if (checkNode === void 0) { checkNode = true; }
52 return getPreviousElement(rootElement, currentElement, checkNode, false /*suppressParentTraversal*/, true /*traverseChildren*/, includeElementsInFocusZones, false /*allowFocusRoot*/, true /*tabbable*/);
53}
54/**
55 * Attempts to focus the first focusable element that is a child or child's child of the rootElement.
56 *
57 * @public
58 * @param rootElement - Element to start the search for a focusable child.
59 * @returns True if focus was set, false if it was not.
60 */
61export function focusFirstChild(rootElement) {
62 var element = getNextElement(rootElement, rootElement, true, false, false, true);
63 if (element) {
64 focusAsync(element);
65 return true;
66 }
67 return false;
68}
69/**
70 * Traverse to find the previous element.
71 * If tabbable is true, the element must have tabIndex != -1.
72 *
73 * @public
74 */
75export function getPreviousElement(rootElement, currentElement, checkNode, suppressParentTraversal, traverseChildren, includeElementsInFocusZones, allowFocusRoot, tabbable) {
76 if (!currentElement || (!allowFocusRoot && currentElement === rootElement)) {
77 return null;
78 }
79 var isCurrentElementVisible = isElementVisible(currentElement);
80 // Check its children.
81 if (traverseChildren &&
82 isCurrentElementVisible &&
83 (includeElementsInFocusZones || !(isElementFocusZone(currentElement) || isElementFocusSubZone(currentElement)))) {
84 var childMatch = getPreviousElement(rootElement, currentElement.lastElementChild, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable);
85 if (childMatch) {
86 if ((tabbable && isElementTabbable(childMatch, true)) || !tabbable) {
87 return childMatch;
88 }
89 var childMatchSiblingMatch = getPreviousElement(rootElement, childMatch.previousElementSibling, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable);
90 if (childMatchSiblingMatch) {
91 return childMatchSiblingMatch;
92 }
93 var childMatchParent = childMatch.parentElement;
94 // At this point if we have not found any potential matches
95 // start looking at the rest of the subtree under the currentParent.
96 // NOTE: We do not want to recurse here because doing so could
97 // cause elements to get skipped.
98 while (childMatchParent && childMatchParent !== currentElement) {
99 var childMatchParentMatch = getPreviousElement(rootElement, childMatchParent.previousElementSibling, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable);
100 if (childMatchParentMatch) {
101 return childMatchParentMatch;
102 }
103 childMatchParent = childMatchParent.parentElement;
104 }
105 }
106 }
107 // Check the current node, if it's not the first traversal.
108 if (checkNode && isCurrentElementVisible && isElementTabbable(currentElement, tabbable)) {
109 return currentElement;
110 }
111 // Check its previous sibling.
112 var siblingMatch = getPreviousElement(rootElement, currentElement.previousElementSibling, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable);
113 if (siblingMatch) {
114 return siblingMatch;
115 }
116 // Check its parent.
117 if (!suppressParentTraversal) {
118 return getPreviousElement(rootElement, currentElement.parentElement, true, false, false, includeElementsInFocusZones, allowFocusRoot, tabbable);
119 }
120 return null;
121}
122/**
123 * Traverse to find the next focusable element.
124 * If tabbable is true, the element must have tabIndex != -1.
125 *
126 * @public
127 * @param checkNode - Include currentElement in search when true.
128 */
129export function getNextElement(rootElement, currentElement, checkNode, suppressParentTraversal, suppressChildTraversal, includeElementsInFocusZones, allowFocusRoot, tabbable) {
130 if (!currentElement || (currentElement === rootElement && suppressChildTraversal && !allowFocusRoot)) {
131 return null;
132 }
133 var isCurrentElementVisible = isElementVisible(currentElement);
134 // Check the current node, if it's not the first traversal.
135 if (checkNode && isCurrentElementVisible && isElementTabbable(currentElement, tabbable)) {
136 return currentElement;
137 }
138 // Check its children.
139 if (!suppressChildTraversal &&
140 isCurrentElementVisible &&
141 (includeElementsInFocusZones || !(isElementFocusZone(currentElement) || isElementFocusSubZone(currentElement)))) {
142 var childMatch = getNextElement(rootElement, currentElement.firstElementChild, true, true, false, includeElementsInFocusZones, allowFocusRoot, tabbable);
143 if (childMatch) {
144 return childMatch;
145 }
146 }
147 if (currentElement === rootElement) {
148 return null;
149 }
150 // Check its sibling.
151 var siblingMatch = getNextElement(rootElement, currentElement.nextElementSibling, true, true, false, includeElementsInFocusZones, allowFocusRoot, tabbable);
152 if (siblingMatch) {
153 return siblingMatch;
154 }
155 if (!suppressParentTraversal) {
156 return getNextElement(rootElement, currentElement.parentElement, false, false, true, includeElementsInFocusZones, allowFocusRoot, tabbable);
157 }
158 return null;
159}
160/**
161 * Determines if an element is visible.
162 *
163 * @public
164 */
165export function isElementVisible(element) {
166 // If the element is not valid, return false.
167 if (!element || !element.getAttribute) {
168 return false;
169 }
170 var visibilityAttribute = element.getAttribute(IS_VISIBLE_ATTRIBUTE);
171 // If the element is explicitly marked with the visibility attribute, return that value as boolean.
172 if (visibilityAttribute !== null && visibilityAttribute !== undefined) {
173 return visibilityAttribute === 'true';
174 }
175 // Fallback to other methods of determining actual visibility.
176 return (element.offsetHeight !== 0 ||
177 element.offsetParent !== null ||
178 // eslint-disable-next-line @typescript-eslint/no-explicit-any
179 element.isVisible === true); // used as a workaround for testing.
180}
181/**
182 * Determines if an element can receive focus programmatically or via a mouse click.
183 * If checkTabIndex is true, additionally checks to ensure the element can be focused with the tab key,
184 * meaning tabIndex != -1.
185 *
186 * @public
187 */
188export function isElementTabbable(element, checkTabIndex) {
189 // If this element is null or is disabled, it is not considered tabbable.
190 if (!element || element.disabled) {
191 return false;
192 }
193 var tabIndex = 0;
194 var tabIndexAttributeValue = null;
195 if (element && element.getAttribute) {
196 tabIndexAttributeValue = element.getAttribute('tabIndex');
197 if (tabIndexAttributeValue) {
198 tabIndex = parseInt(tabIndexAttributeValue, 10);
199 }
200 }
201 var isFocusableAttribute = element.getAttribute ? element.getAttribute(IS_FOCUSABLE_ATTRIBUTE) : null;
202 var isTabIndexSet = tabIndexAttributeValue !== null && tabIndex >= 0;
203 var result = !!element &&
204 isFocusableAttribute !== 'false' &&
205 (element.tagName === 'A' ||
206 element.tagName === 'BUTTON' ||
207 element.tagName === 'INPUT' ||
208 element.tagName === 'TEXTAREA' ||
209 element.tagName === 'SELECT' ||
210 isFocusableAttribute === 'true' ||
211 isTabIndexSet);
212 return checkTabIndex ? tabIndex !== -1 && result : result;
213}
214/**
215 * Determines if a given element is a focus zone.
216 *
217 * @public
218 */
219export function isElementFocusZone(element) {
220 return !!(element && element.getAttribute && !!element.getAttribute(FOCUSZONE_ID_ATTRIBUTE));
221}
222/**
223 * Determines if a given element is a focus sub zone.
224 *
225 * @public
226 */
227export function isElementFocusSubZone(element) {
228 return !!(element && element.getAttribute && element.getAttribute(FOCUSZONE_SUB_ATTRIBUTE) === 'true');
229}
230/**
231 * Determines if an element, or any of its children, contain focus.
232 *
233 * @public
234 */
235export function doesElementContainFocus(element) {
236 var document = getDocument(element);
237 var currentActiveElement = document && document.activeElement;
238 if (currentActiveElement && elementContains(element, currentActiveElement)) {
239 return true;
240 }
241 return false;
242}
243/**
244 * Determines if an, or any of its ancestors, sepcificies that it doesn't want focus to wrap
245 * @param element - element to start searching from
246 * @param noWrapDataAttribute - the no wrap data attribute to match (either)
247 * @returns true if focus should wrap, false otherwise
248 */
249export function shouldWrapFocus(element, noWrapDataAttribute) {
250 return elementContainsAttribute(element, noWrapDataAttribute) === 'true' ? false : true;
251}
252var targetToFocusOnNextRepaint = undefined;
253/**
254 * Sets focus to an element asynchronously. The focus will be set at the next browser repaint,
255 * meaning it won't cause any extra recalculations. If more than one focusAsync is called during one frame,
256 * only the latest called focusAsync element will actually be focused
257 * @param element - The element to focus
258 */
259export function focusAsync(element) {
260 if (element) {
261 // An element was already queued to be focused, so replace that one with the new element
262 if (targetToFocusOnNextRepaint) {
263 targetToFocusOnNextRepaint = element;
264 return;
265 }
266 targetToFocusOnNextRepaint = element;
267 var win = getWindow(element);
268 if (win) {
269 // element.focus() is a no-op if the element is no longer in the DOM, meaning this is always safe
270 win.requestAnimationFrame(function () {
271 var focusableElement = targetToFocusOnNextRepaint;
272 // We are done focusing for this frame, so reset the queued focus element
273 targetToFocusOnNextRepaint = undefined;
274 if (focusableElement) {
275 if (focusableElement.getAttribute && focusableElement.getAttribute(IS_FOCUSABLE_ATTRIBUTE) === 'true') {
276 // Normally, a FocusZone would be responsible for setting the tabindex values on all its descendants.
277 // However, even this animation frame callback can pre-empt the rendering of a FocusZone's child elements,
278 // so it may be necessary to set the tabindex directly here.
279 if (!focusableElement.getAttribute('tabindex')) {
280 focusableElement.setAttribute('tabindex', '0');
281 }
282 }
283 focusableElement.focus();
284 }
285 });
286 }
287 }
288}
289/**
290 * Finds the closest focusable element via an index path from a parent. See
291 * `getElementIndexPath` for getting an index path from an element to a child.
292 */
293export function getFocusableByIndexPath(parent, path) {
294 var element = parent;
295 for (var _i = 0, path_1 = path; _i < path_1.length; _i++) {
296 var index = path_1[_i];
297 var nextChild = element.children[Math.min(index, element.children.length - 1)];
298 if (!nextChild) {
299 break;
300 }
301 element = nextChild;
302 }
303 element =
304 isElementTabbable(element) && isElementVisible(element)
305 ? element
306 : getNextElement(parent, element, true) || getPreviousElement(parent, element);
307 return element;
308}
309/**
310 * Finds the element index path from a parent element to a child element.
311 *
312 * If you had this node structure: "A has children [B, C] and C has child D",
313 * the index path from A to D would be [1, 0], or `parent.chidren[1].children[0]`.
314 */
315export function getElementIndexPath(fromElement, toElement) {
316 var path = [];
317 while (toElement && fromElement && toElement !== fromElement) {
318 var parent_1 = getParent(toElement, true);
319 if (parent_1 === null) {
320 return [];
321 }
322 path.unshift(Array.prototype.indexOf.call(parent_1.children, toElement));
323 toElement = parent_1;
324 }
325 return path;
326}
327//# sourceMappingURL=focus.js.map
\No newline at end of file