UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

228 lines (218 loc) • 8.83 kB
import { c } from 'react-compiler-runtime'; import { useFocusZone } from '../hooks/useFocusZone.js'; import { getScrollContainer } from '../utils/scroll.js'; import { FocusKeys } from '@primer/behaviors'; function useRovingTabIndex(t0) { const $ = c(6); const { containerRef, mouseDownRef } = t0; let t1; if ($[0] !== containerRef || $[1] !== mouseDownRef) { t1 = () => { var _containerRef$current, _containerRef$current2, _containerRef$current3; if (mouseDownRef.current) { return; } const currentItem = (_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.querySelector("[aria-current]"); const firstItem = (_containerRef$current2 = containerRef.current) === null || _containerRef$current2 === void 0 ? void 0 : _containerRef$current2.querySelector("[role=\"treeitem\"]"); if (currentItem instanceof HTMLElement) { return currentItem; } if (document.activeElement instanceof HTMLElement && (_containerRef$current3 = containerRef.current) !== null && _containerRef$current3 !== void 0 && _containerRef$current3.contains(document.activeElement) && document.activeElement.getAttribute("role") === "treeitem") { return document.activeElement; } return firstItem instanceof HTMLElement ? firstItem : undefined; }; $[0] = containerRef; $[1] = mouseDownRef; $[2] = t1; } else { t1 = $[2]; } let t2; if ($[3] !== containerRef || $[4] !== t1) { t2 = { containerRef, bindKeys: FocusKeys.ArrowVertical | FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd | FocusKeys.Backspace | FocusKeys.PageUpDown, preventScroll: true, getNextFocusable: _temp, focusInStrategy: t1 }; $[3] = containerRef; $[4] = t1; $[5] = t2; } else { t2 = $[5]; } useFocusZone(t2); } // DOM utilities used for focus management function _temp(direction, from, event) { var _getNextFocusableElem; if (!(from instanceof HTMLElement)) { return; } try { if (from.closest("dialog:modal")) { return; } } catch {} return (_getNextFocusableElem = getNextFocusableElement(from, event)) !== null && _getNextFocusableElem !== void 0 ? _getNextFocusableElem : from; } function getNextFocusableElement(activeElement, event) { const elementState = getElementState(activeElement); // Reference: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/#keyboard-interaction-24 switch (`${elementState} ${event.key}`) { case 'open ArrowRight': // Focus first child node return getFirstChildElement(activeElement); case 'open ArrowLeft': // Close node; don't change focus return; case 'closed ArrowRight': // Open node; don't change focus return; case 'closed ArrowLeft': // Focus parent element return getParentElement(activeElement); case 'end ArrowRight': // Do nothing return; case 'end ArrowLeft': // Focus parent element return getParentElement(activeElement); } // ArrowUp, ArrowDown, Home, and End behavior are the same regardless of element state switch (event.key) { case 'ArrowUp': // Focus previous visible element return getVisibleElement(activeElement, 'previous'); case 'ArrowDown': // Focus next visible element return getVisibleElement(activeElement, 'next'); case 'Backspace': return getParentElement(activeElement); case 'Home': // Focus first visible element return getFirstElement(activeElement); case 'End': // Focus last visible element return getLastElement(activeElement); case 'PageUp': return getPreviousPageElement(activeElement); case 'PageDown': return getNextPageElement(activeElement); } } function getElementState(element) { if (element.getAttribute('role') !== 'treeitem') { throw new Error('Element is not a treeitem'); } switch (element.getAttribute('aria-expanded')) { case 'true': return 'open'; case 'false': return 'closed'; default: return 'end'; } } function getVisibleElement(element, direction) { const root = element.closest('[role=tree]'); if (!root) return; const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, node => { if (!(node instanceof HTMLElement)) return NodeFilter.FILTER_SKIP; return node.getAttribute('role') === 'treeitem' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; }); let current = walker.firstChild(); while (current !== element) { current = walker.nextNode(); } let next = direction === 'next' ? walker.nextNode() : walker.previousNode(); // If next element is nested inside a collapsed subtree, continue iterating while (next instanceof HTMLElement && (_next$parentElement = next.parentElement) !== null && _next$parentElement !== void 0 && _next$parentElement.closest('[role=treeitem][aria-expanded=false]')) { var _next$parentElement; next = direction === 'next' ? walker.nextNode() : walker.previousNode(); } return next instanceof HTMLElement ? next : undefined; } function getFirstChildElement(element) { const firstChild = element.querySelector('[role=treeitem]'); return firstChild instanceof HTMLElement ? firstChild : undefined; } function getParentElement(element) { const group = element.closest('[role=group]'); const parent = group === null || group === void 0 ? void 0 : group.closest('[role=treeitem]'); return parent instanceof HTMLElement ? parent : undefined; } function getFirstElement(element) { const root = element.closest('[role=tree]'); const first = root === null || root === void 0 ? void 0 : root.querySelector('[role=treeitem]'); return first instanceof HTMLElement ? first : undefined; } function getLastElement(element) { const root = element.closest('[role=tree]'); const items = Array.from((root === null || root === void 0 ? void 0 : root.querySelectorAll('[role=treeitem]')) || []); // If there are no items, return undefined if (items.length === 0) return; let index = items.length - 1; let last = items[index]; // If last element is nested inside a collapsed subtree, continue iterating while (index > 0 && last instanceof HTMLElement && (_last$parentElement = last.parentElement) !== null && _last$parentElement !== void 0 && _last$parentElement.closest('[role=treeitem][aria-expanded=false]')) { var _last$parentElement; index -= 1; last = items[index]; } return last instanceof HTMLElement ? last : undefined; } const defaultSize = { height: 32 }; /** * Determine the page size for the given tree based on an item in the tree. We * estimate this size by trying to see how many items will fit in the given * tree. If the tree is within a scroll container, we will use the height of * that container. Otherwise, we'll use the current window height */ function getPageSize(root, item) { var _item$getBoundingClie, _scrollContainer$clie; const scrollContainer = getScrollContainer(root); const { height: itemHeight } = (_item$getBoundingClie = item === null || item === void 0 ? void 0 : item.getBoundingClientRect()) !== null && _item$getBoundingClie !== void 0 ? _item$getBoundingClie : defaultSize; const availableHeight = (_scrollContainer$clie = scrollContainer === null || scrollContainer === void 0 ? void 0 : scrollContainer.clientHeight) !== null && _scrollContainer$clie !== void 0 ? _scrollContainer$clie : window.innerHeight; return Math.floor(availableHeight / itemHeight); } function getNextPageElement(element) { const root = element.closest('[role="tree"]'); if (!root) { return; } const items = Array.from(root.querySelectorAll('[role="treeitem"]')); if (items.length === 0) { return; } const itemLabel = items[0].firstElementChild; const pageSize = getPageSize(root, itemLabel); const page = Math.floor(items.indexOf(element) / pageSize); const offset = items.indexOf(element) - pageSize * page; return items[Math.min(items.length - 1, (page + 1) * pageSize + offset)]; } function getPreviousPageElement(element) { const root = element.closest('[role="tree"]'); if (!root) { return; } const items = Array.from(root.querySelectorAll('[role="treeitem"]')); if (items.length === 0) { return; } const itemLabel = items[0].firstElementChild; const pageSize = getPageSize(root, itemLabel); const page = Math.floor(items.indexOf(element) / pageSize); const offset = items.indexOf(element) - pageSize * page; return items[Math.max(0, (page - 1) * pageSize + offset)]; } export { getElementState, getFirstChildElement, getFirstElement, getLastElement, getNextFocusableElement, getParentElement, getVisibleElement, useRovingTabIndex };