Skip to content
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

Open
wants to merge 27 commits into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
98a8348
feat: added ellipsis dropdown in tabs
deepansh946 Jan 7, 2025
1790c35
fix: tests
deepansh946 Jan 7, 2025
0ed340e
chore: added changeset & some refactor
deepansh946 Jan 7, 2025
f3683d6
fix: lock file
deepansh946 Jan 7, 2025
acd7057
chore(changeset): update package name
wingkwong Jan 18, 2025
6823cf4
chore: added multiple tabs story & updated changeset
deepansh946 Jan 23, 2025
2df0fdd
fix: useEffect deps & some UI updates
deepansh946 Jan 23, 2025
b71f011
fix: tests
deepansh946 Jan 23, 2025
07fdf26
fix: merge conflicts
deepansh946 Jan 23, 2025
0bfa233
feat: added ellipsis dropdown in tabs
deepansh946 Jan 7, 2025
11b2b9a
fix: tests
deepansh946 Jan 7, 2025
8b27006
chore: added changeset & some refactor
deepansh946 Jan 7, 2025
3e1fc95
fix: lock file
deepansh946 Jan 7, 2025
b6e3349
chore(changeset): update package name
wingkwong Jan 18, 2025
2ba85c7
chore: added multiple tabs story & updated changeset
deepansh946 Jan 23, 2025
31ebb0d
fix: useEffect deps & some UI updates
deepansh946 Jan 23, 2025
1324f40
fix: tests
deepansh946 Jan 23, 2025
1724aad
fix: merge conflicts
deepansh946 Jan 23, 2025
367ea76
Merge branch 'feat/dropdown-in-tabs' of github.com:deepansh946/nextui…
deepansh946 Jan 30, 2025
bb89a9c
Merge branch 'canary' into feat/dropdown-in-tabs
deepansh946 Jan 30, 2025
2adbc8b
refactor: tabs & tests code
deepansh946 Jan 30, 2025
6b7e4ff
fix: tab scroll & btn element
deepansh946 Feb 10, 2025
b519b2a
refactor: added documentation & tab list props
deepansh946 Feb 10, 2025
b56e81e
fix: story name & example
deepansh946 Feb 10, 2025
97553ca
refactor: removed extra classes
deepansh946 Feb 10, 2025
2201762
feat: add scrollable dropdown menu & story with many tabs
deepansh946 Feb 10, 2025
26cdc42
Merge branch 'canary' into feat/dropdown-in-tabs
deepansh946 Feb 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curly-snails-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nextui-org/tabs": major
---

Added ellipsis to tabs
1 change: 1 addition & 0 deletions packages/components/tabs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
},
"dependencies": {
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/dropdown": "workspace:*",
"@nextui-org/react-utils": "workspace:*",
"@nextui-org/aria-utils": "workspace:*",
"@nextui-org/framer-utils": "workspace:*",
Expand Down
152 changes: 137 additions & 15 deletions packages/components/tabs/src/tabs.tsx
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";
Expand Down Expand Up @@ -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],
);

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,
Expand All @@ -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}
/>
))}
</>
);

Expand Down
Loading