From 98a8348509a342b7d1a2fe6a3488f1cd0cd56fea Mon Sep 17 00:00:00 2001 From: Deepansh Bhargava Date: Tue, 7 Jan 2025 18:05:05 +0530 Subject: [PATCH 1/5] feat: added ellipsis dropdown in tabs --- packages/components/tabs/package.json | 1 + packages/components/tabs/src/tabs.tsx | 141 +++++++++++++++++++++++--- 2 files changed, 127 insertions(+), 15 deletions(-) diff --git a/packages/components/tabs/package.json b/packages/components/tabs/package.json index d591602dc4..d311a4eab9 100644 --- a/packages/components/tabs/package.json +++ b/packages/components/tabs/package.json @@ -66,6 +66,7 @@ "@nextui-org/test-utils": "workspace:*", "@nextui-org/button": "workspace:*", "@nextui-org/shared-icons": "workspace:*", + "@nextui-org/dropdown": "workspace:*", "clean-package": "2.2.0", "react": "^18.0.0", "react-dom": "^18.0.0" diff --git a/packages/components/tabs/src/tabs.tsx b/packages/components/tabs/src/tabs.tsx index ead0fae5fe..df41e4e97e 100644 --- a/packages/components/tabs/src/tabs.tsx +++ b/packages/components/tabs/src/tabs.tsx @@ -1,6 +1,9 @@ -import {ForwardedRef, ReactElement, useId} from "react"; +import {ForwardedRef, ReactElement, useId, useRef, useState, useEffect, useCallback} from "react"; import {LayoutGroup} from "framer-motion"; import {forwardRef} from "@nextui-org/system"; +import {EllipsisIcon} from "@nextui-org/shared-icons"; +import {clsx, dataAttr} from "@nextui-org/shared-utils"; +import {Dropdown, DropdownTrigger, DropdownMenu, DropdownItem} from "@nextui-org/dropdown"; import {UseTabsProps, useTabs} from "./use-tabs"; import Tab from "./tab"; @@ -28,9 +31,89 @@ const Tabs = forwardRef(function Tabs( }); const layoutId = useId(); + const tabListRef = useRef(null); + const [showOverflow, setShowOverflow] = useState(false); + const [hiddenTabs, setHiddenTabs] = useState>([]); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); const layoutGroupEnabled = !props.disableAnimation && !props.disableCursorAnimation; + const checkOverflow = useCallback(() => { + if (!tabListRef.current) return; + + const tabList = tabListRef.current; + const isOverflowing = tabList.scrollWidth > tabList.clientWidth; + + setShowOverflow(isOverflowing); + + if (!isOverflowing) { + setHiddenTabs([]); + + return; + } + + const tabs = [...state.collection]; + const hiddenTabsList: Array<{key: string; title: string}> = []; + const {left: containerLeft, right: containerRight} = tabList.getBoundingClientRect(); + + tabs.forEach((item) => { + const tabElement = tabList.querySelector(`[data-key="${item.key}"]`); + + if (!tabElement) return; + + const {left: tabLeft, right: tabRight} = tabElement.getBoundingClientRect(); + const isHidden = tabRight > containerRight || tabLeft < containerLeft; + + if (isHidden) { + hiddenTabsList.push({ + key: String(item.key), + title: item.textValue || "", + }); + } + }); + + setHiddenTabs(hiddenTabsList); + }, [state.collection]); + + const scrollToTab = useCallback((key: string) => { + if (!tabListRef.current) return; + + const tabElement = tabListRef.current.querySelector(`[data-key="${key}"]`); + + if (!tabElement) return; + + tabElement.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); + }, []); + + const handleTabSelect = useCallback( + (key: string) => { + state.setSelectedKey(key); + setIsDropdownOpen(false); + + scrollToTab(key); + checkOverflow(); + }, + [state, scrollToTab, checkOverflow], + ); + + useEffect(() => { + if (!tabListRef.current) return; + + tabListRef.current.style.overflowX = isDropdownOpen ? "hidden" : "auto"; + }, [isDropdownOpen]); + + useEffect(() => { + checkOverflow(); + + window.addEventListener("resize", checkOverflow); + + return () => window.removeEventListener("resize", checkOverflow); + }, [checkOverflow]); + const tabsProps = { state, listRef: values.listRef, @@ -49,23 +132,51 @@ const Tabs = forwardRef(function Tabs( const renderTabs = ( <> -
- +
+ {layoutGroupEnabled ? {tabs} : tabs} + {showOverflow && ( + + + + + handleTabSelect(key as string)} + > + {hiddenTabs.map((tab) => ( + {tab.title} + ))} + + + )}
- {[...state.collection].map((item) => { - return ( - - ); - })} + {[...state.collection].map((item) => ( + + ))} ); From 1790c35c1bfaf108a3e292a0e542d0b272eea1f4 Mon Sep 17 00:00:00 2001 From: Deepansh Bhargava Date: Tue, 7 Jan 2025 19:53:48 +0530 Subject: [PATCH 2/5] fix: tests --- packages/components/tabs/src/tabs.tsx | 57 +++++++++++++++------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/packages/components/tabs/src/tabs.tsx b/packages/components/tabs/src/tabs.tsx index df41e4e97e..95b6c01501 100644 --- a/packages/components/tabs/src/tabs.tsx +++ b/packages/components/tabs/src/tabs.tsx @@ -1,4 +1,4 @@ -import {ForwardedRef, ReactElement, useId, useRef, useState, useEffect, useCallback} from "react"; +import {ForwardedRef, ReactElement, useId, useState, useEffect, useCallback} from "react"; import {LayoutGroup} from "framer-motion"; import {forwardRef} from "@nextui-org/system"; import {EllipsisIcon} from "@nextui-org/shared-icons"; @@ -31,17 +31,18 @@ const Tabs = forwardRef(function Tabs( }); const layoutId = useId(); - const tabListRef = useRef(null); const [showOverflow, setShowOverflow] = useState(false); const [hiddenTabs, setHiddenTabs] = useState>([]); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const layoutGroupEnabled = !props.disableAnimation && !props.disableCursorAnimation; + const tabListProps = getTabListProps(); + const tabList = + tabListProps.ref && "current" in tabListProps.ref ? tabListProps.ref.current : null; const checkOverflow = useCallback(() => { - if (!tabListRef.current) return; + if (!tabList) return; - const tabList = tabListRef.current; const isOverflowing = tabList.scrollWidth > tabList.clientWidth; setShowOverflow(isOverflowing); @@ -73,21 +74,24 @@ const Tabs = forwardRef(function Tabs( }); setHiddenTabs(hiddenTabsList); - }, [state.collection]); + }, [state.collection, tabListProps.ref]); - const scrollToTab = useCallback((key: string) => { - if (!tabListRef.current) return; + const scrollToTab = useCallback( + (key: string) => { + if (!tabList) return; - const tabElement = tabListRef.current.querySelector(`[data-key="${key}"]`); + const tabElement = tabList.querySelector(`[data-key="${key}"]`); - if (!tabElement) return; + if (!tabElement) return; - tabElement.scrollIntoView({ - behavior: "smooth", - block: "nearest", - inline: "center", - }); - }, []); + tabElement.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); + }, + [tabListProps.ref], + ); const handleTabSelect = useCallback( (key: string) => { @@ -101,10 +105,10 @@ const Tabs = forwardRef(function Tabs( ); useEffect(() => { - if (!tabListRef.current) return; + if (!tabList) return; - tabListRef.current.style.overflowX = isDropdownOpen ? "hidden" : "auto"; - }, [isDropdownOpen]); + tabList.style.overflowX = isDropdownOpen ? "hidden" : "auto"; + }, [isDropdownOpen, tabListProps.ref]); useEffect(() => { checkOverflow(); @@ -132,13 +136,16 @@ const Tabs = forwardRef(function Tabs( const renderTabs = ( <> -
+
( {layoutGroupEnabled ? {tabs} : tabs} {showOverflow && ( - +