UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

318 lines (315 loc) • 8.74 kB
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 };