import React, { useContext, useEffect, useId, useRef, useState, type ReactNode } from 'react' import classnames from 'classnames' import { TabList as RACTabList, TabListStateContext, type TabListProps as RACTabListProps, } from 'react-aria-components' import { Icon } from '~components/Icon' import { isRTL as isRTLCheck } from '~components/utils/isRTL' import { SCROLL_AMOUNT } from '../../constants' import styles from './TabList.module.css' export type TabListProps = { /** * Accessible name for the set of tabs */ 'aria-label': string /** * Removes the built in padding */ 'noPadding'?: boolean 'children': ReactNode 'data-testid'?: string } & RACTabListProps /** * Wrapper for the tabs themselves */ export const TabList = (props: TabListProps): JSX.Element => { const { 'aria-label': ariaLabel, noPadding = false, children, className, 'data-testid': testId, ...restProps } = props const [isDocumentReady, setIsDocumentReady] = useState(false) const [leftArrowEnabled, setLeftArrowEnabled] = useState(false) const [rightArrowEnabled, setRightArrowEnabled] = useState(false) const tabListRef = useRef(null) const tabListId = useId() const [isRTL, setIsRTL] = useState(false) const [containerElement, setContainerElement] = useState() const tabListContext = useContext(TabListStateContext) const selectedKey = tabListContext?.selectedKey const prevSelectedKey = useRef(selectedKey) useEffect(() => { if (!isDocumentReady) { setIsDocumentReady(true) return } const container = document.getElementById(tabListId) setContainerElement(container) setIsRTL(container ? isRTLCheck(container) : false) }, [isDocumentReady, tabListId]) useEffect(() => { if (!isDocumentReady) { return } const tabs = containerElement?.querySelectorAll('[data-kz-tab]') if (!tabs || tabs.length === 0) { return } const firstTabObserver = new IntersectionObserver( (entries) => { if (!entries[0].isIntersecting) { setLeftArrowEnabled(true) return } setLeftArrowEnabled(false) }, { threshold: 0.8, root: containerElement, }, ) firstTabObserver.observe(isRTL ? tabs[tabs.length - 1] : tabs[0]) const lastTabObserver = new IntersectionObserver( (entries) => { if (!entries[0].isIntersecting) { setRightArrowEnabled(true) return } setRightArrowEnabled(false) }, { threshold: 0.8, root: containerElement, }, ) lastTabObserver.observe(isRTL ? tabs[0] : tabs[tabs.length - 1]) return () => { firstTabObserver.disconnect() lastTabObserver.disconnect() } }, [isDocumentReady, containerElement, isRTL, tabListContext?.collection.size]) useEffect(() => { if (!isDocumentReady) { return } // Only scroll the selected tab into view when the selection actually changes // (i.e. user interaction). Skipping the no-op runs avoids scrolling the page // on mount when the Tabs sit below the fold. if (prevSelectedKey.current === selectedKey) { return } prevSelectedKey.current = selectedKey containerElement ?.querySelector('[role="tab"][data-selected=true]') ?.scrollIntoView({ block: 'nearest', inline: 'center' }) }, [selectedKey, containerElement, isDocumentReady]) const handleArrowPress = (direction: 'left' | 'right'): void => { if (tabListRef.current) { const tabListScrollPos = tabListRef.current.scrollLeft const newSpot = direction === 'left' ? tabListScrollPos - SCROLL_AMOUNT : tabListScrollPos + SCROLL_AMOUNT tabListRef.current.scrollLeft = newSpot } } return (
{leftArrowEnabled && ( // making a conscious decision to use
over
) }