import React, { createContext, CSSProperties, useContext, useEffect, useRef, useState, } from "react" import { cn } from "../../design-lib/utils" interface AnimatedTabContextProps { repositionHighlight: ( e: | React.MouseEvent | React.FocusEvent ) => void resetHighlight: () => void vertical: boolean muted: boolean highlightedTab: string | null addTabRef: (ref: HTMLDivElement | null) => void } const AnimatedTabContext = createContext({ repositionHighlight: () => {}, resetHighlight: () => {}, vertical: false, muted: false, highlightedTab: null, addTabRef: () => {}, }) interface AnimationTabProps { active?: boolean disabled?: boolean className?: string children: React.ReactNode } export const TabItem: React.FC< AnimationTabProps & React.HTMLProps > = ({ active, className, children, disabled, ...props }) => { const tabContext = useContext(AnimatedTabContext) if (!tabContext) { throw new Error("NavigationItem must be used within a AnimatedTab") } const tabRef = useRef(null) const { repositionHighlight, vertical, highlightedTab, addTabRef, muted } = tabContext const handleMouseOver = ( e: | React.MouseEvent | React.FocusEvent ) => { repositionHighlight(e) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { if (document.activeElement === tabRef.current) { tabRef.current?.click() } } } useEffect(() => { window.addEventListener("keydown", handleKeyDown as any) return () => { window.removeEventListener("keydown", handleKeyDown as any) } }, [tabRef.current]) useEffect(() => { addTabRef(tabRef.current) }, [tabRef.current]) return (
handleMouseOver(ev)} onFocus={(ev) => handleMouseOver(ev)} role="tab" tabIndex={0} aria-selected={active} aria-disabled={disabled} className={cn( "relative inline-block cursor-pointer text-sm transition-opacity duration-150 focus:outline-none data-[active=true]:opacity-100", vertical ? "px-6 py-3" : "p-4", { "opacity-disabled": muted }, { "pointer-events-none cursor-not-allowed opacity-disabled": disabled }, className )} ref={tabRef} {...props} data-active={highlightedTab === tabRef.current?.textContent} >
{children}
{active &&
}
) } interface AnimatedTabProps { vertical?: boolean muted?: boolean keepHighlighted?: boolean children: React.ReactNode className?: string } interface BoundingBox { width: number height: number left: number top: number } export const AnimatedTab: React.FC = ({ vertical = false, muted = false, keepHighlighted = false, children, className, }) => { const [tabBoundingBox, setTabBoundingBox] = useState(null) const [isHoveredFromNull, setIsHoveredFromNull] = useState(true) const highlightRef = useRef(null) const wrapperRef = useRef(null) const [wrapperBoundingBox, setWrapperBoundingBox] = useState(null) const [highlightedTab, setHighlightedTab] = useState(null) const repositionHighlight = ( e: | React.MouseEvent | React.FocusEvent ) => { const target = e.target as Element setTabBoundingBox(target.getBoundingClientRect() as BoundingBox) setWrapperBoundingBox( wrapperRef.current?.getBoundingClientRect() as BoundingBox ) setIsHoveredFromNull(!highlightedTab) setHighlightedTab(target.textContent) } const resetHighlight = () => { setTabBoundingBox(null) setIsHoveredFromNull(true) setHighlightedTab(null) } const highlightStyles: CSSProperties = {} const [tabRefs, setTabRefs] = useState<(HTMLDivElement | null)[]>([]) const addTabRef = (ref: HTMLDivElement | null) => { setTabRefs((prevRefs) => [...prevRefs, ref]) } const handleKeyDown = (e: React.KeyboardEvent) => { const index = tabRefs.indexOf(document.activeElement as HTMLDivElement) let nextIndex = -1 if (e.key === "ArrowDown" && vertical) { nextIndex = (index + 1) % tabRefs.length } else if (e.key === "ArrowUp" && vertical) { nextIndex = (index - 1 + tabRefs.length) % tabRefs.length } else if (e.key === "ArrowRight" && !vertical) { nextIndex = (index + 1) % tabRefs.length } else if (e.key === "ArrowLeft" && !vertical) { nextIndex = (index - 1 + tabRefs.length) % tabRefs.length } if (nextIndex !== -1 && tabRefs[nextIndex]) { tabRefs[nextIndex]?.focus() } } useEffect(() => { const handleWrapperKeyDown = (e: KeyboardEvent) => { if ( document.activeElement?.closest("[data-vertical]") === wrapperRef.current ) { handleKeyDown(e as unknown as React.KeyboardEvent) } } wrapperRef.current?.addEventListener("keydown", handleWrapperKeyDown) return () => { wrapperRef.current?.removeEventListener("keydown", handleWrapperKeyDown) } }, [tabRefs, wrapperRef.current]) if (tabBoundingBox && wrapperBoundingBox) { highlightStyles.transitionDuration = isHoveredFromNull ? "0ms" : "150ms" highlightStyles.opacity = highlightedTab ? 1 : 0 if (vertical) { highlightStyles.height = `${tabBoundingBox.height}px` highlightStyles.transform = `translateY(${ tabBoundingBox.top - wrapperBoundingBox.top }px)` } else { highlightStyles.width = `${tabBoundingBox.width}px` highlightStyles.transform = `translateX(${ tabBoundingBox.left - wrapperBoundingBox.left }px)` } } useEffect(() => { // Check if keepHighlighted is true if (keepHighlighted) { // Programmatically set the first tab as the highlighted tab const firstTab = wrapperRef.current?.querySelector( '[role="tab"]' ) as HTMLDivElement if (firstTab) { firstTab.focus() setHighlightedTab(firstTab.textContent) } } }, [keepHighlighted, wrapperRef.current]) return (
{} : () => resetHighlight()} data-vertical={vertical} >
{children}
) }