@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
@ -88,7 +88,6 @@ export const CalendarEventParticipantsResponseStatusField = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const participantsContainerRef = useRef<HTMLDivElement>(null);
|
const participantsContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
const styledChips = orderedParticipants.map((participant, index) => (
|
const styledChips = orderedParticipants.map((participant, index) => (
|
||||||
<ParticipantChip key={index} participant={participant} />
|
<ParticipantChip key={index} participant={participant} />
|
||||||
));
|
));
|
||||||
@ -103,16 +102,11 @@ export const CalendarEventParticipantsResponseStatusField = ({
|
|||||||
<EllipsisDisplay>{responseStatus}</EllipsisDisplay>
|
<EllipsisDisplay>{responseStatus}</EllipsisDisplay>
|
||||||
</StyledLabelContainer>
|
</StyledLabelContainer>
|
||||||
</StyledLabelAndIconContainer>
|
</StyledLabelAndIconContainer>
|
||||||
<StyledDiv
|
<StyledDiv ref={participantsContainerRef}>
|
||||||
ref={participantsContainerRef}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
>
|
|
||||||
{isRightDrawerAnimationCompleted && (
|
{isRightDrawerAnimationCompleted && (
|
||||||
<ExpandableList
|
<ExpandableList
|
||||||
isHovered={isHovered}
|
anchorElement={participantsContainerRef.current || undefined}
|
||||||
reference={participantsContainerRef.current || undefined}
|
isChipCountDisplayed
|
||||||
forceDisplayHiddenCount
|
|
||||||
>
|
>
|
||||||
{styledChips}
|
{styledChips}
|
||||||
</ExpandableList>
|
</ExpandableList>
|
||||||
|
|||||||
@ -2,10 +2,13 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
import {
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
ExpandableList,
|
|
||||||
ExpandableListProps,
|
type ActivityTargetChipsProps = {
|
||||||
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
|
||||||
|
anchorElement?: HTMLElement;
|
||||||
|
maxWidth?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div<{ maxWidth?: number }>`
|
const StyledContainer = styled.div<{ maxWidth?: number }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -16,20 +19,12 @@ const StyledContainer = styled.div<{ maxWidth?: number }>`
|
|||||||
|
|
||||||
export const ActivityTargetChips = ({
|
export const ActivityTargetChips = ({
|
||||||
activityTargetObjectRecords,
|
activityTargetObjectRecords,
|
||||||
isHovered,
|
anchorElement,
|
||||||
reference,
|
|
||||||
maxWidth,
|
maxWidth,
|
||||||
}: {
|
}: ActivityTargetChipsProps) => {
|
||||||
activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
|
|
||||||
maxWidth?: number;
|
|
||||||
} & ExpandableListProps) => {
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer maxWidth={maxWidth}>
|
<StyledContainer maxWidth={maxWidth}>
|
||||||
<ExpandableList
|
<ExpandableList anchorElement={anchorElement} isChipCountDisplayed>
|
||||||
isHovered={isHovered}
|
|
||||||
reference={reference}
|
|
||||||
forceDisplayHiddenCount
|
|
||||||
>
|
|
||||||
{activityTargetObjectRecords.map(
|
{activityTargetObjectRecords.map(
|
||||||
(activityTargetObjectRecord, index) => (
|
(activityTargetObjectRecord, index) => (
|
||||||
<RecordChip
|
<RecordChip
|
||||||
|
|||||||
@ -53,12 +53,13 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Relations"
|
label="Relations"
|
||||||
displayModeContent={
|
displayModeContent={({ cellElement }) => (
|
||||||
<ActivityTargetChips
|
<ActivityTargetChips
|
||||||
|
anchorElement={cellElement}
|
||||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
|
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
|
||||||
/>
|
/>
|
||||||
</RecordFieldInputScope>
|
</RecordFieldInputScope>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { useContext } from 'react';
|
|||||||
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
||||||
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
|
|
||||||
|
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
|
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
|
||||||
@ -38,13 +37,17 @@ import { isFieldSelect } from '../types/guards/isFieldSelect';
|
|||||||
import { isFieldText } from '../types/guards/isFieldText';
|
import { isFieldText } from '../types/guards/isFieldText';
|
||||||
import { isFieldUuid } from '../types/guards/isFieldUuid';
|
import { isFieldUuid } from '../types/guards/isFieldUuid';
|
||||||
|
|
||||||
type FieldDisplayProps = ExpandableListProps;
|
type FieldDisplayProps = {
|
||||||
|
isCellSoftFocused?: boolean;
|
||||||
|
cellElement?: HTMLElement;
|
||||||
|
fromTableCell?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const FieldDisplay = ({
|
export const FieldDisplay = ({
|
||||||
isHovered,
|
isCellSoftFocused,
|
||||||
reference,
|
cellElement,
|
||||||
fromTableCell,
|
fromTableCell,
|
||||||
}: FieldDisplayProps & { fromTableCell?: boolean }) => {
|
}: FieldDisplayProps) => {
|
||||||
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
|
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
|
||||||
|
|
||||||
const isChipDisplay =
|
const isChipDisplay =
|
||||||
@ -75,7 +78,11 @@ export const FieldDisplay = ({
|
|||||||
) : isFieldLink(fieldDefinition) ? (
|
) : isFieldLink(fieldDefinition) ? (
|
||||||
<LinkFieldDisplay />
|
<LinkFieldDisplay />
|
||||||
) : isFieldLinks(fieldDefinition) ? (
|
) : isFieldLinks(fieldDefinition) ? (
|
||||||
<LinksFieldDisplay />
|
<LinksFieldDisplay
|
||||||
|
isCellSoftFocused={isCellSoftFocused}
|
||||||
|
cellElement={cellElement}
|
||||||
|
fromTableCell={fromTableCell}
|
||||||
|
/>
|
||||||
) : isFieldCurrency(fieldDefinition) ? (
|
) : isFieldCurrency(fieldDefinition) ? (
|
||||||
<CurrencyFieldDisplay />
|
<CurrencyFieldDisplay />
|
||||||
) : isFieldFullName(fieldDefinition) ? (
|
) : isFieldFullName(fieldDefinition) ? (
|
||||||
@ -84,9 +91,9 @@ export const FieldDisplay = ({
|
|||||||
<SelectFieldDisplay />
|
<SelectFieldDisplay />
|
||||||
) : isFieldMultiSelect(fieldDefinition) ? (
|
) : isFieldMultiSelect(fieldDefinition) ? (
|
||||||
<MultiSelectFieldDisplay
|
<MultiSelectFieldDisplay
|
||||||
isHovered={isHovered}
|
isCellSoftFocused={isCellSoftFocused}
|
||||||
reference={reference}
|
cellElement={cellElement}
|
||||||
withDropDownBorder={fromTableCell}
|
fromTableCell={fromTableCell}
|
||||||
/>
|
/>
|
||||||
) : isFieldAddress(fieldDefinition) ? (
|
) : isFieldAddress(fieldDefinition) ? (
|
||||||
<AddressFieldDisplay />
|
<AddressFieldDisplay />
|
||||||
|
|||||||
@ -1,8 +1,25 @@
|
|||||||
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
|
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
|
||||||
import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';
|
import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';
|
||||||
|
|
||||||
export const LinksFieldDisplay = () => {
|
type LinksFieldDisplayProps = {
|
||||||
|
isCellSoftFocused?: boolean;
|
||||||
|
cellElement?: HTMLElement;
|
||||||
|
fromTableCell?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LinksFieldDisplay = ({
|
||||||
|
isCellSoftFocused,
|
||||||
|
cellElement,
|
||||||
|
fromTableCell,
|
||||||
|
}: LinksFieldDisplayProps) => {
|
||||||
const { fieldValue } = useLinksField();
|
const { fieldValue } = useLinksField();
|
||||||
|
|
||||||
return <LinksDisplay value={fieldValue} />;
|
return (
|
||||||
|
<LinksDisplay
|
||||||
|
value={fieldValue}
|
||||||
|
anchorElement={cellElement}
|
||||||
|
isChipCountDisplayed={isCellSoftFocused}
|
||||||
|
withExpandedListBorder={fromTableCell}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
|
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
|
||||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||||
import {
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
ExpandableList,
|
|
||||||
ExpandableListProps,
|
type MultiSelectFieldDisplayProps = {
|
||||||
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
isCellSoftFocused?: boolean;
|
||||||
|
cellElement?: HTMLElement;
|
||||||
|
fromTableCell?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type MultiSelectFieldDisplayProps = ExpandableListProps;
|
|
||||||
export const MultiSelectFieldDisplay = ({
|
export const MultiSelectFieldDisplay = ({
|
||||||
isHovered,
|
isCellSoftFocused,
|
||||||
reference,
|
cellElement,
|
||||||
withDropDownBorder,
|
fromTableCell,
|
||||||
}: MultiSelectFieldDisplayProps) => {
|
}: MultiSelectFieldDisplayProps) => {
|
||||||
const { fieldValues, fieldDefinition } = useMultiSelectField();
|
const { fieldValues, fieldDefinition } = useMultiSelectField();
|
||||||
|
|
||||||
@ -19,11 +21,13 @@ export const MultiSelectFieldDisplay = ({
|
|||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return selectedOptions ? (
|
if (!selectedOptions) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
<ExpandableList
|
<ExpandableList
|
||||||
isHovered={isHovered}
|
anchorElement={cellElement}
|
||||||
reference={reference}
|
isChipCountDisplayed={isCellSoftFocused}
|
||||||
withDropDownBorder={withDropDownBorder}
|
withExpandedListBorder={fromTableCell}
|
||||||
>
|
>
|
||||||
{selectedOptions.map((selectedOption, index) => (
|
{selectedOptions.map((selectedOption, index) => (
|
||||||
<Tag
|
<Tag
|
||||||
@ -33,7 +37,5 @@ export const MultiSelectFieldDisplay = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ExpandableList>
|
</ExpandableList>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -99,7 +99,12 @@ export const RecordInlineCell = ({
|
|||||||
isReadOnly={readonly}
|
isReadOnly={readonly}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
displayModeContent={<FieldDisplay />}
|
displayModeContent={({ cellElement, isCellSoftFocused }) => (
|
||||||
|
<FieldDisplay
|
||||||
|
cellElement={cellElement}
|
||||||
|
isCellSoftFocused={isCellSoftFocused}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
isDisplayModeContentEmpty={isFieldEmpty}
|
isDisplayModeContentEmpty={isFieldEmpty}
|
||||||
isDisplayModeFixHeight
|
isDisplayModeFixHeight
|
||||||
editModeContentOnly={isFieldInputOnly}
|
editModeContentOnly={isFieldInputOnly}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||||
import { Tooltip } from 'react-tooltip';
|
import { Tooltip } from 'react-tooltip';
|
||||||
import { css, useTheme } from '@emotion/react';
|
import { css, useTheme } from '@emotion/react';
|
||||||
@ -7,7 +7,6 @@ import { IconComponent } from 'twenty-ui';
|
|||||||
|
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
||||||
import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
|
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
|
|
||||||
import { useInlineCell } from '../hooks/useInlineCell';
|
import { useInlineCell } from '../hooks/useInlineCell';
|
||||||
@ -111,7 +110,13 @@ type RecordInlineCellContainerProps = {
|
|||||||
buttonIcon?: IconComponent;
|
buttonIcon?: IconComponent;
|
||||||
editModeContent?: React.ReactNode;
|
editModeContent?: React.ReactNode;
|
||||||
editModeContentOnly?: boolean;
|
editModeContentOnly?: boolean;
|
||||||
displayModeContent: React.ReactNode;
|
displayModeContent: ({
|
||||||
|
isCellSoftFocused,
|
||||||
|
cellElement,
|
||||||
|
}: {
|
||||||
|
isCellSoftFocused: boolean;
|
||||||
|
cellElement?: HTMLDivElement;
|
||||||
|
}) => React.ReactNode;
|
||||||
customEditHotkeyScope?: HotkeyScope;
|
customEditHotkeyScope?: HotkeyScope;
|
||||||
isDisplayModeContentEmpty?: boolean;
|
isDisplayModeContentEmpty?: boolean;
|
||||||
isDisplayModeFixHeight?: boolean;
|
isDisplayModeFixHeight?: boolean;
|
||||||
@ -136,24 +141,25 @@ export const RecordInlineCellContainer = ({
|
|||||||
loading = false,
|
loading = false,
|
||||||
}: RecordInlineCellContainerProps) => {
|
}: RecordInlineCellContainerProps) => {
|
||||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||||
const reference = useRef<HTMLDivElement>(null);
|
// Used by some fields in ExpandableList as an anchor for the floating element.
|
||||||
|
// floating-ui mentions that `useState` must be used instead of `useRef`,
|
||||||
|
// see https://floating-ui.com/docs/useFloating#elements
|
||||||
|
const [cellElement, setCellElement] = useState<HTMLDivElement | null>(null);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [isHoveredForDisplayMode, setIsHoveredForDisplayMode] = useState(false);
|
const [isCellSoftFocused, setIsCellSoftFocused] = useState(false);
|
||||||
const [newDisplayModeContent, setNewDisplayModeContent] =
|
|
||||||
useState<React.ReactNode>(displayModeContent);
|
|
||||||
|
|
||||||
const handleContainerMouseEnter = () => {
|
const handleContainerMouseEnter = () => {
|
||||||
if (!readonly) {
|
if (!readonly) {
|
||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
}
|
}
|
||||||
setIsHoveredForDisplayMode(true);
|
setIsCellSoftFocused(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContainerMouseLeave = () => {
|
const handleContainerMouseLeave = () => {
|
||||||
if (!readonly) {
|
if (!readonly) {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
}
|
}
|
||||||
setIsHoveredForDisplayMode(false);
|
setIsCellSoftFocused(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
|
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
|
||||||
@ -174,17 +180,6 @@ export const RecordInlineCellContainer = ({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const labelId = `label-${entityId}-${fieldDefinition?.metadata?.fieldName}`;
|
const labelId = `label-${entityId}-${fieldDefinition?.metadata?.fieldName}`;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (React.isValidElement<ExpandableListProps>(displayModeContent)) {
|
|
||||||
setNewDisplayModeContent(
|
|
||||||
React.cloneElement(displayModeContent, {
|
|
||||||
isHovered: isHoveredForDisplayMode,
|
|
||||||
reference: reference.current || undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [isHoveredForDisplayMode, displayModeContent, reference]);
|
|
||||||
|
|
||||||
const showContent = () => {
|
const showContent = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <StyledInlineCellSkeletonLoader />;
|
return <StyledInlineCellSkeletonLoader />;
|
||||||
@ -215,7 +210,10 @@ export const RecordInlineCellContainer = ({
|
|||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
emptyPlaceholder={showLabel ? 'Empty' : label}
|
emptyPlaceholder={showLabel ? 'Empty' : label}
|
||||||
>
|
>
|
||||||
{newDisplayModeContent}
|
{displayModeContent({
|
||||||
|
isCellSoftFocused,
|
||||||
|
cellElement: cellElement ?? undefined,
|
||||||
|
})}
|
||||||
</RecordInlineCellDisplayMode>
|
</RecordInlineCellDisplayMode>
|
||||||
{showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
|
{showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
|
||||||
</StyledClickableContainer>
|
</StyledClickableContainer>
|
||||||
@ -252,7 +250,7 @@ export const RecordInlineCellContainer = ({
|
|||||||
)}
|
)}
|
||||||
</StyledLabelAndIconContainer>
|
</StyledLabelAndIconContainer>
|
||||||
)}
|
)}
|
||||||
<StyledValueContainer ref={reference}>
|
<StyledValueContainer ref={setCellElement}>
|
||||||
{showContent()}
|
{showContent()}
|
||||||
</StyledValueContainer>
|
</StyledValueContainer>
|
||||||
</StyledInlineCellBaseContainer>
|
</StyledInlineCellBaseContainer>
|
||||||
|
|||||||
@ -102,7 +102,13 @@ export const RecordTableCell = ({
|
|||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
nonEditModeContent={<FieldDisplay fromTableCell />}
|
nonEditModeContent={({ isCellSoftFocused, cellElement }) => (
|
||||||
|
<FieldDisplay
|
||||||
|
isCellSoftFocused={isCellSoftFocused}
|
||||||
|
cellElement={cellElement}
|
||||||
|
fromTableCell
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
import React, {
|
import React, { ReactElement, useContext, useState } from 'react';
|
||||||
ReactElement,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { IconArrowUpRight } from 'twenty-ui';
|
import { IconArrowUpRight } from 'twenty-ui';
|
||||||
@ -20,7 +14,6 @@ import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/rec
|
|||||||
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
|
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
|
||||||
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
|
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
|
||||||
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
|
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
|
||||||
import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
|
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
|
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
|
||||||
@ -58,7 +51,13 @@ const StyledCellBaseContainer = styled.div<{ softFocus: boolean }>`
|
|||||||
|
|
||||||
export type RecordTableCellContainerProps = {
|
export type RecordTableCellContainerProps = {
|
||||||
editModeContent: ReactElement;
|
editModeContent: ReactElement;
|
||||||
nonEditModeContent: ReactElement;
|
nonEditModeContent?: ({
|
||||||
|
isCellSoftFocused,
|
||||||
|
cellElement,
|
||||||
|
}: {
|
||||||
|
isCellSoftFocused: boolean;
|
||||||
|
cellElement?: HTMLTableCellElement;
|
||||||
|
}) => ReactElement;
|
||||||
editHotkeyScope?: HotkeyScope;
|
editHotkeyScope?: HotkeyScope;
|
||||||
transparent?: boolean;
|
transparent?: boolean;
|
||||||
maxContentWidth?: number;
|
maxContentWidth?: number;
|
||||||
@ -76,10 +75,14 @@ export const RecordTableCellContainer = ({
|
|||||||
editHotkeyScope,
|
editHotkeyScope,
|
||||||
}: RecordTableCellContainerProps) => {
|
}: RecordTableCellContainerProps) => {
|
||||||
const { columnIndex } = useContext(RecordTableCellContext);
|
const { columnIndex } = useContext(RecordTableCellContext);
|
||||||
const reference = useRef<HTMLTableCellElement>(null);
|
// Used by some fields in ExpandableList as an anchor for the floating element.
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
// floating-ui mentions that `useState` must be used instead of `useRef`,
|
||||||
const [newNonEditModeContent, setNewNonEditModeContent] =
|
// see https://floating-ui.com/docs/useFloating#elements
|
||||||
useState<ReactElement>(nonEditModeContent);
|
const [cellElement, setCellElement] = useState<HTMLTableCellElement | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [isCellBaseContainerHovered, setIsCellBaseContainerHovered] =
|
||||||
|
useState(false);
|
||||||
const { isReadOnly, isSelected, recordId } = useContext(
|
const { isReadOnly, isSelected, recordId } = useContext(
|
||||||
RecordTableRowContext,
|
RecordTableRowContext,
|
||||||
);
|
);
|
||||||
@ -127,13 +130,13 @@ export const RecordTableCellContainer = ({
|
|||||||
const handleContainerMouseEnter = () => {
|
const handleContainerMouseEnter = () => {
|
||||||
onCellMouseEnter({
|
onCellMouseEnter({
|
||||||
cellPosition,
|
cellPosition,
|
||||||
isHovered,
|
isHovered: isCellBaseContainerHovered,
|
||||||
setIsHovered,
|
setIsHovered: setIsCellBaseContainerHovered,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContainerMouseLeave = () => {
|
const handleContainerMouseLeave = () => {
|
||||||
setIsHovered(false);
|
setIsCellBaseContainerHovered(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const editModeContentOnly = useIsFieldInputOnly();
|
const editModeContentOnly = useIsFieldInputOnly();
|
||||||
@ -150,20 +153,9 @@ export const RecordTableCellContainer = ({
|
|||||||
(!isFirstColumn || !isEmpty) &&
|
(!isFirstColumn || !isEmpty) &&
|
||||||
!isReadOnly;
|
!isReadOnly;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (React.isValidElement<ExpandableListProps>(nonEditModeContent)) {
|
|
||||||
setNewNonEditModeContent(
|
|
||||||
React.cloneElement(nonEditModeContent, {
|
|
||||||
isHovered: showButton,
|
|
||||||
reference: reference.current || undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [nonEditModeContent, showButton, reference]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTd
|
<StyledTd
|
||||||
ref={reference}
|
ref={setCellElement}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
isInEditMode={isCurrentTableCellInEditMode}
|
isInEditMode={isCurrentTableCellInEditMode}
|
||||||
@ -181,7 +173,12 @@ export const RecordTableCellContainer = ({
|
|||||||
) : hasSoftFocus ? (
|
) : hasSoftFocus ? (
|
||||||
<>
|
<>
|
||||||
<RecordTableCellSoftFocusMode>
|
<RecordTableCellSoftFocusMode>
|
||||||
{editModeContentOnly ? editModeContent : newNonEditModeContent}
|
{editModeContentOnly
|
||||||
|
? editModeContent
|
||||||
|
: nonEditModeContent?.({
|
||||||
|
isCellSoftFocused: true,
|
||||||
|
cellElement: cellElement ?? undefined,
|
||||||
|
})}
|
||||||
</RecordTableCellSoftFocusMode>
|
</RecordTableCellSoftFocusMode>
|
||||||
{showButton && (
|
{showButton && (
|
||||||
<RecordTableCellButton
|
<RecordTableCellButton
|
||||||
@ -196,7 +193,10 @@ export const RecordTableCellContainer = ({
|
|||||||
<RecordTableCellDisplayMode>
|
<RecordTableCellDisplayMode>
|
||||||
{editModeContentOnly
|
{editModeContentOnly
|
||||||
? editModeContent
|
? editModeContent
|
||||||
: newNonEditModeContent}
|
: nonEditModeContent?.({
|
||||||
|
isCellSoftFocused: false,
|
||||||
|
cellElement: cellElement ?? undefined,
|
||||||
|
})}
|
||||||
</RecordTableCellDisplayMode>
|
</RecordTableCellDisplayMode>
|
||||||
)}
|
)}
|
||||||
{showButton && (
|
{showButton && (
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent, OverflowingTextWithTooltip } from 'twenty-ui';
|
||||||
|
|
||||||
import { ThemeColor } from '@/ui/theme/constants/MainColorNames';
|
import { ThemeColor } from '@/ui/theme/constants/MainColorNames';
|
||||||
import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema';
|
import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema';
|
||||||
@ -69,7 +69,9 @@ export const Tag = ({
|
|||||||
<Icon size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
|
<Icon size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
|
||||||
</StyledIconContainer>
|
</StyledIconContainer>
|
||||||
)}
|
)}
|
||||||
<StyledContent>{text}</StyledContent>
|
<StyledContent>
|
||||||
|
<OverflowingTextWithTooltip text={text} />
|
||||||
|
</StyledContent>
|
||||||
</StyledTag>
|
</StyledTag>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { MouseEventHandler, useMemo } from 'react';
|
import { MouseEventHandler, useMemo } from 'react';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
import {
|
||||||
|
ExpandableList,
|
||||||
|
ExpandableListProps,
|
||||||
|
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
|
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
|
||||||
import {
|
import {
|
||||||
LinkType,
|
LinkType,
|
||||||
@ -13,16 +15,19 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
||||||
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
||||||
|
|
||||||
const StyledContainer = styled(EllipsisDisplay)`
|
type LinksDisplayProps = Pick<
|
||||||
display: flex;
|
ExpandableListProps,
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
'anchorElement' | 'isChipCountDisplayed' | 'withExpandedListBorder'
|
||||||
`;
|
> & {
|
||||||
|
|
||||||
type LinksDisplayProps = {
|
|
||||||
value?: FieldLinksValue;
|
value?: FieldLinksValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LinksDisplay = ({ value }: LinksDisplayProps) => {
|
export const LinksDisplay = ({
|
||||||
|
anchorElement,
|
||||||
|
isChipCountDisplayed,
|
||||||
|
withExpandedListBorder,
|
||||||
|
value,
|
||||||
|
}: LinksDisplayProps) => {
|
||||||
const links = useMemo(
|
const links = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[
|
[
|
||||||
@ -49,7 +54,11 @@ export const LinksDisplay = ({ value }: LinksDisplayProps) => {
|
|||||||
const handleClick: MouseEventHandler = (event) => event.stopPropagation();
|
const handleClick: MouseEventHandler = (event) => event.stopPropagation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<ExpandableList
|
||||||
|
anchorElement={anchorElement}
|
||||||
|
isChipCountDisplayed={isChipCountDisplayed}
|
||||||
|
withExpandedListBorder={withExpandedListBorder}
|
||||||
|
>
|
||||||
{links.map(({ url, label, type }, index) =>
|
{links.map(({ url, label, type }, index) =>
|
||||||
type === LinkType.LinkedIn || type === LinkType.Twitter ? (
|
type === LinkType.LinkedIn || type === LinkType.Twitter ? (
|
||||||
<SocialLink key={index} href={url} onClick={handleClick} type={type}>
|
<SocialLink key={index} href={url} onClick={handleClick} type={type}>
|
||||||
@ -61,6 +70,6 @@ export const LinksDisplay = ({ value }: LinksDisplayProps) => {
|
|||||||
</RoundedLink>
|
</RoundedLink>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</StyledContainer>
|
</ExpandableList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
import { Dispatch, ReactElement, SetStateAction } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { ChildrenProperty } from '@/ui/layout/expandable-list/components/ExpandableList';
|
|
||||||
|
|
||||||
const StyledChildContainer = styled.div<{
|
|
||||||
shrink?: number;
|
|
||||||
isVisible?: boolean;
|
|
||||||
displayHiddenCount?: boolean;
|
|
||||||
}>`
|
|
||||||
display: ${({ isVisible = true }) => (isVisible ? 'flex' : 'none')};
|
|
||||||
flex-shrink: ${({ shrink = 1 }) => shrink};
|
|
||||||
overflow: ${({ displayHiddenCount }) =>
|
|
||||||
displayHiddenCount ? 'hidden' : 'none'};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledChildrenContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
export const ChildrenContainer = ({
|
|
||||||
children,
|
|
||||||
childrenProperties,
|
|
||||||
setChildrenWidths,
|
|
||||||
isFocusedMode,
|
|
||||||
}: {
|
|
||||||
children: ReactElement[];
|
|
||||||
childrenProperties: Record<number, ChildrenProperty>;
|
|
||||||
setChildrenWidths: Dispatch<SetStateAction<Record<number, number>>>;
|
|
||||||
isFocusedMode: boolean;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<StyledChildrenContainer>
|
|
||||||
{children.map((child, index) => {
|
|
||||||
return (
|
|
||||||
<StyledChildContainer
|
|
||||||
ref={(el) => {
|
|
||||||
if (!el || isFocusedMode) return;
|
|
||||||
setChildrenWidths((prevState) => {
|
|
||||||
prevState[index] = el.getBoundingClientRect().width;
|
|
||||||
return prevState;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
key={index}
|
|
||||||
displayHiddenCount={isFocusedMode}
|
|
||||||
isVisible={childrenProperties[index]?.isVisible}
|
|
||||||
shrink={childrenProperties[index]?.shrink}
|
|
||||||
>
|
|
||||||
{child}
|
|
||||||
</StyledChildContainer>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</StyledChildrenContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,48 +1,49 @@
|
|||||||
import { ReactElement, useEffect, useState } from 'react';
|
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { offset, useFloating } from '@floating-ui/react';
|
|
||||||
import { Chip, ChipVariant } from 'twenty-ui';
|
import { Chip, ChipVariant } from 'twenty-ui';
|
||||||
|
|
||||||
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
|
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown';
|
||||||
import { ChildrenContainer } from '@/ui/layout/expandable-list/components/ChildrenContainer';
|
import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement';
|
||||||
import { getChildrenProperties } from '@/ui/layout/expandable-list/utils/getChildProperties';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { getChipContentWidth } from '@/ui/layout/expandable-list/utils/getChipContentWidth';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const GAP_WIDTH = 4;
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
min-width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledRelationsListContainer = styled.div<{
|
const StyledChildrenContainer = styled.div`
|
||||||
withDropDownBorder?: boolean;
|
|
||||||
}>`
|
|
||||||
backdrop-filter: ${({ theme }) => theme.blur.strong};
|
|
||||||
background-color: ${({ theme }) => theme.background.secondary};
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
box-shadow: '0px 2px 4px ${({ theme }) =>
|
|
||||||
theme.boxShadow.light}, 2px 4px 16px ${({ theme }) =>
|
|
||||||
theme.boxShadow.strong}';
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
overflow: hidden;
|
||||||
outline: ${(props) =>
|
max-width: 100%;
|
||||||
props.withDropDownBorder
|
flex: 0 1 fit-content;
|
||||||
? `1px solid ${props.theme.font.color.extraLight}`
|
position: relative; // Needed so children elements compute their offsetLeft relatively to this element.
|
||||||
: 'none'};
|
`;
|
||||||
|
|
||||||
|
const StyledChildContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledChipCount = styled(Chip)`
|
||||||
|
flex-shrink: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type ExpandableListProps = {
|
export type ExpandableListProps = {
|
||||||
isHovered?: boolean;
|
anchorElement?: HTMLElement;
|
||||||
reference?: HTMLDivElement;
|
isChipCountDisplayed?: boolean;
|
||||||
forceDisplayHiddenCount?: boolean;
|
withExpandedListBorder?: boolean;
|
||||||
withDropDownBorder?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChildrenProperty = {
|
export type ChildrenProperty = {
|
||||||
@ -52,95 +53,127 @@ export type ChildrenProperty = {
|
|||||||
|
|
||||||
export const ExpandableList = ({
|
export const ExpandableList = ({
|
||||||
children,
|
children,
|
||||||
isHovered,
|
anchorElement,
|
||||||
reference,
|
isChipCountDisplayed: isChipCountDisplayedFromProps,
|
||||||
forceDisplayHiddenCount = false,
|
withExpandedListBorder = false,
|
||||||
withDropDownBorder = false,
|
|
||||||
}: {
|
}: {
|
||||||
children: ReactElement[];
|
children: ReactElement[];
|
||||||
} & ExpandableListProps) => {
|
} & ExpandableListProps) => {
|
||||||
const [containerWidth, setContainerWidth] = useState(0);
|
// isChipCountDisplayedInternal => uncontrolled display of the chip count.
|
||||||
const [isDropdownMenuOpen, setIsDropdownMenuOpen] = useState(false);
|
// isChipCountDisplayedFromProps => controlled display of the chip count.
|
||||||
const [childrenWidths, setChildrenWidths] = useState<Record<number, number>>(
|
// If isChipCountDisplayedFromProps is provided, isChipCountDisplayedInternal is not taken into account.
|
||||||
{},
|
const [isChipCountDisplayedInternal, setIsChipCountDisplayedInternal] =
|
||||||
|
useState(false);
|
||||||
|
const isChipCountDisplayed = isDefined(isChipCountDisplayedFromProps)
|
||||||
|
? isChipCountDisplayedFromProps
|
||||||
|
: isChipCountDisplayedInternal;
|
||||||
|
|
||||||
|
const [isListExpanded, setIsListExpanded] = useState(false);
|
||||||
|
|
||||||
|
// Used with floating-ui if anchorElement is not provided.
|
||||||
|
// floating-ui mentions that `useState` must be used instead of `useRef`
|
||||||
|
// @see https://floating-ui.com/docs/useFloating#elements
|
||||||
|
const [childrenContainerElement, setChildrenContainerElement] =
|
||||||
|
useState<HTMLDivElement | null>(null);
|
||||||
|
const [previousChildrenContainerWidth, setPreviousChildrenContainerWidth] =
|
||||||
|
useState(childrenContainerElement?.clientWidth ?? 0);
|
||||||
|
|
||||||
|
// Used with useListenClickOutside.
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [firstHiddenChildIndex, setFirstHiddenChildIndex] = useState(
|
||||||
|
children.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Because Chip width depends on the number of hidden children which depends on the Chip width, we have a circular dependency
|
const hiddenChildrenCount = children.length - firstHiddenChildIndex;
|
||||||
// To avoid it, we set the Chip width and make sure it can display its content (a number greater than 1)
|
const canDisplayChipCount = isChipCountDisplayed && hiddenChildrenCount > 0;
|
||||||
const chipContentWidth = getChipContentWidth(children.length);
|
|
||||||
const chipContainerWidth = chipContentWidth + 2 * GAP_WIDTH; // Because Chip component has 4px padding-left and right
|
|
||||||
const availableWidth = containerWidth - (chipContainerWidth + GAP_WIDTH); // Because there is a 4px gap between ChildrenContainer and ChipContainer
|
|
||||||
const isFocusedMode =
|
|
||||||
(isHovered || forceDisplayHiddenCount) &&
|
|
||||||
Object.values(childrenWidths).length > 0;
|
|
||||||
|
|
||||||
const childrenProperties = getChildrenProperties(
|
const handleChipCountClick = useCallback((event: React.MouseEvent) => {
|
||||||
isFocusedMode,
|
|
||||||
availableWidth,
|
|
||||||
childrenWidths,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hiddenChildrenCount = Object.values(childrenProperties).filter(
|
|
||||||
(childProperties) => !childProperties.isVisible,
|
|
||||||
).length;
|
|
||||||
|
|
||||||
const displayHiddenCountChip = isFocusedMode && hiddenChildrenCount > 0;
|
|
||||||
|
|
||||||
const { refs, floatingStyles } = useFloating({
|
|
||||||
// @ts-expect-error placement accepts 'start' as value even if the typing does not permit it
|
|
||||||
placement: 'start',
|
|
||||||
middleware: [offset({ mainAxis: -1, crossAxis: -1 })],
|
|
||||||
elements: { reference },
|
|
||||||
});
|
|
||||||
|
|
||||||
const openDropdownMenu = (event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDropdownMenuOpen(true);
|
setIsListExpanded(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
const resetFirstHiddenChildIndex = useCallback(() => {
|
||||||
|
setFirstHiddenChildIndex(children.length);
|
||||||
|
}, [children.length]);
|
||||||
|
|
||||||
|
// Recompute first hidden child when:
|
||||||
|
// - isChipCountDisplayed changes
|
||||||
|
// - children length changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isHovered) {
|
resetFirstHiddenChildIndex();
|
||||||
setIsDropdownMenuOpen(false);
|
}, [isChipCountDisplayed, children.length, resetFirstHiddenChildIndex]);
|
||||||
}
|
|
||||||
}, [isHovered]);
|
useListenClickOutside({
|
||||||
|
refs: [containerRef],
|
||||||
|
callback: () => {
|
||||||
|
// Handle container resize
|
||||||
|
if (
|
||||||
|
childrenContainerElement?.clientWidth !== previousChildrenContainerWidth
|
||||||
|
) {
|
||||||
|
resetFirstHiddenChildIndex();
|
||||||
|
setPreviousChildrenContainerWidth(
|
||||||
|
childrenContainerElement?.clientWidth ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer
|
<StyledContainer
|
||||||
ref={(el) => {
|
ref={containerRef}
|
||||||
if (!el) return;
|
onMouseEnter={
|
||||||
setContainerWidth(el.getBoundingClientRect().width);
|
isChipCountDisplayedFromProps
|
||||||
}}
|
? undefined
|
||||||
|
: () => setIsChipCountDisplayedInternal(true)
|
||||||
|
}
|
||||||
|
onMouseLeave={
|
||||||
|
isChipCountDisplayedFromProps
|
||||||
|
? undefined
|
||||||
|
: () => setIsChipCountDisplayedInternal(false)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ChildrenContainer
|
<StyledChildrenContainer ref={setChildrenContainerElement}>
|
||||||
childrenProperties={childrenProperties}
|
{children.slice(0, firstHiddenChildIndex).map((child, index) => (
|
||||||
setChildrenWidths={setChildrenWidths}
|
<StyledChildContainer
|
||||||
isFocusedMode={isFocusedMode}
|
key={index}
|
||||||
>
|
ref={(childElement) => {
|
||||||
{children}
|
if (
|
||||||
</ChildrenContainer>
|
// First element is always displayed.
|
||||||
{displayHiddenCountChip && (
|
index > 0 &&
|
||||||
|
isFirstOverflowingChildElement({
|
||||||
|
containerElement: childrenContainerElement,
|
||||||
|
childElement,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
setFirstHiddenChildIndex(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{child}
|
||||||
|
</StyledChildContainer>
|
||||||
|
))}
|
||||||
|
</StyledChildrenContainer>
|
||||||
|
{canDisplayChipCount && (
|
||||||
<AnimatedContainer>
|
<AnimatedContainer>
|
||||||
<Chip
|
<StyledChipCount
|
||||||
label={`+${hiddenChildrenCount}`}
|
label={`+${hiddenChildrenCount}`}
|
||||||
variant={ChipVariant.Highlighted}
|
variant={ChipVariant.Highlighted}
|
||||||
onClick={openDropdownMenu}
|
onClick={handleChipCountClick}
|
||||||
/>
|
/>
|
||||||
</AnimatedContainer>
|
</AnimatedContainer>
|
||||||
)}
|
)}
|
||||||
{isDropdownMenuOpen && (
|
{isListExpanded && (
|
||||||
<DropdownMenu
|
<ExpandedListDropdown
|
||||||
ref={refs.setFloating}
|
anchorElement={anchorElement ?? childrenContainerElement ?? undefined}
|
||||||
style={floatingStyles}
|
onClickOutside={() => {
|
||||||
width={
|
resetFirstHiddenChildIndex();
|
||||||
reference
|
setIsListExpanded(false);
|
||||||
? Math.max(220, reference.getBoundingClientRect().width)
|
}}
|
||||||
: undefined
|
withBorder={withExpandedListBorder}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<StyledRelationsListContainer withDropDownBorder={withDropDownBorder}>
|
{children}
|
||||||
{children}
|
</ExpandedListDropdown>
|
||||||
</StyledRelationsListContainer>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
)}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { offset, useFloating } from '@floating-ui/react';
|
||||||
|
|
||||||
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
|
||||||
|
type ExpandedListDropdownProps = {
|
||||||
|
anchorElement?: HTMLElement;
|
||||||
|
children: ReactNode;
|
||||||
|
onClickOutside?: () => void;
|
||||||
|
withBorder?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledExpandedListContainer = styled.div<{
|
||||||
|
withBorder?: boolean;
|
||||||
|
}>`
|
||||||
|
backdrop-filter: ${({ theme }) => theme.blur.strong};
|
||||||
|
background-color: ${({ theme }) => theme.background.secondary};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
box-shadow: ${({ theme }) =>
|
||||||
|
`0px 2px 4px ${theme.boxShadow.light}, 2px 4px 16px ${theme.boxShadow.strong}`};
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
|
${({ theme, withBorder }) =>
|
||||||
|
withBorder &&
|
||||||
|
css`
|
||||||
|
outline: 1px solid ${theme.font.color.extraLight};
|
||||||
|
`};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ExpandedListDropdown = ({
|
||||||
|
anchorElement,
|
||||||
|
children,
|
||||||
|
onClickOutside,
|
||||||
|
withBorder,
|
||||||
|
}: ExpandedListDropdownProps) => {
|
||||||
|
const { refs, floatingStyles } = useFloating({
|
||||||
|
// @ts-expect-error placement accepts 'start' as value even if the typing does not permit it
|
||||||
|
placement: 'start',
|
||||||
|
middleware: [offset({ mainAxis: -1, crossAxis: -1 })],
|
||||||
|
elements: { reference: anchorElement },
|
||||||
|
});
|
||||||
|
|
||||||
|
useListenClickOutside({
|
||||||
|
refs: [refs.floating],
|
||||||
|
callback: onClickOutside ?? (() => {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu
|
||||||
|
ref={refs.setFloating}
|
||||||
|
style={floatingStyles}
|
||||||
|
width={
|
||||||
|
anchorElement
|
||||||
|
? Math.max(220, anchorElement.getBoundingClientRect().width)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledExpandedListContainer withBorder={withBorder}>
|
||||||
|
{children}
|
||||||
|
</StyledExpandedListContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,13 +1,11 @@
|
|||||||
import { ReactElement, useRef, useState } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { userEvent, within } from '@storybook/test';
|
||||||
import { ComponentDecorator } from 'packages/twenty-ui';
|
import { ComponentDecorator } from 'packages/twenty-ui';
|
||||||
|
|
||||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||||
import {
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
ExpandableList,
|
|
||||||
ExpandableListProps,
|
|
||||||
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
|
||||||
import { MAIN_COLOR_NAMES } from '@/ui/theme/constants/MainColorNames';
|
import { MAIN_COLOR_NAMES } from '@/ui/theme/constants/MainColorNames';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -15,60 +13,55 @@ const StyledContainer = styled.div`
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type RenderProps = ExpandableListProps & {
|
|
||||||
children: ReactElement[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const Render = (args: RenderProps) => {
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
const reference = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer
|
|
||||||
ref={reference}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
>
|
|
||||||
<ExpandableList
|
|
||||||
reference={reference.current || undefined}
|
|
||||||
forceDisplayHiddenCount={args.forceDisplayHiddenCount}
|
|
||||||
withDropDownBorder={args.withDropDownBorder}
|
|
||||||
isHovered={isHovered}
|
|
||||||
>
|
|
||||||
{args.children}
|
|
||||||
</ExpandableList>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const meta: Meta<typeof ExpandableList> = {
|
const meta: Meta<typeof ExpandableList> = {
|
||||||
title: 'UI/Layout/ExpandableList/ExpandableList',
|
title: 'UI/Layout/ExpandableList/ExpandableList',
|
||||||
component: ExpandableList,
|
component: ExpandableList,
|
||||||
decorators: [ComponentDecorator],
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<StyledContainer>
|
||||||
|
<Story />
|
||||||
|
</StyledContainer>
|
||||||
|
),
|
||||||
|
ComponentDecorator,
|
||||||
|
],
|
||||||
args: {
|
args: {
|
||||||
children: [
|
children: Array.from({ length: 7 }, (_, index) => (
|
||||||
<Tag key={1} text={'Option 1'} color={MAIN_COLOR_NAMES[0]} />,
|
<Tag
|
||||||
<Tag key={2} text={'Option 2'} color={MAIN_COLOR_NAMES[1]} />,
|
key={index}
|
||||||
<Tag key={3} text={'Option 3'} color={MAIN_COLOR_NAMES[2]} />,
|
text={`Option ${index + 1}`}
|
||||||
<Tag key={4} text={'Option 4'} color={MAIN_COLOR_NAMES[3]} />,
|
color={MAIN_COLOR_NAMES[index]}
|
||||||
<Tag key={5} text={'Option 5'} color={MAIN_COLOR_NAMES[4]} />,
|
/>
|
||||||
<Tag key={6} text={'Option 6'} color={MAIN_COLOR_NAMES[5]} />,
|
)),
|
||||||
<Tag key={7} text={'Option 7'} color={MAIN_COLOR_NAMES[6]} />,
|
isChipCountDisplayed: false,
|
||||||
],
|
|
||||||
isHovered: undefined,
|
|
||||||
reference: undefined,
|
|
||||||
forceDisplayHiddenCount: false,
|
|
||||||
withDropDownBorder: false,
|
|
||||||
},
|
},
|
||||||
argTypes: {
|
argTypes: {
|
||||||
children: { control: false },
|
children: { control: false },
|
||||||
isHovered: { control: false },
|
anchorElement: { control: false },
|
||||||
reference: { control: false },
|
|
||||||
},
|
},
|
||||||
render: Render,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof ExpandableList>;
|
type Story = StoryObj<typeof ExpandableList>;
|
||||||
|
|
||||||
export const Default: Story = {};
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const WithChipCount: Story = {
|
||||||
|
args: { isChipCountDisplayed: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithExpandedList: Story = {
|
||||||
|
...WithChipCount,
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const chipCount = await canvas.findByText('+3');
|
||||||
|
|
||||||
|
await userEvent.click(chipCount);
|
||||||
|
|
||||||
|
expect(await canvas.findByText('Option 7')).toBeDefined();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithExpandedListBorder: Story = {
|
||||||
|
...WithExpandedList,
|
||||||
|
args: { ...WithExpandedList.args, withExpandedListBorder: true },
|
||||||
|
};
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
import { getChildrenProperties } from '@/ui/layout/expandable-list/utils/getChildProperties';
|
|
||||||
|
|
||||||
describe('getChildrenProperties', () => {
|
|
||||||
it('should return default value when isFocused is False', () => {
|
|
||||||
const isFocused = false;
|
|
||||||
const availableWidth = 100;
|
|
||||||
expect(getChildrenProperties(isFocused, availableWidth, {})).toEqual({});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
getChildrenProperties(isFocused, availableWidth, { 0: 40, 1: 40 }),
|
|
||||||
).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return proper value when isFocused is True', () => {
|
|
||||||
const isFocused = true;
|
|
||||||
const availableWidth = 100;
|
|
||||||
expect(getChildrenProperties(isFocused, availableWidth, {})).toEqual({});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
getChildrenProperties(isFocused, availableWidth, { 0: 40, 1: 40 }),
|
|
||||||
).toEqual({
|
|
||||||
0: { shrink: 0, isVisible: true },
|
|
||||||
1: { shrink: 0, isVisible: true },
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
getChildrenProperties(isFocused, availableWidth, {
|
|
||||||
0: 40,
|
|
||||||
1: 40,
|
|
||||||
2: 40,
|
|
||||||
3: 40,
|
|
||||||
4: 40,
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
0: { shrink: 0, isVisible: true },
|
|
||||||
1: { shrink: 0, isVisible: true },
|
|
||||||
2: { shrink: 1, isVisible: true },
|
|
||||||
3: { shrink: 1, isVisible: false },
|
|
||||||
4: { shrink: 1, isVisible: false },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { getChipContentWidth } from '@/ui/layout/expandable-list/utils/getChipContentWidth';
|
|
||||||
|
|
||||||
describe('getChipContentWidth', () => {
|
|
||||||
it('should return proper value', () => {
|
|
||||||
expect(getChipContentWidth(0)).toEqual(0);
|
|
||||||
expect(getChipContentWidth(1)).toEqual(0);
|
|
||||||
expect(getChipContentWidth(2)).toEqual(17);
|
|
||||||
expect(getChipContentWidth(20)).toEqual(25);
|
|
||||||
expect(getChipContentWidth(200)).toEqual(33);
|
|
||||||
expect(getChipContentWidth(2000)).toEqual(41);
|
|
||||||
expect(getChipContentWidth(20000)).toEqual(49);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import {
|
|
||||||
ChildrenProperty,
|
|
||||||
GAP_WIDTH,
|
|
||||||
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
|
||||||
|
|
||||||
export const getChildrenProperties = (
|
|
||||||
isFocusedMode: boolean,
|
|
||||||
availableWidth: number,
|
|
||||||
childrenWidths: Record<number, number>,
|
|
||||||
) => {
|
|
||||||
if (!isFocusedMode) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
let cumulatedChildrenWidth = 0;
|
|
||||||
const result: Record<number, ChildrenProperty> = {};
|
|
||||||
Object.values(childrenWidths).forEach((width, index) => {
|
|
||||||
// Because there is a 4px gap between children
|
|
||||||
const childWidth = width + GAP_WIDTH;
|
|
||||||
let shrink = 1;
|
|
||||||
let isVisible = true;
|
|
||||||
if (cumulatedChildrenWidth > availableWidth) {
|
|
||||||
isVisible = false;
|
|
||||||
} else if (cumulatedChildrenWidth + childWidth <= availableWidth) {
|
|
||||||
shrink = 0;
|
|
||||||
}
|
|
||||||
result[index] = { shrink, isVisible };
|
|
||||||
cumulatedChildrenWidth += childWidth;
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export const getChipContentWidth = (numberOfChildren: number) => {
|
|
||||||
if (numberOfChildren <= 1) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return 17 + 8 * Math.trunc(Math.log10(numberOfChildren));
|
|
||||||
};
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const isFirstOverflowingChildElement = ({
|
||||||
|
containerElement,
|
||||||
|
childElement,
|
||||||
|
}: {
|
||||||
|
containerElement: HTMLElement | null;
|
||||||
|
childElement: HTMLElement | null;
|
||||||
|
}) =>
|
||||||
|
isDefined(containerElement) &&
|
||||||
|
isDefined(childElement) &&
|
||||||
|
// First element is always displayed.
|
||||||
|
isDefined(childElement.previousElementSibling) &&
|
||||||
|
containerElement.scrollWidth > containerElement.clientWidth &&
|
||||||
|
childElement.offsetLeft > containerElement.clientWidth &&
|
||||||
|
(childElement.previousElementSibling as HTMLElement).offsetLeft <
|
||||||
|
containerElement.clientWidth;
|
||||||
@ -10,22 +10,15 @@ type RoundedLinkProps = {
|
|||||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledClickable = styled.div`
|
const StyledLink = styled(ReactLink)`
|
||||||
overflow: hidden;
|
max-width: 100%;
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
overflow: hidden;
|
|
||||||
text-decoration: none;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledChip = styled(Chip)`
|
const StyledChip = styled(Chip)`
|
||||||
border-color: ${({ theme }) => theme.border.color.strong};
|
border-color: ${({ theme }) => theme.border.color.strong};
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
max-width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RoundedLink = ({
|
export const RoundedLink = ({
|
||||||
@ -33,20 +26,21 @@ export const RoundedLink = ({
|
|||||||
className,
|
className,
|
||||||
href,
|
href,
|
||||||
onClick,
|
onClick,
|
||||||
}: RoundedLinkProps) => (
|
}: RoundedLinkProps) => {
|
||||||
<div>
|
if (!children) return null;
|
||||||
{children !== '' ? (
|
|
||||||
<StyledClickable className={className}>
|
return (
|
||||||
<ReactLink target="_blank" to={href} onClick={onClick}>
|
<StyledLink
|
||||||
<StyledChip
|
className={className}
|
||||||
label={`${children}`}
|
target="_blank"
|
||||||
variant={ChipVariant.Rounded}
|
to={href}
|
||||||
size={ChipSize.Small}
|
onClick={onClick}
|
||||||
/>
|
>
|
||||||
</ReactLink>
|
<StyledChip
|
||||||
</StyledClickable>
|
label={`${children}`}
|
||||||
) : (
|
variant={ChipVariant.Rounded}
|
||||||
<></>
|
size={ChipSize.Small}
|
||||||
)}
|
/>
|
||||||
</div>
|
</StyledLink>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
@ -29,22 +29,13 @@ export const OverflowingTextWithTooltip = ({
|
|||||||
mutliline?: boolean;
|
mutliline?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const textElementId = `title-id-${uuidV4()}`;
|
const textElementId = `title-id-${uuidV4()}`;
|
||||||
|
const [textElement, setTextElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const textRef = useRef<HTMLDivElement>(null);
|
const isTitleOverflowing =
|
||||||
|
(text?.length ?? 0) > 0 &&
|
||||||
const [isTitleOverflowing, setIsTitleOverflowing] = useState(false);
|
!!textElement &&
|
||||||
|
(textElement.scrollHeight > textElement.clientHeight ||
|
||||||
useEffect(() => {
|
textElement.scrollWidth > textElement.clientWidth);
|
||||||
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]);
|
|
||||||
|
|
||||||
const handleTooltipClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
const handleTooltipClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@ -56,7 +47,7 @@ export const OverflowingTextWithTooltip = ({
|
|||||||
<StyledOverflowingText
|
<StyledOverflowingText
|
||||||
data-testid="tooltip"
|
data-testid="tooltip"
|
||||||
className={className}
|
className={className}
|
||||||
ref={textRef}
|
ref={setTextElement}
|
||||||
id={textElementId}
|
id={textElementId}
|
||||||
cursorPointer={isTitleOverflowing}
|
cursorPointer={isTitleOverflowing}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user