Added tooltip on overflowing texts (#771)

* Ok

* Fixes

* Fix according to PR

* Fix lint

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-07-20 06:23:42 +02:00
committed by GitHub
parent 60b50387a7
commit 7670ae5638
13 changed files with 176 additions and 28 deletions

View File

@ -18,6 +18,8 @@ import {
beautifyPastDateRelativeToNow, beautifyPastDateRelativeToNow,
} from '~/utils/date-utils'; } from '~/utils/date-utils';
import { OverflowingTextWithTooltip } from '../../../ui/tooltip/OverflowingTextWithTooltip';
const StyledMainContainer = styled.div` const StyledMainContainer = styled.div`
align-items: flex-start; align-items: flex-start;
align-self: stretch; align-self: stretch;
@ -144,17 +146,15 @@ const StyledCardTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.medium}; font-weight: ${({ theme }) => theme.font.weight.medium};
line-height: ${({ theme }) => theme.text.lineHeight.lg}; line-height: ${({ theme }) => theme.text.lineHeight.lg};
width: 100%;
`; `;
const StyledCardContent = styled.div` const StyledCardContent = styled.div`
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
align-self: stretch; align-self: stretch;
color: ${({ theme }) => theme.font.color.secondary}; color: ${({ theme }) => theme.font.color.secondary};
display: -webkit-box;
overflow: hidden; width: 100%;
text-overflow: ellipsis;
`; `;
const StyledTooltip = styled(Tooltip)` const StyledTooltip = styled(Tooltip)`
@ -279,10 +279,18 @@ export function Timeline({ entity }: { entity: CommentableEntity }) {
} }
> >
<StyledCardTitle> <StyledCardTitle>
{commentThread.title ? commentThread.title : '(No title)'} <OverflowingTextWithTooltip
text={
commentThread.title
? commentThread.title
: '(No title)'
}
/>
</StyledCardTitle> </StyledCardTitle>
<StyledCardContent> <StyledCardContent>
{body ? body : '(No content)'} <OverflowingTextWithTooltip
text={body ? body : '(No content)'}
/>
</StyledCardContent> </StyledCardContent>
</StyledCard> </StyledCard>
</StyledCardContainer> </StyledCardContainer>

View File

@ -45,6 +45,7 @@ const StyledBoardCard = styled.div<{ selected: boolean }>`
const StyledBoardCardWrapper = styled.div` const StyledBoardCardWrapper = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)}; padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`; `;
const StyledBoardCardHeader = styled.div` const StyledBoardCardHeader = styled.div`
@ -64,6 +65,7 @@ const StyledBoardCardHeader = styled.div`
width: ${({ theme }) => theme.icon.size.md}px; width: ${({ theme }) => theme.icon.size.md}px;
} }
`; `;
const StyledBoardCardBody = styled.div` const StyledBoardCardBody = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -12,7 +12,9 @@ export const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
isFirstColumn ? 'none' : theme.border.color.light}; isFirstColumn ? 'none' : theme.border.color.light};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 200px;
min-width: 200px; min-width: 200px;
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
`; `;

View File

@ -5,6 +5,8 @@ import styled from '@emotion/styled';
import { Avatar, AvatarType } from '@/users/components/Avatar'; import { Avatar, AvatarType } from '@/users/components/Avatar';
import { OverflowingTextWithTooltip } from '../../tooltip/OverflowingTextWithTooltip';
export enum ChipVariant { export enum ChipVariant {
opaque = 'opaque', opaque = 'opaque',
transparent = 'transparent', transparent = 'transparent',
@ -94,7 +96,9 @@ export function EntityChip({
size={14} size={14}
type={avatarType} type={avatarType}
/> />
<StyledName>{name}</StyledName> <StyledName>
<OverflowingTextWithTooltip text={name} />
</StyledName>
</StyledContainerLink> </StyledContainerLink>
) : ( ) : (
<StyledContainerReadOnly data-testid="entity-chip"> <StyledContainerReadOnly data-testid="entity-chip">
@ -105,7 +109,9 @@ export function EntityChip({
size={14} size={14}
type={avatarType} type={avatarType}
/> />
<StyledName>{name}</StyledName> <StyledName>
<OverflowingTextWithTooltip text={name} />
</StyledName>
</StyledContainerReadOnly> </StyledContainerReadOnly>
); );
} }

View File

@ -23,6 +23,8 @@ const DropdownMenuSelectableItemContainer = styled(DropdownMenuItem)<Props>`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
max-width: 150px;
`; `;
const StyledLeftContainer = styled.div` const StyledLeftContainer = styled.div`
@ -30,6 +32,8 @@ const StyledLeftContainer = styled.div`
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(2)};
overflow: hidden;
`; `;
const StyledRightIcon = styled.div` const StyledRightIcon = styled.div`

View File

@ -72,7 +72,7 @@ export function NumberEditableField({
/> />
} }
displayModeContent={internalValue} displayModeContent={internalValue}
isDisplayModeContentEmpty={!(internalValue !== '')} isDisplayModeContentEmpty={!(internalValue !== '' && internalValue)}
/> />
</RecoilScope> </RecoilScope>
); );

View File

@ -5,6 +5,8 @@ import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText'; import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { OverflowingTextWithTooltip } from '../../../tooltip/OverflowingTextWithTooltip';
type OwnProps = { type OwnProps = {
icon?: React.ReactNode; icon?: React.ReactNode;
placeholder?: string; placeholder?: string;
@ -54,7 +56,7 @@ export function TextEditableField({
}} }}
/> />
} }
displayModeContent={internalValue} displayModeContent={<OverflowingTextWithTooltip text={internalValue} />}
isDisplayModeContentEmpty={!(internalValue !== '')} isDisplayModeContentEmpty={!(internalValue !== '')}
/> />
</RecoilScope> </RecoilScope>

View File

@ -9,6 +9,8 @@ import {
beautifyPastDateRelativeToNow, beautifyPastDateRelativeToNow,
} from '~/utils/date-utils'; } from '~/utils/date-utils';
import { OverflowingTextWithTooltip } from '../../../tooltip/OverflowingTextWithTooltip';
type OwnProps = { type OwnProps = {
id?: string; id?: string;
logoOrAvatar?: string; logoOrAvatar?: string;
@ -31,6 +33,7 @@ const StyledInfoContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
width: 100%;
`; `;
const StyledDate = styled.div` const StyledDate = styled.div`
@ -42,6 +45,8 @@ const StyledTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.xl}; font-size: ${({ theme }) => theme.font.size.xl};
font-weight: ${({ theme }) => theme.font.weight.semiBold}; font-weight: ${({ theme }) => theme.font.weight.semiBold};
max-width: 100%;
`; `;
const StyledTooltip = styled(Tooltip)` const StyledTooltip = styled(Tooltip)`
@ -50,6 +55,8 @@ const StyledTooltip = styled(Tooltip)`
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
`; `;
@ -74,7 +81,9 @@ export function ShowPageSummaryCard({
placeholder={title} placeholder={title}
/> />
<StyledInfoContainer> <StyledInfoContainer>
<StyledTitle>{title}</StyledTitle> <StyledTitle>
<OverflowingTextWithTooltip text={title} />
</StyledTitle>
<StyledDate id={dateElementId}> <StyledDate id={dateElementId}>
Added {beautifiedCreatedAt} ago Added {beautifiedCreatedAt} ago
</StyledDate> </StyledDate>

View File

@ -6,6 +6,8 @@ import { IconButton } from '@/ui/button/components/IconButton';
import { IconChevronLeft, IconPlus } from '@/ui/icon/index'; import { IconChevronLeft, IconPlus } from '@/ui/icon/index';
import NavCollapseButton from '@/ui/navbar/components/NavCollapseButton'; import NavCollapseButton from '@/ui/navbar/components/NavCollapseButton';
import { OverflowingTextWithTooltip } from '../../../tooltip/OverflowingTextWithTooltip';
export const TOP_BAR_MIN_HEIGHT = 40; export const TOP_BAR_MIN_HEIGHT = 40;
const TopBarContainer = styled.div` const TopBarContainer = styled.div`
@ -14,18 +16,25 @@ const TopBarContainer = styled.div`
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
display: flex; display: flex;
flex-direction: row; flex-direction: row;
font-size: 14px; font-size: ${({ theme }) => theme.font.size.lg};
justify-content: space-between;
min-height: ${TOP_BAR_MIN_HEIGHT}px; min-height: ${TOP_BAR_MIN_HEIGHT}px;
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(3)}; padding-right: ${({ theme }) => theme.spacing(3)};
`; `;
const StyledLeftContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
width: 100%;
`;
const TitleContainer = styled.div` const TitleContainer = styled.div`
display: flex; display: flex;
font-family: 'Inter'; font-size: ${({ theme }) => theme.font.size.md};
font-size: 14px; margin-left: ${({ theme }) => theme.spacing(1)};
margin-left: 4px; max-width: 50%;
width: 100%;
`; `;
const BackIconButton = styled(IconButton)` const BackIconButton = styled(IconButton)`
@ -51,15 +60,19 @@ export function TopBar({
return ( return (
<> <>
<TopBarContainer> <TopBarContainer>
<NavCollapseButton hideIfOpen={true} /> <StyledLeftContainer>
{hasBackButton && ( <NavCollapseButton hideIfOpen={true} />
<BackIconButton {hasBackButton && (
icon={<IconChevronLeft size={16} />} <BackIconButton
onClick={navigateBack} icon={<IconChevronLeft size={16} />}
/> onClick={navigateBack}
)} />
{icon} )}
<TitleContainer data-testid="top-bar-title">{title}</TitleContainer> {icon}
<TitleContainer data-testid="top-bar-title">
<OverflowingTextWithTooltip text={title} />
</TitleContainer>
</StyledLeftContainer>
{onAddButtonClick && ( {onAddButtonClick && (
<IconButton <IconButton
icon={<IconPlus size={16} />} icon={<IconPlus size={16} />}

View File

@ -9,6 +9,7 @@ import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { OverflowingTextWithTooltip } from '../../tooltip/OverflowingTextWithTooltip';
import { useEntitySelectScroll } from '../hooks/useEntitySelectScroll'; import { useEntitySelectScroll } from '../hooks/useEntitySelectScroll';
import { EntityForSelect } from '../types/EntityForSelect'; import { EntityForSelect } from '../types/EntityForSelect';
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope'; import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
@ -86,7 +87,7 @@ export function SingleEntitySelectBase<
size={16} size={16}
type={entity.avatarType ?? 'rounded'} type={entity.avatarType ?? 'rounded'}
/> />
{entity.name} <OverflowingTextWithTooltip text={entity.name} />
</DropdownMenuSelectableItem> </DropdownMenuSelectableItem>
)) ))
)} )}

View File

@ -14,6 +14,7 @@ export const borderLight = {
strong: grayScale.gray25, strong: grayScale.gray25,
medium: grayScale.gray20, medium: grayScale.gray20,
light: grayScale.gray15, light: grayScale.gray15,
invertedSecondary: grayScale.gray50,
inverted: grayScale.gray60, inverted: grayScale.gray60,
}, },
...common, ...common,
@ -24,6 +25,7 @@ export const borderDark = {
strong: grayScale.gray65, strong: grayScale.gray65,
medium: grayScale.gray70, medium: grayScale.gray70,
light: grayScale.gray75, light: grayScale.gray75,
invertedSecondary: grayScale.gray40,
inverted: grayScale.gray30, inverted: grayScale.gray30,
}, },
...common, ...common,

View File

@ -0,0 +1,18 @@
import { Tooltip } from 'react-tooltip';
import styled from '@emotion/styled';
export const AppTooltip = styled(Tooltip)`
background-color: ${({ theme }) => theme.background.primary};
box-shadow: ${({ theme }) => theme.boxShadow.light};
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
max-width: 40%;
padding: ${({ theme }) => theme.spacing(2)};
word-break: break-word;
z-index: ${({ theme }) => theme.lastLayerZIndex};
`;

View File

@ -0,0 +1,81 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import styled from '@emotion/styled';
import { v4 as uuidV4 } from 'uuid';
import { AppTooltip } from './AppTooltip';
const StyledOverflowingText = styled.div<{ cursorPointer: boolean }>`
cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'inherit')};
font-family: inherit;
font-size: inherit;
font-weight: inherit;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
`;
export function OverflowingTextWithTooltip({
text,
}: {
text: string | null | undefined;
}) {
const textElementId = `title-id-${uuidV4()}`;
const textRef = useRef<HTMLDivElement>(null);
const [isTitleOverflowing, setIsTitleOverflowing] = useState(false);
useEffect(() => {
const isOverflowing =
(text?.length ?? 0) > 0 && textRef.current
? textRef.current?.scrollHeight > textRef.current?.clientHeight ||
textRef.current.scrollWidth > textRef.current.clientWidth
: false;
if (isTitleOverflowing !== isOverflowing) {
setIsTitleOverflowing(isOverflowing);
}
}, [isTitleOverflowing, text]);
function handleTooltipClick(event: React.MouseEvent<HTMLDivElement>) {
event.stopPropagation();
event.preventDefault();
}
function handleTooltipMouseUp(event: React.MouseEvent<HTMLDivElement>) {
event.stopPropagation();
event.preventDefault();
}
return (
<>
<StyledOverflowingText
ref={textRef}
id={textElementId}
cursorPointer={isTitleOverflowing}
>
{text}
</StyledOverflowingText>
{isTitleOverflowing &&
createPortal(
<div onMouseUp={handleTooltipMouseUp} onClick={handleTooltipClick}>
<AppTooltip
anchorSelect={`#${textElementId}`}
content={text ?? ''}
clickable
delayHide={100}
offset={5}
noArrow
place="bottom"
positionStrategy="absolute"
/>
</div>,
document.body,
)}
</>
);
}