@theguild/components
Version:
226 lines (225 loc) • 7.24 kB
JavaScript
"use client";
import { jsx, jsxs } from "react/jsx-runtime";
import {
Fragment,
useEffect,
useId,
useLayoutEffect,
useRef,
useState
} from "react";
import { useSearchParams } from "next/navigation";
import cn from "clsx";
import {
Tab as HeadlessTab,
TabGroup,
TabList,
TabPanel,
TabPanels
} from "@headlessui/react";
import { useHash } from "../use-hash";
function isTabObjectItem(item) {
return !!item && typeof item === "object" && "label" in item;
}
const Tabs = ({
items,
children,
searchParamKey = "tab",
storageKey = null,
defaultIndex = 0,
selectedIndex: _selectedIndex,
onChange,
className,
tabClassName
}) => {
const id = useId();
if (storageKey === true) {
storageKey = `tabs-${id}`;
}
let [selectedIndex, setSelectedIndex] = useState(defaultIndex);
if (_selectedIndex !== void 0) {
selectedIndex = _selectedIndex;
}
const tabPanelsRef = useRef(null);
const tabIndexFromSearchParams = useActiveTabFromURL(
tabPanelsRef,
items,
searchParamKey,
setSelectedIndex,
id
);
useActiveTabFromStorage(storageKey, items, setSelectedIndex, tabIndexFromSearchParams !== -1, id);
const handleChange = (index) => {
onChange?.(index);
if (storageKey) {
const newValue = getTabKey(items, index, id);
localStorage.setItem(storageKey, newValue);
window.dispatchEvent(new StorageEvent("storage", { key: storageKey, newValue }));
} else {
setSelectedIndex(index);
}
if (searchParamKey) {
const searchParams = new URLSearchParams(window.location.search);
const tabKeys = new Set(searchParams.getAll(searchParamKey));
for (let i = 0; i < items.length; i++) {
const key = getTabKey(items, i, id);
tabKeys.delete(key);
}
searchParams.delete(searchParamKey);
for (const key of tabKeys) {
searchParams.append(searchParamKey, key);
}
searchParams.append(searchParamKey, getTabKey(items, index, id));
window.history.replaceState(
null,
"",
`${window.location.pathname}?${searchParams.toString()}`
);
}
};
return /* @__PURE__ */ jsxs(
TabGroup,
{
selectedIndex,
defaultIndex,
onChange: handleChange,
as: Fragment,
children: [
/* @__PURE__ */ jsx(
TabList,
{
className: (args) => cn(
"nextra-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain",
"mt-4 flex w-full gap-2 border-b border-beige-200 pb-px dark:border-neutral-800",
"focus-visible:hive-focus",
typeof className === "function" ? className(args) : className
),
children: items.map((item, index) => /* @__PURE__ */ jsx(
HeadlessTab,
{
disabled: isTabObjectItem(item) && item.disabled,
className: (args) => {
const { selected, disabled, hover, focus } = args;
return cn(
focus && "hive-focus ring-inset",
"cursor-pointer whitespace-nowrap",
"rounded-t p-2 font-medium leading-5 transition-colors",
"-mb-0.5 select-none border-b-2",
selected ? "border-current outline-none" : hover ? "border-beige-200 dark:border-neutral-800" : "border-transparent",
selected ? "text-green-900 dark:text-primary" : disabled ? "pointer-events-none text-beige-400 dark:text-neutral-600" : hover ? "text-black dark:text-white" : "text-beige-600 dark:text-beige-200",
typeof tabClassName === "function" ? tabClassName(args) : tabClassName
);
},
children: isTabObjectItem(item) ? item.label : item
},
index
))
}
),
/* @__PURE__ */ jsx(TabPanels, { ref: tabPanelsRef, children })
]
}
);
};
const Tab = ({
children,
// For SEO display all the Panel in the DOM and set `display: none;` for those that are not selected
unmount = false,
className,
...props
}) => {
return /* @__PURE__ */ jsx(
TabPanel,
{
...props,
unmount,
className: (args) => cn(
"mt-[1.25em] rounded",
args.focus && "hive-focus",
typeof className === "function" ? className(args) : className
),
children
}
);
};
function useActiveTabFromURL(tabPanelsRef, items, searchParamKey, setSelectedIndex, id) {
const hash = useHash();
const searchParams = useSearchParams();
const tabsInSearchParams = searchParams.getAll(searchParamKey).sort();
const tabIndexFromSearchParams = items.findIndex(
(_, index) => tabsInSearchParams.includes(getTabKey(items, index, id))
);
useIsomorphicLayoutEffect(() => {
const tabPanel = hash ? tabPanelsRef.current?.querySelector(`[role=tabpanel]:has([id="${hash}"])`) : null;
if (tabPanel) {
let index = 0;
for (const el of tabPanelsRef.current.children) {
if (el === tabPanel) {
setSelectedIndex(Number(index));
location.hash = "";
requestAnimationFrame(() => location.hash = `#${hash}`);
}
index++;
}
} else if (tabIndexFromSearchParams !== -1) {
setSelectedIndex(tabIndexFromSearchParams);
}
return function cleanUpTabFromSearchParams() {
const newSearchParams = new URLSearchParams(window.location.search);
newSearchParams.delete(searchParamKey);
window.history.replaceState(
null,
"",
`${window.location.pathname}?${newSearchParams.toString()}`
);
};
}, [hash, tabsInSearchParams.join(",")]);
return tabIndexFromSearchParams;
}
function useActiveTabFromStorage(storageKey, items, setSelectedIndex, ignoreLocalStorage, id) {
useIsomorphicLayoutEffect(() => {
if (!storageKey || ignoreLocalStorage) {
return;
}
const setSelectedTab = (key) => {
const index = items.findIndex((_, i) => getTabKey(items, i, id) === key);
if (index !== -1) {
setSelectedIndex(index);
}
};
function onStorageChange(event) {
if (event.key === storageKey) {
const value2 = event.newValue;
if (value2) {
setSelectedTab(value2);
}
}
}
const value = localStorage.getItem(storageKey);
if (value) {
setSelectedTab(value);
}
window.addEventListener("storage", onStorageChange);
return () => {
window.removeEventListener("storage", onStorageChange);
};
}, [storageKey]);
}
function getTabKey(items, index, prefix) {
const item = items[index];
const isObject = isTabObjectItem(item);
if (isObject && item.key) {
return item.key;
}
const label = isObject ? item.label : item;
const key = typeof label === "string" ? slugify(label) : `${prefix}-${index.toString()}`;
return key;
}
function slugify(label) {
return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
}
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
export {
Tab,
Tabs
};