310 lines
8.3 KiB
TypeScript
310 lines
8.3 KiB
TypeScript
"use client"
|
|
|
|
import React, { Children, useCallback, useEffect, useMemo, useRef } from "react"
|
|
import {
|
|
Badge,
|
|
BaseTabType,
|
|
CodeBlockProps,
|
|
CodeBlockStyle,
|
|
useColorMode,
|
|
useTabs,
|
|
} from "../.."
|
|
import clsx from "clsx"
|
|
import { CodeBlockActions, CodeBlockActionsProps } from "../CodeBlock/Actions"
|
|
import { CodeBlockHeaderWrapper } from "../CodeBlock/Header/Wrapper"
|
|
|
|
type CodeTab = BaseTabType & {
|
|
codeProps: CodeBlockProps
|
|
codeBlock: React.ReactNode
|
|
children?: React.ReactNode
|
|
}
|
|
|
|
type CodeTabProps = {
|
|
children: React.ReactNode
|
|
className?: string
|
|
group?: string
|
|
blockStyle?: CodeBlockStyle
|
|
}
|
|
|
|
export const CodeTabs = ({
|
|
children,
|
|
className,
|
|
group = "client",
|
|
blockStyle = "loud",
|
|
}: CodeTabProps) => {
|
|
const { colorMode } = useColorMode()
|
|
|
|
const isCodeBlock = (
|
|
node: React.ReactNode
|
|
): node is
|
|
| React.ReactElement<unknown, string | React.JSXElementConstructor<any>>
|
|
| React.ReactPortal => {
|
|
if (!React.isValidElement(node)) {
|
|
return false
|
|
}
|
|
|
|
if (node.type === "pre") {
|
|
return true
|
|
}
|
|
|
|
const typedProps = node.props as Record<string, unknown>
|
|
|
|
return "source" in typedProps
|
|
}
|
|
|
|
const getCodeBlockProps = (
|
|
codeBlock: React.ReactElement<
|
|
unknown,
|
|
string | React.JSXElementConstructor<any>
|
|
>
|
|
): CodeBlockProps | undefined => {
|
|
if (typeof codeBlock.props !== "object" || !codeBlock.props) {
|
|
return undefined
|
|
}
|
|
|
|
if ("source" in codeBlock.props) {
|
|
return codeBlock.props as CodeBlockProps
|
|
}
|
|
|
|
if (
|
|
"children" in codeBlock.props &&
|
|
typeof codeBlock.props.children === "object" &&
|
|
codeBlock.props.children
|
|
) {
|
|
return getCodeBlockProps(
|
|
codeBlock.props.children as React.ReactElement<
|
|
unknown,
|
|
string | React.JSXElementConstructor<any>
|
|
>
|
|
)
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
const tabs: CodeTab[] = useMemo(() => {
|
|
const tempTabs: CodeTab[] = []
|
|
Children.forEach(children, (child) => {
|
|
if (!React.isValidElement(child)) {
|
|
return
|
|
}
|
|
const typedChildProps = child.props as CodeTab
|
|
if (
|
|
!React.isValidElement(child) ||
|
|
!typedChildProps.label ||
|
|
!typedChildProps.value ||
|
|
!React.isValidElement(typedChildProps.children)
|
|
) {
|
|
return
|
|
}
|
|
|
|
const codeBlock: React.ReactNode = isCodeBlock(typedChildProps.children)
|
|
? typedChildProps.children
|
|
: undefined
|
|
|
|
if (!codeBlock) {
|
|
return
|
|
}
|
|
|
|
let codeBlockProps = codeBlock.props as CodeBlockProps
|
|
const showBadge = !codeBlockProps.title
|
|
const originalBadgeLabel = codeBlockProps.badgeLabel
|
|
|
|
const commonProps = {
|
|
badgeLabel: showBadge ? undefined : originalBadgeLabel,
|
|
hasTabs: true,
|
|
className: clsx("!my-0", codeBlockProps.className),
|
|
}
|
|
|
|
if (
|
|
typeof codeBlock.type !== "string" &&
|
|
(("name" in codeBlock.type && codeBlock.type.name === "CodeBlock") ||
|
|
"source" in codeBlockProps)
|
|
) {
|
|
codeBlockProps = {
|
|
...codeBlockProps,
|
|
...commonProps,
|
|
}
|
|
}
|
|
|
|
const modifiedProps: CodeBlockProps = {
|
|
...(getCodeBlockProps(codeBlock) || {
|
|
source: "",
|
|
}),
|
|
...commonProps,
|
|
}
|
|
|
|
tempTabs.push({
|
|
label: typedChildProps.label,
|
|
value: typedChildProps.value,
|
|
codeProps: {
|
|
...modifiedProps,
|
|
badgeLabel: !showBadge ? undefined : originalBadgeLabel,
|
|
},
|
|
codeBlock: {
|
|
...codeBlock,
|
|
props: {
|
|
...codeBlockProps,
|
|
children: {
|
|
...(typeof codeBlockProps.children === "object"
|
|
? codeBlockProps.children
|
|
: {}),
|
|
props: modifiedProps,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
return tempTabs
|
|
}, [children])
|
|
|
|
const { selectedTab, changeSelectedTab } = useTabs<CodeTab>({
|
|
tabs,
|
|
group,
|
|
})
|
|
|
|
const tabRefs: (HTMLButtonElement | null)[] = useMemo(() => [], [])
|
|
const codeTabSelectorRef = useRef<HTMLSpanElement | null>(null)
|
|
const codeTabsWrapperRef = useRef<HTMLDivElement | null>(null)
|
|
|
|
const bgColor = useMemo(
|
|
() =>
|
|
clsx(
|
|
blockStyle === "loud" && "bg-medusa-contrast-bg-base",
|
|
blockStyle === "subtle" && [
|
|
colorMode === "light" && "bg-medusa-bg-component",
|
|
colorMode === "dark" && "bg-medusa-code-bg-header",
|
|
]
|
|
),
|
|
[blockStyle, colorMode]
|
|
)
|
|
|
|
const boxShadow = useMemo(
|
|
() =>
|
|
clsx(
|
|
blockStyle === "loud" &&
|
|
"shadow-elevation-code-block dark:shadow-elevation-code-block-dark",
|
|
blockStyle === "subtle" && "shadow-none"
|
|
),
|
|
[blockStyle]
|
|
)
|
|
|
|
const changeTabSelectorCoordinates = useCallback(
|
|
(selectedTabElm: HTMLElement) => {
|
|
if (!codeTabSelectorRef?.current || !codeTabsWrapperRef?.current) {
|
|
return
|
|
}
|
|
const selectedTabsCoordinates = selectedTabElm.getBoundingClientRect()
|
|
const tabsWrapperCoordinates =
|
|
codeTabsWrapperRef.current.getBoundingClientRect()
|
|
codeTabSelectorRef.current.style.left = `${
|
|
selectedTabsCoordinates.left - tabsWrapperCoordinates.left
|
|
}px`
|
|
codeTabSelectorRef.current.style.width = `${selectedTabsCoordinates.width}px`
|
|
if (blockStyle !== "loud") {
|
|
codeTabSelectorRef.current.style.height = `${selectedTabsCoordinates.height}px`
|
|
}
|
|
},
|
|
[blockStyle]
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (codeTabSelectorRef?.current && tabRefs.length) {
|
|
const selectedTabElm = tabRefs.find(
|
|
(tab) => tab?.getAttribute("aria-selected") === "true"
|
|
)
|
|
if (selectedTabElm) {
|
|
changeTabSelectorCoordinates(
|
|
selectedTabElm.parentElement || selectedTabElm
|
|
)
|
|
}
|
|
}
|
|
}, [codeTabSelectorRef, tabRefs, changeTabSelectorCoordinates, selectedTab])
|
|
|
|
const actionsProps: CodeBlockActionsProps | undefined = useMemo(() => {
|
|
if (!selectedTab) {
|
|
return
|
|
}
|
|
|
|
return {
|
|
source: selectedTab?.codeProps.source,
|
|
blockStyle,
|
|
noReport: selectedTab?.codeProps.noReport,
|
|
noCopy: selectedTab?.codeProps.noCopy,
|
|
inInnerCode: true,
|
|
showGradientBg: false,
|
|
inHeader: true,
|
|
isCollapsed: false,
|
|
}
|
|
}, [selectedTab])
|
|
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
"my-docs_1 w-full max-w-full",
|
|
"rounded-docs_lg",
|
|
bgColor,
|
|
boxShadow,
|
|
className
|
|
)}
|
|
>
|
|
<CodeBlockHeaderWrapper blockStyle={blockStyle} ref={codeTabsWrapperRef}>
|
|
<span
|
|
className={clsx(
|
|
"xs:absolute xs:transition-all xs:duration-200 xs:ease-ease xs:bottom-0",
|
|
blockStyle === "loud" && "bg-medusa-contrast-fg-primary h-px",
|
|
blockStyle === "subtle" && [
|
|
colorMode === "light" &&
|
|
"xs:border-medusa-border-base xs:bg-medusa-bg-base",
|
|
colorMode === "dark" &&
|
|
"xs:border-medusa-code-border xs:bg-medusa-code-bg-base",
|
|
]
|
|
)}
|
|
ref={codeTabSelectorRef}
|
|
></span>
|
|
<div className="flex gap-docs_1 items-center">
|
|
{selectedTab?.codeProps.badgeLabel && (
|
|
<Badge
|
|
variant={selectedTab?.codeProps.badgeColor || "code"}
|
|
className="!font-base"
|
|
>
|
|
{selectedTab.codeProps.badgeLabel}
|
|
</Badge>
|
|
)}
|
|
<ul
|
|
className={clsx(
|
|
"!list-none flex gap-docs_0.75 items-center",
|
|
"p-0 mb-0"
|
|
)}
|
|
>
|
|
{Children.map(children, (child, index) => {
|
|
if (!React.isValidElement(child)) {
|
|
return <></>
|
|
}
|
|
|
|
return (
|
|
<child.type
|
|
{...(typeof child.props === "object" ? child.props : {})}
|
|
changeSelectedTab={changeSelectedTab}
|
|
pushRef={(tabButton: HTMLButtonElement | null) =>
|
|
tabRefs.push(tabButton)
|
|
}
|
|
blockStyle={blockStyle}
|
|
isSelected={
|
|
!selectedTab
|
|
? index === 0
|
|
: selectedTab.value === (child.props as CodeTab).value
|
|
}
|
|
/>
|
|
)
|
|
})}
|
|
</ul>
|
|
</div>
|
|
{actionsProps && <CodeBlockActions {...actionsProps} />}
|
|
</CodeBlockHeaderWrapper>
|
|
{selectedTab?.codeBlock}
|
|
</div>
|
|
)
|
|
}
|