UNPKG

@kubit-ui-web/react-components

Version:

Kubit React Components is a customizable, accessible library of React web components, designed to enhance your application's user experience

163 lines 6.35 kB
const FOCUSABLE_QUERY_SELECTOR = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]):not([tabindex="-1"]), summary, [tabindex]:not([tabindex="-1"])'; export const getFocusableDescendants = (element) => { const focusableNodes = Array.from(element.querySelectorAll(FOCUSABLE_QUERY_SELECTOR)).filter(node => !node.hasAttribute('aria-disabled:true') && !node.getAttribute('aria-hidden:true') && !node.hasAttribute('disabled')); return focusableNodes?.length ? focusableNodes : false; }; /** * Retrieves an array of focusable descendants within a given element. * * @param element - The element to search for focusable descendants. * @param elementsToOmit - Optional array of elements to omit from the search. * @returns An array of HTMLElements representing the focusable descendants. */ export const getFocusableDescendantsV2 = ({ element, elementsToOmit, }) => { const focusableNodes = Array.from(element.querySelectorAll(FOCUSABLE_QUERY_SELECTOR)).filter(node => { // It is not focusable if it is disabled if (node.getAttribute('aria-disabled') === 'true' || node.hasAttribute('disabled')) { return false; } // It is not focusable if it is aria-hidden if (node.getAttribute('aria-hidden') === 'true') { return false; } // It is not focusable if it is inside elements to omit if (elementsToOmit?.some(elementToOmit => elementToOmit.contains(node))) { return false; } // It is not focusable if it is not a summary element and it is inside details closed element if (node.tagName !== 'SUMMARY' && node.closest('details:not([open])')) { return false; } return true; }); return focusableNodes; }; /** * Find the next focusable element (without looking to children) * Return true when a focusable element has been found and focused * @param element * @returns boolean */ export const focusNextFocusableElement = (element) => { let focusableElement = element; let foundFocusableElement = false; // Search next focusable element while (focusableElement) { while (focusableElement.nextElementSibling) { focusableElement = focusableElement.nextElementSibling; if (isElementOrSomeChildrenFocusable(focusableElement)) { foundFocusableElement = true; break; } } if (foundFocusableElement) { break; } focusableElement = focusableElement.parentNode; } // If next focusable element has been found if (foundFocusableElement) { focusElementOrFirstDescendant(focusableElement); } return foundFocusableElement; }; /** * Find the previous focusable element (without looking to children) * Return true when a focusable element has been found and focused * @param element * @returns boolean */ export const focusPreviousFocusableElement = (element) => { let focusableElement = element; let foundFocusableElement = false; let focusableElementIsParent = false; while (focusableElement) { while (focusableElement.previousElementSibling) { focusableElement = focusableElement.previousElementSibling; if (isElementOrSomeChildrenFocusable(focusableElement)) { foundFocusableElement = true; break; } } if (foundFocusableElement) { break; } focusableElement = focusableElement.parentNode; // Father is the previous focusable element if (isElementFocusable(focusableElement)) { foundFocusableElement = true; focusableElementIsParent = true; break; } } if (foundFocusableElement) { if (focusableElementIsParent) { focusableElement.focus(); } else { focusLastDescendantOrElement(focusableElement); } } return foundFocusableElement; }; const isElementFocusable = (element) => { return (!element.hasAttribute('aria-disabled:true') && !element.getAttribute('aria-hidden:true') && element.matches(FOCUSABLE_QUERY_SELECTOR)); }; const isSomeChildrenFocusable = (element) => { const focusableElements = getFocusableDescendants(element); return !!(typeof focusableElements !== 'boolean' && focusableElements?.length); }; const isElementOrSomeChildrenFocusable = (element) => { return isElementFocusable(element) || isSomeChildrenFocusable(element); }; export const focusElementOrFirstDescendant = (element) => { if (isElementFocusable(element)) { element.focus(); return; } focusFirstDescendant(element); }; export const focusLastDescendantOrElement = (element) => { const focusableElements = getFocusableDescendants(element); if (typeof focusableElements !== 'boolean' && focusableElements?.length) { const focusableElement = focusableElements[focusableElements.length - 1]; focusableElement.focus(); return; } if (isElementFocusable(element)) { element.focus(); } }; export const focusFirstDescendant = (element) => { if (element?.childNodes?.length) { const focusableElements = getFocusableDescendants(element); if (typeof focusableElements !== 'boolean' && focusableElements?.length) { focusableElements.some(node => { node.focus(); return document.activeElement === node; }); } } }; /** * Sets focus on the first focusable descendant of the given element. * * @param {Object} options - The options for focusing the first descendant. * @param {HTMLElement} options.element - The element on which to set focus. * @param {HTMLElement[]} [options.elementsToOmit] - An optional array of elements to omit from focus. * * @returns {void} */ export const focusFirstDescendantV2 = ({ element, elementsToOmit, }) => { if (element?.childNodes?.length) { const focusableElements = getFocusableDescendantsV2({ element, elementsToOmit }); if (focusableElements.length) { focusableElements[0].focus(); } } }; //# sourceMappingURL=focusHandlers.js.map