UNPKG

@theguild/components

Version:
226 lines (225 loc) • 7.24 kB
"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 };