@momentum-ui/react-collaboration
Version:
Cisco Momentum UI Framework for React Collaboration Applications
65 lines (59 loc) • 2.46 kB
text/typescript
export const PRESERVE_TABINDEX_CLASSNAME = 'md-nav-preserve-tabindex';
const FOCUSABLE_ELEMENT_SELECTORS =
'a[href], button, mdc-button, input, textarea, select, details, [tabindex]';
const PRESERVE_TABINDEX_SELECTORS = `[data-preserve-tabindex],.${PRESERVE_TABINDEX_CLASSNAME}`;
type Options = {
/**
* whether only tabbable children should be returned or all
*/
includeTabbableOnly?: boolean;
/**
* Exclude elements with `data-preserve-tabindex` attribute and all their children
*/
excludePreserveTabindex?: boolean;
};
const defaultOptions: Required<Options> = {
includeTabbableOnly: true,
excludePreserveTabindex: true,
};
/**
* Returns all focusable child elements as an Element Array
*
* An element focusable if it:
* - it is interactive element (anchor with href, button, input, textarea, select and details)
* - it is not disabled
* - it is not hidden
* - it or any of its parents do not have `aria-hidden=true` attribute
* - it or any of its parents do not have `data-preserve-tabindex` attribute or `
* md-nav-preserve-tabindex` class
* - it has none empty or not "-1" tabindex, see `includeTabbableOnly` parameter
*
* @remarks
* Element with 0 tabindex can be tabbed to, while elements with any tabindex value can be
* manually focused
*
* @param root Element lookup starts from this element
* @param options Options to customize the behavior
*/
export function getKeyboardFocusableElements<T extends HTMLElement>(
root: T,
options?: Options
): Array<HTMLElement> {
const { includeTabbableOnly, excludePreserveTabindex } = { ...defaultOptions, ...options };
const tabindex = includeTabbableOnly ? '-1' : '';
const preserveTabindexContainers = excludePreserveTabindex
? Array.from(root.querySelectorAll(PRESERVE_TABINDEX_SELECTORS))
: [];
// Exclude elements with `aris-hidden=true` attribute and all their children
const ariaHiddenContainers = Array.from(root.querySelectorAll('[aria-hidden="true"]'));
return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENT_SELECTORS)).filter(
(el) =>
!el.hasAttribute('disabled') &&
el.getAttribute('hidden') === null &&
el.getAttribute('tabindex') !== tabindex &&
// note: container.contains(container) is true
!preserveTabindexContainers.some((p) => p.contains(el)) &&
!ariaHiddenContainers.some((p) => p.contains(el)) &&
!el.hasAttribute('data-exclude-focus')
) as Array<HTMLElement>;
}