import React from 'react' import styles from './_desktop-router-tabs.module.scss' import { useDynamicTabWidths } from '../../../../hooks/useDynamicTabWidths/useDynamicTabWidths' const { useRef, useLayoutEffect } = React const activeClassName = 'active' const customClassName = styles.routertabsLink // Type definition for NavLink props interface NavLinkProps { className?: string to?: string [key: string]: any } // Helper type that allows us to safely work with JSX elements type ReactElementWithProps = React.ReactElement export type DesktopRouterTabsProps = { /** Multiple children. Typically in the form of NavLink components */ children: React.ReactNode[] /** Determines the style of the tabs. If true, the subtabs style will be applied. */ subtabs?: boolean /** Current path from the router */ currentPath: string /** Refresh router tab animation */ refreshRouterTabs?: boolean /** Optional prop to add a test id to the DesktopRouterTabs for QA testing */ qaTestId?: string } export default function DesktopRouterTabs({ children, subtabs = false, currentPath, refreshRouterTabs, qaTestId, }: DesktopRouterTabsProps) { const activeBarRef = useRef(null) const bottomBarRef = useRef(null) const navlinkContainerRef = useRef(null) const previousAnimationRef = useRef(null) /** * Dynamic tab width measurement hook. * Monitors tab content changes and triggers realignment when widths change. * See useDynamicTabWidths for implementation details. */ const { measurementKey: tabsMeasurementKey } = useDynamicTabWidths({ containerRef: subtabs ? bottomBarRef : navlinkContainerRef, tabClassName: customClassName, enabled: true, }) // Count only valid React elements const numberOfChildrenNodes = React.Children.toArray(children).filter( (child) => React.isValidElement(child), ).length useLayoutEffect(() => { // every rerender, check to see if we need to update the location of the active bar underneath the NavLink const childrenNodes = Array.from( (subtabs ? bottomBarRef : navlinkContainerRef ).current?.getElementsByClassName(customClassName) || [], ) const active = childrenNodes.find((ref) => ref?.classList.contains(activeClassName), ) function animateElement() { if (active && activeBarRef.current && bottomBarRef.current) { const { left: destinationLeft, width: destinationWidth } = active.getBoundingClientRect() const { left: firstBarLeft, width: firstBarWidth } = activeBarRef.current.getBoundingClientRect() const { left: bottomBarLeft } = bottomBarRef.current.getBoundingClientRect() activeBarRef.current.style.transition = '' activeBarRef.current.style.left = `${destinationLeft - bottomBarLeft}px` activeBarRef.current.style.width = `${destinationWidth}px` const { left: lastBarLeft, width: lastBarWidth } = activeBarRef.current.getBoundingClientRect() const deltaX = firstBarLeft - lastBarLeft const deltaW = firstBarWidth / lastBarWidth previousAnimationRef.current = activeBarRef.current.animate( [ { transform: `translateX(${deltaX}px) scaleX(${deltaW})` }, { transform: 'none' }, ], { duration: 250, easing: 'ease-in-out', }, ) } } if (childrenNodes.length && activeBarRef.current) { active?.classList?.add(styles.routertabsLinkActive) if (active) { if ( previousAnimationRef.current && previousAnimationRef.current.playState !== 'finished' ) { previousAnimationRef.current.onfinish = animateElement } else { animateElement() } } } }, [ currentPath, subtabs, numberOfChildrenNodes, refreshRouterTabs, tabsMeasurementKey, // Re-align active bar when tab measurements change activeBarRef, bottomBarRef, navlinkContainerRef, previousAnimationRef, ]) return (
{!subtabs ? (
) : null}
) }