import * as React from "react"; import classNames from "classnames"; import { AriaTabListProps, useTab, useTabList, useTabPanel } from "react-aria"; import { Node, TabListState, useTabListState } from "react-stately"; import { animated, easings, useSpring } from "@react-spring/web"; import { Justification, Stack } from "../Stack/Stack"; import { useResolvedColorToken } from "../ThemeProvider/ThemeProvider"; import styles from "./Tabs.module.css"; const INDICATOR_SPRING_CONFIG = { tension: 200, friction: 20, clamp: true, easing: easings.easeOutQuad, }; export interface TabsProps extends Omit, "onSelectionChange"> { justify?: Justification; className?: string; panelClassName?: string; onSelect?: (itemKey: string) => void; } export function Tabs(props: TabsProps) { const { justify = "start", className, panelClassName, onSelect, ...stateProps } = props; const innerRef = React.useRef(null); const state = useTabListState({ ...stateProps, onSelectionChange(key) { // React.Key can be a number, but we're restricting that to only strings // for simplicity. onSelect?.(key as string); }, }); const { tabListProps } = useTabList(stateProps, state, innerRef); return (
{[...state.collection].map((item) => ( ))}
); } function TabIndicator(props: { state: TabListState; containerRef: React.RefObject; }) { const { state, containerRef } = props; const key = state.selectedKey; const isInitial = React.useRef(true); const [indicatorPosition, setIndicatorPosition] = React.useState({ x: 0, y: 0, endX: 0, endY: 0, }); React.useEffect(() => { const container = containerRef.current; if (container == null) return; const element = container.querySelector(`[data-key="${key}"]`); if (element == null) return; setIndicatorPosition({ x: element.offsetLeft, y: element.offsetTop, endX: element.offsetLeft + element.offsetWidth, endY: element.offsetTop + element.offsetHeight, }); }, [containerRef, key]); const [{ x, y, width, height }] = useSpring( () => ({ x: indicatorPosition.x, y: indicatorPosition.y, width: indicatorPosition.endX - indicatorPosition.x, height: indicatorPosition.endY - indicatorPosition.y, immediate: isInitial.current, config: INDICATOR_SPRING_CONFIG, onRest: () => (isInitial.current = false), }), [indicatorPosition], ); return ; } interface TabProps { item: Node; state: TabListState; } function Tab({ item, state }: TabProps) { let { key, rendered } = item; let ref = React.useRef(null); let { tabProps, isDisabled, isSelected } = useTab({ key }, state, ref); const resolvedColor = useResolvedColorToken( isSelected ? "ACCENT_FOREGROUND" : isDisabled ? "TEXT_SECONDARY" : "INTERACTIVE_NORMAL", ).rgba; const [{ color }] = useSpring( () => ({ color: resolvedColor, config: INDICATOR_SPRING_CONFIG, delay: 80, }), [resolvedColor], ); return ( {rendered} ); } interface TabPanelProps { state: TabListState; className?: string; } function TabPanel({ state, className }: TabPanelProps) { let panelRef = React.useRef(null); let { tabPanelProps } = useTabPanel({}, state, panelRef); return (
{state.selectedItem?.props.children}
); }