@primer/react
Version:
An implementation of GitHub's Primer Design System using React
318 lines (315 loc) • 8.74 kB
JavaScript
import { c } from 'react-compiler-runtime';
import React, { useId, createContext, useContext } from 'react';
import useIsomorphicLayoutEffect from '../../utils/useIsomorphicLayoutEffect.js';
import { useControllableState } from '../../hooks/useControllableState.js';
import { jsx } from 'react/jsx-runtime';
import { useProvidedRefOrCreate } from '../../hooks/useProvidedRefOrCreate.js';
/**
* The Tabs component provides the base structure for a tabbed interface, without providing any formal requirement on DOM structure or styling.
* It manages the state of the selected tab, handles tab ordering/selection and provides context to its child components to ensure an accessible experience.
*
* This is intended to be used in conjunction with the `useTab`, `useTabList`, and `useTabPanel` hooks to build a fully accessible tabs component.
* The `Tab`, `TabList`, and `TabPanel` components are provided for convenience to showcase the API & implementation.
*/
function Tabs(props) {
var _props$defaultValue;
const $ = c(13);
const {
children,
onValueChange
} = props;
const groupId = useId();
const t0 = (_props$defaultValue = props.defaultValue) !== null && _props$defaultValue !== void 0 ? _props$defaultValue : props.value;
let t1;
if ($[0] !== props.value || $[1] !== t0) {
t1 = {
name: "tab-selection",
defaultValue: t0,
value: props.value
};
$[0] = props.value;
$[1] = t0;
$[2] = t1;
} else {
t1 = $[2];
}
const [selectedValue, setSelectedValue] = useControllableState(t1);
const savedOnValueChange = React.useRef(onValueChange);
let t2;
if ($[3] !== groupId || $[4] !== selectedValue || $[5] !== setSelectedValue) {
t2 = {
groupId,
selectedValue,
selectTab(value) {
var _savedOnValueChange$c;
setSelectedValue(value);
(_savedOnValueChange$c = savedOnValueChange.current) === null || _savedOnValueChange$c === void 0 ? void 0 : _savedOnValueChange$c.call(savedOnValueChange, {
value
});
}
};
$[3] = groupId;
$[4] = selectedValue;
$[5] = setSelectedValue;
$[6] = t2;
} else {
t2 = $[6];
}
const contextValue = t2;
let t3;
let t4;
if ($[7] !== onValueChange) {
t3 = () => {
savedOnValueChange.current = onValueChange;
};
t4 = [onValueChange];
$[7] = onValueChange;
$[8] = t3;
$[9] = t4;
} else {
t3 = $[8];
t4 = $[9];
}
useIsomorphicLayoutEffect(t3, t4);
let t5;
if ($[10] !== children || $[11] !== contextValue) {
t5 = /*#__PURE__*/jsx(TabsContext.Provider, {
value: contextValue,
children: children
});
$[10] = children;
$[11] = contextValue;
$[12] = t5;
} else {
t5 = $[12];
}
return t5;
}
function useTabList(props) {
const $ = c(9);
const {
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
"aria-orientation": ariaOrientation
} = props;
const ref = useProvidedRefOrCreate(props.ref);
let t0;
if ($[0] !== ariaOrientation || $[1] !== ref) {
t0 = event => {
const {
current: tablist
} = ref;
if (tablist === null) {
return;
}
const tabs = getFocusableTabs(tablist);
const isVertical = ariaOrientation === "vertical";
const nextKey = isVertical ? "ArrowDown" : "ArrowRight";
const prevKey = isVertical ? "ArrowUp" : "ArrowLeft";
if (event.key === nextKey || event.key === prevKey || event.key === "Home" || event.key === "End") {
event.preventDefault();
event.stopPropagation();
}
if (event.key === nextKey) {
const selectedTabIndex = tabs.findIndex(_temp);
if (selectedTabIndex === -1) {
return;
}
const nextTabIndex = (selectedTabIndex + 1) % tabs.length;
tabs[nextTabIndex].focus();
} else {
if (event.key === prevKey) {
const selectedTabIndex_0 = tabs.findIndex(_temp2);
if (selectedTabIndex_0 === -1) {
return;
}
const nextTabIndex_0 = (tabs.length + selectedTabIndex_0 - 1) % tabs.length;
tabs[nextTabIndex_0].focus();
} else {
if (event.key === "Home") {
if (tabs[0]) {
tabs[0].focus();
}
} else {
if (event.key === "End") {
if (tabs.length > 0) {
tabs[tabs.length - 1].focus();
}
}
}
}
}
};
$[0] = ariaOrientation;
$[1] = ref;
$[2] = t0;
} else {
t0 = $[2];
}
const onKeyDown = t0;
const t1 = ariaOrientation !== null && ariaOrientation !== void 0 ? ariaOrientation : "horizontal";
let t2;
if ($[3] !== ariaLabel || $[4] !== ariaLabelledby || $[5] !== onKeyDown || $[6] !== ref || $[7] !== t1) {
t2 = {
tabListProps: {
ref,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
"aria-orientation": t1,
role: "tablist",
onKeyDown
}
};
$[3] = ariaLabel;
$[4] = ariaLabelledby;
$[5] = onKeyDown;
$[6] = ref;
$[7] = t1;
$[8] = t2;
} else {
t2 = $[8];
}
return t2;
}
function _temp2(tab_0) {
return tab_0.getAttribute("aria-selected") === "true";
}
function _temp(tab) {
return tab.getAttribute("aria-selected") === "true";
}
function getFocusableTabs(tablist) {
return Array.from(tablist.querySelectorAll('[role="tab"]:not([aria-disabled])'));
}
/**
* A custom hook that provides the props needed for a tab component.
* The props returned should be spread onto the component (typically a button) with the `role=tab`, under a `tablist`.
*/
function useTab(props) {
const $ = c(21);
const {
disabled,
value
} = props;
const tabs = useTabs();
const selected = tabs.selectedValue === value;
const id = `${tabs.groupId}-tab-${value}`;
const panelId = `${tabs.groupId}-panel-${value}`;
let t0;
if ($[0] !== tabs || $[1] !== value) {
t0 = function onKeyDown(event) {
if (event.key === " " || event.key === "Enter") {
tabs.selectTab(value);
}
};
$[0] = tabs;
$[1] = value;
$[2] = t0;
} else {
t0 = $[2];
}
const onKeyDown = t0;
let t1;
if ($[3] !== disabled || $[4] !== tabs || $[5] !== value) {
t1 = function onMouseDown(event_0) {
if (!disabled && event_0.button === 0 && event_0.ctrlKey === false) {
tabs.selectTab(value);
} else {
event_0.preventDefault();
}
};
$[3] = disabled;
$[4] = tabs;
$[5] = value;
$[6] = t1;
} else {
t1 = $[6];
}
const onMouseDown = t1;
let t2;
if ($[7] !== disabled || $[8] !== selected || $[9] !== tabs || $[10] !== value) {
t2 = function onFocus() {
if (!selected && !disabled) {
tabs.selectTab(value);
}
};
$[7] = disabled;
$[8] = selected;
$[9] = tabs;
$[10] = value;
$[11] = t2;
} else {
t2 = $[11];
}
const onFocus = t2;
const t3 = disabled ? true : undefined;
const t4 = selected ? 0 : -1;
let t5;
if ($[12] !== id || $[13] !== onFocus || $[14] !== onKeyDown || $[15] !== onMouseDown || $[16] !== panelId || $[17] !== selected || $[18] !== t3 || $[19] !== t4) {
t5 = {
tabProps: {
"aria-disabled": t3,
"aria-controls": panelId,
"aria-selected": selected,
onKeyDown,
onMouseDown,
onFocus,
id,
role: "tab",
tabIndex: t4
}
};
$[12] = id;
$[13] = onFocus;
$[14] = onKeyDown;
$[15] = onMouseDown;
$[16] = panelId;
$[17] = selected;
$[18] = t3;
$[19] = t4;
$[20] = t5;
} else {
t5 = $[20];
}
return t5;
}
/** Utility hook for tab panels */
function useTabPanel(props) {
const $ = c(5);
const {
value
} = props;
const tabs = useTabs();
const id = `${tabs.groupId}-panel-${value}`;
const tabId = `${tabs.groupId}-tab-${value}`;
const t0 = tabs.selectedValue === value ? "" : undefined;
const t1 = tabs.selectedValue !== value;
let t2;
if ($[0] !== id || $[1] !== t0 || $[2] !== t1 || $[3] !== tabId) {
t2 = {
tabPanelProps: {
"aria-labelledby": tabId,
"data-selected": t0,
id,
hidden: t1,
role: "tabpanel"
}
};
$[0] = id;
$[1] = t0;
$[2] = t1;
$[3] = tabId;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
const TabsContext = /*#__PURE__*/createContext(null);
function useTabs() {
const context = useContext(TabsContext);
if (context) {
return context;
}
throw new Error("Component must be used within a <Tabs> component");
}
export { Tabs, useTab, useTabList, useTabPanel };