diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx index 68c7c7a62c4d..8be4a0663f20 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx @@ -11,6 +11,7 @@ type TabProps = { title: string; Icon?: IconComponent; active?: boolean; + inDropdown?: boolean; className?: string; onClick?: () => void; disabled?: boolean; @@ -21,7 +22,7 @@ type TabProps = { const StyledTab = styled('button', { shouldForwardProp: (prop) => isPropValid(prop) && prop !== 'active', -})<{ active?: boolean; disabled?: boolean; to?: string }>` +})<{ active?: boolean; disabled?: boolean; to?: string; inDropdown?: boolean }>` align-items: center; border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; border-color: ${({ theme, active }) => @@ -43,8 +44,14 @@ const StyledTab = styled('button', { justify-content: center; margin-bottom: 0; padding: ${({ theme }) => theme.spacing(2) + ' 0'}; - pointer-events: ${({ disabled }) => (disabled ? 'none' : '')}; + pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')}; text-decoration: none; + + &:hover { + background: ${({ theme, inDropdown }) => + inDropdown ? theme.background.tertiary : 'transparent'}; + } + border-radius: ${({ theme }) => theme.border.radius.sm}; `; const StyledHover = styled.span` @@ -73,6 +80,7 @@ export const Tab = ({ title, Icon, active = false, + inDropdown = false, onClick, className, disabled, @@ -91,6 +99,7 @@ export const Tab = ({ active={active} className={className} disabled={disabled} + inDropdown={inDropdown} data-testid={'tab-' + id} as={to ? Link : 'button'} to={to} diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index ec3c6d965de7..2074a3a5629a 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -2,12 +2,11 @@ import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab/components/TabList import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { TabListScope } from '@/ui/layout/tab/scopes/TabListScope'; import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard'; -import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import styled from '@emotion/styled'; -import * as React from 'react'; -import { useEffect } from 'react'; -import { IconComponent } from 'twenty-ui'; +import { useEffect, useState, useCallback } from 'react'; import { Tab } from './Tab'; +import { MoreTabsDropdown } from './TabMoreDropdown'; +import { IconComponent, IconChevronDown } from 'twenty-ui'; export type SingleTabProps = { title: string; @@ -38,17 +37,41 @@ const StyledContainer = styled.div` user-select: none; `; +const StyledDropdownContainer = styled.div` + display: flex; + flex-direction: column; + padding: ${({ theme }) => theme.spacing(1)}; +`; + export const TabList = ({ tabs, tabListInstanceId, - loading, + loading = false, behaveAsLinks = true, - isInRightDrawer, + isInRightDrawer = false, className, }: TabListProps) => { + const { activeTabId, setActiveTabId } = useTabList(tabListInstanceId); const visibleTabs = tabs.filter((tab) => !tab.hide); - const { activeTabId, setActiveTabId } = useTabList(tabListInstanceId); + const [maxVisibleTabs, setMaxVisibleTabs] = useState( + visibleTabs.length, + ); + + const containerRef = (node: HTMLDivElement) => { + const containerWidth = node.offsetWidth; + const firstTab = node.querySelector( + '.tab-item', + ) as HTMLElement; + if (!firstTab) return; + + const tabWidth = firstTab.offsetWidth + 16; // 16px := gap between tabs + const calculatedMaxVisible = Math.floor(containerWidth / tabWidth) - 1; // -1 to make space for the dropdown button + setMaxVisibleTabs(calculatedMaxVisible); + }; + + const truncatedTabs = visibleTabs.slice(0, maxVisibleTabs); + const remainingTabs = visibleTabs.slice(maxVisibleTabs); const initialActiveTabId = activeTabId || visibleTabs[0]?.id || ''; @@ -60,39 +83,65 @@ export const TabList = ({ return null; } + const handleTabClick = (tabId: string) => { + if (!behaveAsLinks) { + setActiveTabId(tabId); + } + }; + return ( tab.id)} /> - - - {visibleTabs.map((tab) => ( - { - if (!behaveAsLinks) { - setActiveTabId(tab.id); - } - }} - /> - ))} - - + + {truncatedTabs.map((tab) => ( + handleTabClick(tab.id)} + inDropdown={false} + // Add a class for measurement + className="tab-item" + /> + ))} + {remainingTabs.length > 0 && ( + + {remainingTabs.map((tab) => ( + handleTabClick(tab.id)} + inDropdown={true} + /> + ))} + + } + dropdownPlacement="bottom-start" + /> + )} + ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabMoreDropdown.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabMoreDropdown.tsx new file mode 100644 index 000000000000..a4af6cbcc46f --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabMoreDropdown.tsx @@ -0,0 +1,61 @@ +import { Button, IconComponent } from 'twenty-ui'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import styled from '@emotion/styled'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; + +type MoreTabsDropdownProps = { + id: string; + title: string; + Icon: IconComponent; + dropdownContent?: React.ReactNode; + dropdownPlacement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'; + onDropdownOpen?: () => void; + onDropdownClose?: () => void; +}; + +export const MoreTabsDropdown = ({ + id, + title, + Icon, + dropdownContent, + dropdownPlacement = 'bottom-start', + onDropdownOpen, + onDropdownClose, +}: MoreTabsDropdownProps) => { + if (!dropdownContent) { + return