import { useMemo } from 'react'; import { css } from '@patternfly/react-styles'; import styles from '../../../css/topology-components'; import { truncateMiddle } from '../../../utils/truncate-middle'; import { createSvgIdUrl, useCombineRefs, useHover, useSize } from '../../../utils'; import { WithContextMenuProps, WithDndDragProps } from '../../../behavior'; import NodeShadows, { NODE_SHADOW_FILTER_ID_DANGER, NODE_SHADOW_FILTER_ID_HOVER } from '../NodeShadows'; import LabelBadge from './LabelBadge'; import LabelContextMenu from './LabelContextMenu'; import LabelIcon from './LabelIcon'; import LabelActionIcon from './LabelActionIcon'; import { BadgeLocation, LabelPosition, Node, NodeStatus } from '../../../types'; export type NodeLabelProps = { element?: Node; children?: string; className?: string; paddingX?: number; paddingY?: number; x?: number; y?: number; position?: LabelPosition; centerLabelOnEdge?: boolean; boxRef?: React.Ref; cornerRadius?: number; status?: NodeStatus; secondaryLabel?: string; truncateLength?: number; // Defaults to 13 labelIconClass?: string; // Icon to show in label labelIcon?: React.ReactNode; labelIconPadding?: number; dragRef?: WithDndDragProps['dndDragRef']; hover?: boolean; dragging?: boolean; edgeDragging?: boolean; dropTarget?: boolean; actionIcon?: React.ReactElement; actionIconClassName?: string; onActionIconClick?: (e: React.MouseEvent) => void; badge?: string; badgeColor?: string; badgeTextColor?: string; badgeBorderColor?: string; badgeClassName?: string; badgeLocation?: BadgeLocation; hideContextMenuKebab?: boolean; } & Partial; /** * Renders a `` component with a `` box behind. */ const NodeLabel: React.FunctionComponent = ({ children, className, paddingX = 0, paddingY = 0, cornerRadius = 4, x = 0, y = 0, position = LabelPosition.bottom, centerLabelOnEdge, secondaryLabel, status, badge, badgeColor, badgeTextColor, badgeBorderColor, badgeClassName, badgeLocation = BadgeLocation.inner, labelIconClass, labelIcon, labelIconPadding = 4, truncateLength, dragRef, hover, dragging, edgeDragging, dropTarget, onContextMenu, contextMenuOpen, hideContextMenuKebab, actionIcon, actionIconClassName, onActionIconClick, boxRef, ...other }) => { const [labelHover, labelHoverRef] = useHover(); const refs = useCombineRefs(dragRef, typeof truncateLength === 'number' ? labelHoverRef : undefined); const [textSize, textRef] = useSize([children, truncateLength, className, labelHover, contextMenuOpen]); const [secondaryTextSize, secondaryTextRef] = useSize([secondaryLabel, truncateLength, className, labelHover]); const [badgeSize, badgeRef] = useSize([badge]); const [actionSize, actionRef] = useSize([actionIcon, paddingX]); const [contextSize, contextRef] = useSize([onContextMenu, paddingX]); const { width, height, backgroundHeight, startX, startY, badgeStartX, badgeStartY, actionStartX, contextStartX, iconSpace, badgeSpace } = useMemo(() => { if (!textSize) { return { width: 0, height: 0, backgroundHeight: 0, startX: 0, startY: 0, badgeStartX: 0, badgeStartY: 0, actionStartX: 0, contextStartX: 0, iconSpace: 0, badgeSpace: 0 }; } const badgeSpace = badge && badgeSize && badgeLocation === BadgeLocation.inner ? badgeSize.width + paddingX : 0; const height = Math.max(textSize.height, badgeSize?.height ?? 0) + paddingY * 2; const iconSpace = labelIconClass || labelIcon ? (height + paddingY * 0.5) / 2 : 0; const actionSpace = actionIcon && actionSize ? actionSize.width : 0; const contextSpace = !hideContextMenuKebab && onContextMenu && contextSize ? contextSize.width : 0; const primaryWidth = iconSpace + badgeSpace + paddingX + textSize.width + actionSpace + contextSpace + paddingX; const secondaryWidth = secondaryLabel && secondaryTextSize ? secondaryTextSize.width + 2 * paddingX : 0; const width = Math.max(primaryWidth, secondaryWidth); const backgroundHeight = height + (secondaryLabel && secondaryTextSize ? secondaryTextSize.height + paddingY * 2 : 0); let startX: number; let startY: number; if (position === LabelPosition.top) { startX = x - width / 2; startY = -y - height - (centerLabelOnEdge ? -backgroundHeight / 2 : paddingY); } else if (position === LabelPosition.right) { startX = x + iconSpace - (centerLabelOnEdge ? width / 2 : 0); startY = y - height / 2; } else if (position === LabelPosition.left) { startX = centerLabelOnEdge ? x - width / 2 : -width - paddingX; startY = y - height / 2; } else { startX = x - width / 2 + iconSpace / 2; startY = y - (centerLabelOnEdge ? backgroundHeight / 2 : 0); } const actionStartX = iconSpace + badgeSpace + paddingX + textSize.width + paddingX; const contextStartX = actionStartX + actionSpace; let badgeStartX = 0; let badgeStartY = 0; if (badgeSize) { if (badgeLocation === BadgeLocation.below) { badgeStartX = (width - badgeSize.width) / 2; badgeStartY = height + paddingY; } else { badgeStartX = iconSpace + paddingX; badgeStartY = (height - badgeSize.height) / 2; } } return { width, height, backgroundHeight, startX, startY, actionStartX, contextStartX, badgeStartX, badgeStartY, iconSpace, badgeSpace: badgeSize && badgeLocation === BadgeLocation.inner ? badgeSpace : 0 }; }, [ textSize, badge, badgeSize, badgeLocation, paddingX, paddingY, labelIconClass, labelIcon, actionIcon, actionSize, hideContextMenuKebab, onContextMenu, contextSize, secondaryLabel, secondaryTextSize, centerLabelOnEdge, position, x, y ]); let filterId; if (status === 'danger') { filterId = NODE_SHADOW_FILTER_ID_DANGER; } else if (hover || dragging || edgeDragging || dropTarget) { filterId = NODE_SHADOW_FILTER_ID_HOVER; } return ( {textSize && ( )} {textSize && badge && ( )} {textSize && secondaryLabel && ( <> {truncateLength > 0 && !labelHover ? truncateMiddle(secondaryLabel, { length: truncateLength }) : secondaryLabel} )} {textSize && (labelIconClass || labelIcon) && ( )} {truncateLength > 0 && !labelHover && !contextMenuOpen ? truncateMiddle(children, { length: truncateLength }) : children} {textSize && actionIcon && ( <> )} {textSize && onContextMenu && !hideContextMenuKebab && ( <> )} ); }; export default NodeLabel;