Files
twenty/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx
T
Thomas Trompette 58b8e8afee Fix clipped New chat button label in localized navigation drawer (#23511)
Fixes #23503

<img width="161" height="76" alt="Capture d’écran 2026-07-29 à 17 20
25"
src="https://github.com/user-attachments/assets/a6ea0a12-b91d-419f-8023-8e65d5dce65b"
/>

The expanded "New chat" button had a hardcoded `width: 103px`, sized for
the English label. Localized labels ("Neuer Chat", "Nouveau chat") were
clipped, since `OverflowingTextWithTooltip` can only truncate inside a
parent it cannot resize.

The expanded wrapper is now `width: max-content` with `max-width: 100%`,
so it grows with the label and only truncates (with tooltip) when the
sidebar has no space left. `min-width` keeps the pill from collapsing
below icon size. Collapsed state is unchanged.

### Verified locally (German locale)

| | wrapper width | label |
|---|---|---|
| before | 103px (fixed) | `Neuer C...` clipped |
| after | 109px (max-content) | `Neuer Chat` in full |

- English: 103px -> 98px, visually identical.
- Collapsed drawer: still exactly 24x24, unchanged.
- Very long label (Vietnamese-length): wrapper stops at the sidebar
edge, no overflow, label truncates with tooltip.
2026-07-29 15:24:45 +00:00

282 lines
9.4 KiB
TypeScript

import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import {
type IconComponent,
IconComment,
IconHome,
IconMessageCirclePlus,
} from 'twenty-ui/icon';
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
import { useContext } from 'react';
import { useSwitchToNewAiChat } from '@/ai/hooks/useSwitchToNewAiChat';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { navigationDrawerActiveTabState } from '@/ui/navigation/states/navigationDrawerActiveTabState';
import {
type NavigationDrawerActiveTab,
NAVIGATION_DRAWER_TABS,
} from '@/ui/navigation/states/navigationDrawerTabs';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { PermissionFlagType } from '~/generated-metadata/graphql';
const StyledRow = styled.div<{ isExpanded: boolean }>`
align-items: center;
display: flex;
gap: ${({ isExpanded }) => (isExpanded ? themeCssVariables.spacing[2] : 0)};
justify-content: ${({ isExpanded }) =>
isExpanded ? 'space-between' : 'center'};
transition: gap calc(${themeCssVariables.animation.duration.normal} * 1s) ease;
width: ${({ isExpanded }) => (isExpanded ? '100%' : 'max-content')};
`;
const StyledTabsPill = styled.div`
align-items: center;
background: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.pill};
box-sizing: border-box;
corner-shape: round;
display: flex;
flex-shrink: 0;
gap: ${themeCssVariables.spacing[0.5]};
height: ${themeCssVariables.spacing[7]};
padding: 3px;
width: ${themeCssVariables.spacing[18]};
`;
const StyledTabWrapper = styled.div<{ isActive: boolean }>`
align-items: center;
background: ${({ isActive }) =>
isActive ? themeCssVariables.background.transparent.light : 'transparent'};
border-radius: ${themeCssVariables.border.radius.pill};
color: ${({ isActive }) =>
isActive
? themeCssVariables.font.color.primary
: themeCssVariables.font.color.tertiary};
corner-shape: round;
cursor: pointer;
display: flex;
flex: 1;
height: 100%;
justify-content: center;
&:hover {
background: ${({ isActive }) =>
isActive
? themeCssVariables.background.transparent.light
: themeCssVariables.background.transparent.lighter};
}
`;
const StyledTabIcon = styled.div`
align-items: center;
display: flex;
height: ${themeCssVariables.spacing[5]};
justify-content: center;
width: ${themeCssVariables.spacing[5]};
`;
const StyledNewChatIcon = styled.div`
align-items: center;
display: flex;
flex-grow: 0;
flex-shrink: 0;
justify-content: center;
`;
const StyledNewChatButtonWrapper = styled.div<{ isExpanded: boolean }>`
align-items: center;
background: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.pill};
box-sizing: border-box;
corner-shape: round;
display: flex;
height: ${({ isExpanded }) =>
isExpanded ? themeCssVariables.spacing[7] : themeCssVariables.spacing[6]};
justify-content: center;
max-width: 100%;
min-width: ${({ isExpanded }) =>
isExpanded ? themeCssVariables.spacing[7] : themeCssVariables.spacing[6]};
padding: ${({ isExpanded }) =>
isExpanded ? '3px' : themeCssVariables.spacing[0.5]};
transition:
height calc(${themeCssVariables.animation.duration.normal} * 1s) ease,
padding calc(${themeCssVariables.animation.duration.normal} * 1s) ease;
width: ${({ isExpanded }) =>
isExpanded ? 'max-content' : themeCssVariables.spacing[6]};
`;
const StyledNewChatButton = styled.div`
align-items: center;
border-radius: inherit;
color: ${themeCssVariables.font.color.secondary};
cursor: pointer;
display: flex;
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.medium};
gap: ${themeCssVariables.spacing[1]};
height: 100%;
justify-content: center;
min-width: 0;
overflow: hidden;
padding-inline: ${themeCssVariables.spacing[2]};
transition:
background calc(${themeCssVariables.animation.duration.fast} * 1s) ease,
color calc(${themeCssVariables.animation.duration.fast} * 1s) ease;
width: 100%;
&:hover {
background: ${themeCssVariables.background.transparent.light};
color: ${themeCssVariables.font.color.primary};
}
`;
type MainNavigationDrawerTabsRowProps = {
NavigationMenuTabIcon?: IconComponent;
navigationMenuTabLabel?: string;
};
export const MainNavigationDrawerTabsRow = ({
NavigationMenuTabIcon = IconHome,
navigationMenuTabLabel = t`Home`,
}: MainNavigationDrawerTabsRowProps) => {
const { theme } = useContext(ThemeContext);
const isMobile = useIsMobile();
const isNavigationDrawerExpanded = useAtomStateValue(
isNavigationDrawerExpandedState,
);
const [navigationDrawerActiveTab, setNavigationDrawerActiveTab] =
useAtomState(navigationDrawerActiveTabState);
const { switchToNewChat } = useSwitchToNewAiChat();
const setIsNavigationDrawerExpanded = useSetAtomState(
isNavigationDrawerExpandedState,
);
const hasAiPermission = useHasPermissionFlag(PermissionFlagType.AI);
const isExpanded = isNavigationDrawerExpanded || isMobile;
if (!hasAiPermission) {
return null;
}
const handleTabClick = (tab: NavigationDrawerActiveTab) => () => {
setNavigationDrawerActiveTab(tab);
};
const handleTabKeyDown =
(tab: NavigationDrawerActiveTab) => (event: React.KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
setNavigationDrawerActiveTab(tab);
}
};
const handleNewChatClick = () => {
if (isMobile) {
setIsNavigationDrawerExpanded(false);
}
switchToNewChat();
};
const handleNewChatKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleNewChatClick();
}
};
const getTabIconColor = (isActive: boolean) =>
isActive ? theme.font.color.primary : theme.font.color.tertiary;
return (
<StyledRow isExpanded={isExpanded}>
<NavigationDrawerAnimatedCollapseWrapper>
<StyledTabsPill role="tablist" aria-label={t`Navigation tabs`}>
<StyledTabWrapper
isActive={
navigationDrawerActiveTab ===
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU
}
role="tab"
aria-selected={
navigationDrawerActiveTab ===
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU
}
aria-label={navigationMenuTabLabel}
tabIndex={
navigationDrawerActiveTab ===
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU
? 0
: -1
}
onClick={handleTabClick(NAVIGATION_DRAWER_TABS.NAVIGATION_MENU)}
onKeyDown={handleTabKeyDown(NAVIGATION_DRAWER_TABS.NAVIGATION_MENU)}
>
<StyledTabIcon>
<NavigationMenuTabIcon
size={theme.icon.size.md}
color={getTabIconColor(
navigationDrawerActiveTab ===
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU,
)}
/>
</StyledTabIcon>
</StyledTabWrapper>
<StyledTabWrapper
isActive={
navigationDrawerActiveTab ===
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY
}
role="tab"
aria-selected={
navigationDrawerActiveTab ===
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY
}
aria-label={t`Chat`}
tabIndex={
navigationDrawerActiveTab ===
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY
? 0
: -1
}
onClick={handleTabClick(NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY)}
onKeyDown={handleTabKeyDown(NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY)}
>
<StyledTabIcon>
<IconComment
size={theme.icon.size.md}
color={getTabIconColor(
navigationDrawerActiveTab ===
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY,
)}
/>
</StyledTabIcon>
</StyledTabWrapper>
</StyledTabsPill>
</NavigationDrawerAnimatedCollapseWrapper>
<StyledNewChatButtonWrapper isExpanded={isExpanded}>
<StyledNewChatButton
role="button"
tabIndex={0}
aria-label={t`New chat`}
onClick={handleNewChatClick}
onKeyDown={handleNewChatKeyDown}
>
<StyledNewChatIcon>
<IconMessageCirclePlus size={theme.icon.size.md} />
</StyledNewChatIcon>
{isExpanded && <OverflowingTextWithTooltip text={t`New chat`} />}
</StyledNewChatButton>
</StyledNewChatButtonWrapper>
</StyledRow>
);
};