-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Implemented more button in TabList #10096
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
Changes from 7 commits
e7f038f
ce22054
ef8d0d4
7dc6617
20a722a
59c5f78
5df6e6e
45e2fb6
ebfdb20
1e2132c
3c16fdd
dcaa4ea
fac2868
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 |
---|---|---|
|
@@ -2,12 +2,14 @@ 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 { Tab } from './Tab'; | ||
import { MoreTabsDropdown } from './TabMoreDropdown'; | ||
import { IconComponent, IconChevronDown } from 'twenty-ui'; | ||
import { TAB_CONSTANTS } from '../constants/TabConstants'; | ||
|
||
const MAX_VISIBLE_TABS = TAB_CONSTANTS.MAX_VISIBLE_TABS; | ||
|
||
export type SingleTabProps = { | ||
title: string; | ||
|
@@ -38,17 +40,24 @@ 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, | ||
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. same as above comment |
||
className, | ||
}: TabListProps) => { | ||
const visibleTabs = tabs.filter((tab) => !tab.hide); | ||
|
||
const { activeTabId, setActiveTabId } = useTabList(tabListInstanceId); | ||
const visibleTabs = tabs.filter((tab) => !tab.hide); | ||
const truncatedTabs = visibleTabs.slice(0, MAX_VISIBLE_TABS); | ||
const remainingTabs = visibleTabs.slice(MAX_VISIBLE_TABS, visibleTabs.length); | ||
|
||
const initialActiveTabId = activeTabId || visibleTabs[0]?.id || ''; | ||
|
||
|
@@ -60,39 +69,63 @@ export const TabList = ({ | |
return null; | ||
} | ||
|
||
const handleTabClick = (tabId: string) => { | ||
if (!behaveAsLinks) { | ||
setActiveTabId(tabId); | ||
} | ||
}; | ||
|
||
return ( | ||
<TabListScope tabListScopeId={tabListInstanceId}> | ||
<TabListFromUrlOptionalEffect | ||
isInRightDrawer={!!isInRightDrawer} | ||
isInRightDrawer={isInRightDrawer} | ||
componentInstanceId={tabListInstanceId} | ||
tabListIds={tabs.map((tab) => tab.id)} | ||
/> | ||
<ScrollWrapper | ||
defaultEnableYScroll={false} | ||
contextProviderName="tabList" | ||
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. nice! if we are removing this contextProvider from ScrollWrapper may be check if its being used else where, if not remove the context from scrollwrapper contexts |
||
componentInstanceId={`scroll-wrapper-tab-list-${tabListInstanceId}`} | ||
> | ||
<StyledContainer className={className}> | ||
{visibleTabs.map((tab) => ( | ||
<Tab | ||
id={tab.id} | ||
key={tab.id} | ||
title={tab.title} | ||
Icon={tab.Icon} | ||
logo={tab.logo} | ||
active={tab.id === activeTabId} | ||
disabled={tab.disabled ?? loading} | ||
pill={tab.pill} | ||
to={behaveAsLinks ? `#${tab.id}` : undefined} | ||
onClick={() => { | ||
if (!behaveAsLinks) { | ||
setActiveTabId(tab.id); | ||
} | ||
}} | ||
/> | ||
))} | ||
</StyledContainer> | ||
</ScrollWrapper> | ||
<StyledContainer className={className}> | ||
{truncatedTabs.map((tab) => ( | ||
<Tab | ||
key={tab.id} | ||
id={tab.id} | ||
title={tab.title} | ||
Icon={tab.Icon} | ||
logo={tab.logo} | ||
active={tab.id === activeTabId} | ||
disabled={tab.disabled ?? loading} | ||
pill={tab.pill} | ||
to={behaveAsLinks ? `#${tab.id}` : undefined} | ||
onClick={() => handleTabClick(tab.id)} | ||
inDropdown={false} | ||
/> | ||
))} | ||
{remainingTabs.length > 0 && ( | ||
<MoreTabsDropdown | ||
id="more-button" | ||
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. style: Consider making 'more-button' id unique by appending tabListInstanceId to prevent potential conflicts with multiple tab lists |
||
title={`+${remainingTabs.length} More`} | ||
Icon={IconChevronDown} | ||
dropdownContent={ | ||
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. instead of passing dropdownContent as a react node why not just pass remaining tab and use them in Dropdown ? 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. I’m not sure about this. Since Tab is already rendered in the parent component of MoreButton, it has all the necessary state for rendering tabs. Some props (like activeTabId, behaveAsLinks, and handleTabClick) need to be passed down to MoreButton. If we render the tabs inside MoreButton, it adds complexity, whereas passing dropdownContent as a ReactNode keeps it simpler. What do you think? |
||
<StyledDropdownContainer> | ||
{remainingTabs.map((tab) => ( | ||
<Tab | ||
key={tab.id} | ||
id={tab.id} | ||
title={tab.title} | ||
Icon={tab.Icon} | ||
logo={tab.logo} | ||
active={tab.id === activeTabId} | ||
disabled={tab.disabled ?? loading} | ||
pill={tab.pill} | ||
to={behaveAsLinks ? `#${tab.id}` : undefined} | ||
onClick={() => handleTabClick(tab.id)} | ||
inDropdown={true} | ||
/> | ||
))} | ||
</StyledDropdownContainer> | ||
} | ||
dropdownPlacement="bottom-start" | ||
/> | ||
)} | ||
</StyledContainer> | ||
</TabListScope> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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 <Button title={title} Icon={Icon} />; | ||||||||||||||||||||||||||
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. style: missing variant and size props here to match the dropdown button styling |
||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
const StyledButtonContainer = styled.div` | ||||||||||||||||||||||||||
height: 100%; | ||||||||||||||||||||||||||
display: flex; | ||||||||||||||||||||||||||
align-items: center; | ||||||||||||||||||||||||||
`; | ||||||||||||||||||||||||||
Comment on lines
+29
to
+33
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. style: StyledButtonContainer should be defined outside the component to prevent recreation on each render
Suggested change
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Define a basic hotkey scope for the dropdown | ||||||||||||||||||||||||||
const dropdownHotkeyScope: HotkeyScope = { | ||||||||||||||||||||||||||
scope: `dropdown-${id}`, | ||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||
<Dropdown | ||||||||||||||||||||||||||
dropdownId={`more-button-${id}-dropdown`} | ||||||||||||||||||||||||||
dropdownHotkeyScope={dropdownHotkeyScope} | ||||||||||||||||||||||||||
dropdownPlacement={dropdownPlacement} | ||||||||||||||||||||||||||
dropdownComponents={dropdownContent} | ||||||||||||||||||||||||||
onOpen={onDropdownOpen} | ||||||||||||||||||||||||||
onClose={onDropdownClose} | ||||||||||||||||||||||||||
clickableComponent={ | ||||||||||||||||||||||||||
<StyledButtonContainer> | ||||||||||||||||||||||||||
<Button | ||||||||||||||||||||||||||
variant="tertiary" | ||||||||||||||||||||||||||
size="small" | ||||||||||||||||||||||||||
title={title} | ||||||||||||||||||||||||||
Icon={Icon} | ||||||||||||||||||||||||||
IconPosition="right" | ||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||
Comment on lines
+50
to
+56
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. syntax: IconPosition prop is not defined in Button component types |
||||||||||||||||||||||||||
</StyledButtonContainer> | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||
}; |
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. I don't think ![]() As per @lucasbordeau's comment, we should implement this approach. While your current implementation might work, it also constrains tabs from being more dynamic. Would you be able to look into this alternative approach? 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. hmm, true. 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. I made the changes @ehconitin can review it? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export const TAB_CONSTANTS = { | ||
MAX_VISIBLE_TABS: 4, | ||
} as const; |
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.
we can leave it as it was it defaults to false
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.
It seems it defaults to undefined