@primer/react
Version:
An implementation of GitHub's Primer Design System using React
228 lines (218 loc) • 8.83 kB
JavaScript
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 };