Files
twenty/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx
T
Charles Bochet 647c32ff3e Deprecate runtime theme objects in favor of CSS variables (#18402)
## Summary

- **Eliminate `ICON_SIZES` / `ICON_STROKES` constants**: all icon
dimensions are now resolved at runtime via
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.X)`, ensuring
values always come from computed CSS variables
- **No more consumer imports from `twenty-ui/theme`**: moved
`ColorSchemeContext`, `ColorSchemeProvider`, `ThemeColor`,
`MAIN_COLOR_NAMES`, `getNextThemeColor`, `AnimationDuration` to
`twenty-ui/theme-constants`
- **Remove `ThemeContext` / `ThemeContextProvider` / `ThemeProvider` /
`ThemeType`**: replaced across ~300 files with `themeCssVariables` (for
CSS contexts) or `resolveThemeVariable` / `resolveThemeVariableAsNumber`
(for JS runtime values)
- **Simplify provider chain**: only `ColorSchemeProvider` remains — it
toggles `light`/`dark` class on `document.documentElement` and provides
`colorScheme` via React context
- **Fix pre-existing test failures**: `useIcons.test.ts`
(non-configurable ES module spy) and
`turnRecordFilterGroupIntoGqlOperationFilter.test.ts`
(`Omit<RecordFilter, 'id'>` type mismatch)

### Theme access pattern (before → after)

| Context | Before | After |
|---------|--------|-------|
| CSS (Linaria) | `${({ theme }) => theme.font.color.primary}` |
`${themeCssVariables.font.color.primary}` |
| JS runtime (icon size, animation) | `theme.icon.size.md` /
`ICON_SIZES.md` |
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.md)` |
| Color scheme check | `theme.name === 'dark'` |
`useContext(ColorSchemeContext).colorScheme === 'dark'` |
2026-03-05 14:39:01 +01:00

174 lines
4.5 KiB
TypeScript

import { styled } from '@linaria/react';
import { type ReactNode, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from '@ui/theme-constants';
import { AppTooltip, TooltipDelay } from './AppTooltip';
const spacing4 = themeCssVariables.spacing[4];
const StyledOverflowingMultilineText = styled.div<{
isContentOverflowing: boolean;
size: 'large' | 'small';
displayedMaxRows: number;
}>`
cursor: ${({ isContentOverflowing }) =>
isContentOverflowing ? 'pointer' : 'inherit'};
font-family: inherit;
font-size: inherit;
font-weight: inherit;
max-width: 100%;
overflow: hidden;
text-decoration: inherit;
text-overflow: ellipsis;
height: ${({ size }) => (size === 'large' ? spacing4 : 'auto')};
-webkit-line-clamp: ${({ displayedMaxRows }) =>
displayedMaxRows ? displayedMaxRows.toString() : '1'};
display: -webkit-box;
-webkit-box-orient: vertical;
white-space: pre-wrap;
`;
const StyledOverflowingText = styled.div<{
isContentOverflowing: boolean;
size: 'large' | 'small';
}>`
cursor: ${({ isContentOverflowing }) =>
isContentOverflowing ? 'pointer' : 'inherit'};
font-family: inherit;
font-size: inherit;
font-weight: inherit;
max-width: 100%;
text-decoration: inherit;
text-overflow: ellipsis;
overflow: hidden;
height: ${({ size }) => (size === 'large' ? spacing4 : 'auto')};
white-space: nowrap;
`;
const StyledPre = styled.pre`
font-family: inherit;
white-space: pre-wrap;
`;
type OverflowingTextWithTooltipProps = {
size?: 'large' | 'small';
isTooltipMultiline?: boolean;
displayedMaxRows?: number;
tooltipDelay?: TooltipDelay;
} & (
| {
text: string | null | undefined;
tooltipContent?: never;
}
| {
text: Exclude<ReactNode, string | null | undefined>;
tooltipContent: string;
}
);
export const OverflowingTextWithTooltip = ({
size = 'small',
text,
isTooltipMultiline,
displayedMaxRows,
tooltipContent,
tooltipDelay = TooltipDelay.mediumDelay,
}: OverflowingTextWithTooltipProps) => {
const textElementId = `title-id-${+new Date()}`;
const textRef = useRef<HTMLDivElement>(null);
const [isTitleOverflowing, setIsTitleOverflowing] = useState(false);
const [shouldRenderTooltip, setShouldRenderTooltip] = useState(false);
const handleMouseEnter = () => {
const isOverflowing = textRef.current
? textRef.current?.scrollHeight > textRef.current?.clientHeight ||
textRef.current.scrollWidth > textRef.current.clientWidth
: false;
setIsTitleOverflowing(isOverflowing);
setShouldRenderTooltip(true);
};
const handleMouseLeave = () => {
setIsTitleOverflowing(false);
setShouldRenderTooltip(false);
};
const handleTooltipClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
event.preventDefault();
};
const tooltipText = isNonEmptyString(tooltipContent)
? tooltipContent
: isNonEmptyString(text)
? text
: null;
return (
<>
{isDefined(displayedMaxRows) ? (
<StyledOverflowingMultilineText
data-testid="tooltip"
isContentOverflowing={isTitleOverflowing}
size={size}
displayedMaxRows={displayedMaxRows}
ref={textRef}
id={textElementId}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{text}
</StyledOverflowingMultilineText>
) : (
<StyledOverflowingText
data-testid="tooltip"
isContentOverflowing={isTitleOverflowing}
size={size}
ref={textRef}
id={textElementId}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{text}
</StyledOverflowingText>
)}
{shouldRenderTooltip &&
isTitleOverflowing &&
isDefined(tooltipText) &&
createPortal(
<div onClick={handleTooltipClick}>
<AppTooltip
anchorSelect={`#${textElementId}`}
offset={5}
noArrow
place="bottom"
positionStrategy="absolute"
delay={tooltipDelay}
isOpen={true}
>
{isTooltipMultiline ? (
<StyledPre>{tooltipText}</StyledPre>
) : (
tooltipText
)}
</AppTooltip>
</div>,
document.body,
)}
</>
);
};