-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(tabs): Add ellipsis for multiple tabs #4510
base: canary
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@nextui-org/tabs": major | ||
--- | ||
|
||
Added ellipsis to tabs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. include issue number at the end
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,6 +1,9 @@ | ||||||
import {ForwardedRef, ReactElement, useId} 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"; | ||||||
import {clsx, dataAttr, debounce} 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,8 +31,96 @@ const Tabs = forwardRef(function Tabs<T extends object>( | |||||
}); | ||||||
|
||||||
const layoutId = useId(); | ||||||
const [showOverflow, setShowOverflow] = useState(false); | ||||||
const [hiddenTabs, setHiddenTabs] = useState<Array<{key: string; title: string}>>([]); | ||||||
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 (!tabList) return; | ||||||
|
||||||
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, tabListProps.ref]); | ||||||
|
||||||
const scrollToTab = useCallback( | ||||||
(key: string) => { | ||||||
if (!tabList) return; | ||||||
|
||||||
const tabElement = tabList.querySelector(`[data-key="${key}"]`); | ||||||
|
||||||
if (!tabElement) return; | ||||||
|
||||||
tabElement.scrollIntoView({ | ||||||
behavior: "smooth", | ||||||
block: "nearest", | ||||||
inline: "center", | ||||||
}); | ||||||
}, | ||||||
[tabListProps.ref], | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix useCallback dependency The dependency array references - [tabListProps.ref],
+ [tabList], 📝 Committable suggestion
Suggested change
|
||||||
); | ||||||
|
||||||
const handleTabSelect = useCallback( | ||||||
(key: string) => { | ||||||
state.setSelectedKey(key); | ||||||
setIsDropdownOpen(false); | ||||||
|
||||||
scrollToTab(key); | ||||||
checkOverflow(); | ||||||
}, | ||||||
[state, scrollToTab, checkOverflow], | ||||||
); | ||||||
|
||||||
useEffect(() => { | ||||||
if (!tabList) return; | ||||||
|
||||||
tabList.style.overflowX = isDropdownOpen ? "hidden" : "auto"; | ||||||
}, [isDropdownOpen, tabListProps.ref]); | ||||||
|
||||||
useEffect(() => { | ||||||
const debouncedCheckOverflow = debounce(checkOverflow, 100); | ||||||
|
||||||
debouncedCheckOverflow(); | ||||||
|
||||||
window.addEventListener("resize", debouncedCheckOverflow); | ||||||
|
||||||
return () => { | ||||||
window.removeEventListener("resize", debouncedCheckOverflow); | ||||||
}; | ||||||
}, [checkOverflow]); | ||||||
|
||||||
const tabsProps = { | ||||||
state, | ||||||
|
@@ -49,23 +140,54 @@ const Tabs = forwardRef(function Tabs<T extends object>( | |||||
|
||||||
const renderTabs = ( | ||||||
<> | ||||||
<div {...getBaseProps()}> | ||||||
<Component {...getTabListProps()}> | ||||||
<div | ||||||
{...getBaseProps()} | ||||||
className={clsx("relative flex w-full items-center", getBaseProps().className)} | ||||||
> | ||||||
<Component | ||||||
{...tabListProps} | ||||||
className={clsx( | ||||||
"relative flex overflow-x-auto scrollbar-hide", | ||||||
showOverflow ? "w-[calc(100%-32px)]" : "w-full", | ||||||
tabListProps.className, | ||||||
)} | ||||||
data-has-overflow={dataAttr(showOverflow)} | ||||||
onScroll={checkOverflow} | ||||||
> | ||||||
{layoutGroupEnabled ? <LayoutGroup id={layoutId}>{tabs}</LayoutGroup> : tabs} | ||||||
</Component> | ||||||
{showOverflow && ( | ||||||
<Dropdown> | ||||||
<DropdownTrigger> | ||||||
<button | ||||||
aria-label="Show more tabs" | ||||||
className="flex-none flex items-center justify-center w-10 h-8 ml-1 hover:bg-default-100 rounded-small transition-colors" | ||||||
> | ||||||
<EllipsisIcon className="w-5 h-5" /> | ||||||
<span className="sr-only">More tabs</span> | ||||||
</button> | ||||||
</DropdownTrigger> | ||||||
<DropdownMenu | ||||||
aria-label="Hidden tabs" | ||||||
onAction={(key) => handleTabSelect(key as string)} | ||||||
> | ||||||
{hiddenTabs.map((tab) => ( | ||||||
<DropdownItem key={tab.key}>{tab.title}</DropdownItem> | ||||||
))} | ||||||
</DropdownMenu> | ||||||
</Dropdown> | ||||||
)} | ||||||
</div> | ||||||
{[...state.collection].map((item) => { | ||||||
return ( | ||||||
<TabPanel | ||||||
key={item.key} | ||||||
classNames={values.classNames} | ||||||
destroyInactiveTabPanel={destroyInactiveTabPanel} | ||||||
slots={values.slots} | ||||||
state={values.state} | ||||||
tabKey={item.key} | ||||||
/> | ||||||
); | ||||||
})} | ||||||
{[...state.collection].map((item) => ( | ||||||
<TabPanel | ||||||
key={item.key} | ||||||
classNames={values.classNames} | ||||||
destroyInactiveTabPanel={destroyInactiveTabPanel} | ||||||
slots={values.slots} | ||||||
state={values.state} | ||||||
tabKey={item.key} | ||||||
/> | ||||||
))} | ||||||
</> | ||||||
); | ||||||
|
||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
change to
patch
.