Skip to content

Commit 3399045

Browse files
authored
feat(TextItem): add component (#911)
1 parent 142ecdb commit 3399045

File tree

11 files changed

+909
-221
lines changed

11 files changed

+909
-221
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@cube-dev/ui-kit": minor
3+
---
4+
5+
Added `TextItem` component for displaying text with automatic overflow handling and tooltips. Features include:
6+
- Auto-tooltip on text overflow (enabled by default)
7+
- Text highlighting with `highlight` prop for search results
8+
- Customizable highlight styles via `highlightStyles` prop
9+
- Case-sensitive/insensitive highlight matching
10+
- Inherits all `Text` component props
11+
12+
Added `Text.Highlight` sub-component for semantic text highlighting (uses `<mark>` element).
13+
14+
**Breaking:** Removed `Text.Selection` in favor of `Text.Highlight`.
15+

src/components/content/Item/Item.tsx

Lines changed: 2 additions & 209 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@ import {
77
PointerEvent,
88
ReactNode,
99
RefObject,
10-
useCallback,
11-
useEffect,
1210
useMemo,
13-
useRef,
14-
useState,
1511
} from 'react';
1612
import { OverlayProps } from 'react-aria';
1713
import { useHotkeys } from 'react-hotkeys-hook';
@@ -60,12 +56,10 @@ import {
6056
import { mergeProps } from '../../../utils/react';
6157
import { ItemAction } from '../../actions/ItemAction';
6258
import { ItemActionProvider } from '../../actions/ItemActionContext';
63-
import {
64-
CubeTooltipProviderProps,
65-
TooltipProvider,
66-
} from '../../overlays/Tooltip/TooltipProvider';
59+
import { CubeTooltipProviderProps } from '../../overlays/Tooltip/TooltipProvider';
6760
import { HotKeys } from '../HotKeys';
6861
import { ItemBadge } from '../ItemBadge';
62+
import { useAutoTooltip } from '../use-auto-tooltip';
6963

7064
export interface CubeItemProps extends BaseProps, ContainerStyleProps {
7165
icon?: ReactNode | 'checkbox';
@@ -454,207 +448,6 @@ const ItemElement = tasty({
454448
styleProps: CONTAINER_STYLES,
455449
});
456450

457-
export function useAutoTooltip({
458-
tooltip,
459-
children,
460-
labelProps,
461-
isDynamicLabel = false, // if actions are set
462-
}: {
463-
tooltip: CubeItemProps['tooltip'];
464-
children: ReactNode;
465-
labelProps?: Props;
466-
isDynamicLabel?: boolean;
467-
}) {
468-
// Determine if auto tooltip is enabled
469-
// Auto tooltip only works when children is a string (overflow detection needs text)
470-
const isAutoTooltipEnabled = useMemo(() => {
471-
if (typeof children !== 'string') return false;
472-
473-
// Boolean true enables auto overflow detection
474-
if (tooltip === true) return true;
475-
if (typeof tooltip === 'object') {
476-
// If title is provided and auto is explicitly true, enable auto overflow detection
477-
if (tooltip.title) {
478-
return tooltip.auto === true;
479-
}
480-
481-
// If no title is provided, default to auto=true unless explicitly disabled
482-
const autoValue = tooltip.auto !== undefined ? tooltip.auto : true;
483-
return !!autoValue;
484-
}
485-
return false;
486-
}, [tooltip, children]);
487-
488-
// Track label overflow for auto tooltip (only when enabled)
489-
const externalLabelRef = (labelProps as any)?.ref;
490-
const [isLabelOverflowed, setIsLabelOverflowed] = useState(false);
491-
const elementRef = useRef<HTMLElement | null>(null);
492-
const resizeObserverRef = useRef<ResizeObserver | null>(null);
493-
494-
const checkLabelOverflow = useCallback(() => {
495-
const label = elementRef.current;
496-
if (!label) {
497-
setIsLabelOverflowed(false);
498-
return;
499-
}
500-
501-
const hasOverflow = label.scrollWidth > label.clientWidth;
502-
setIsLabelOverflowed(hasOverflow);
503-
}, []);
504-
505-
useEffect(() => {
506-
if (isAutoTooltipEnabled) {
507-
checkLabelOverflow();
508-
}
509-
}, [isAutoTooltipEnabled, checkLabelOverflow]);
510-
511-
// Attach ResizeObserver via callback ref to handle DOM node changes
512-
const handleLabelElementRef = useCallback(
513-
(element: HTMLElement | null) => {
514-
// Call external callback ref to notify external refs
515-
if (externalLabelRef) {
516-
if (typeof externalLabelRef === 'function') {
517-
externalLabelRef(element);
518-
} else {
519-
(externalLabelRef as any).current = element;
520-
}
521-
}
522-
523-
// Disconnect previous observer
524-
if (resizeObserverRef.current) {
525-
try {
526-
resizeObserverRef.current.disconnect();
527-
} catch {
528-
// do nothing
529-
}
530-
resizeObserverRef.current = null;
531-
}
532-
533-
elementRef.current = element;
534-
535-
if (element && isAutoTooltipEnabled) {
536-
// Create a fresh observer to capture the latest callback
537-
const obs = new ResizeObserver(() => {
538-
checkLabelOverflow();
539-
});
540-
resizeObserverRef.current = obs;
541-
obs.observe(element);
542-
// Initial check
543-
checkLabelOverflow();
544-
} else {
545-
setIsLabelOverflowed(false);
546-
}
547-
},
548-
[externalLabelRef, isAutoTooltipEnabled, checkLabelOverflow],
549-
);
550-
551-
// Cleanup on unmount
552-
useEffect(() => {
553-
return () => {
554-
if (resizeObserverRef.current) {
555-
try {
556-
resizeObserverRef.current.disconnect();
557-
} catch {
558-
// do nothing
559-
}
560-
resizeObserverRef.current = null;
561-
}
562-
elementRef.current = null;
563-
};
564-
}, []);
565-
566-
const finalLabelProps = useMemo(() => {
567-
const props = {
568-
...(labelProps || {}),
569-
};
570-
571-
delete props.ref;
572-
573-
return props;
574-
}, [labelProps]);
575-
576-
const renderWithTooltip = (
577-
renderElement: (
578-
tooltipTriggerProps?: HTMLAttributes<HTMLElement>,
579-
tooltipRef?: RefObject<HTMLElement>,
580-
) => ReactNode,
581-
defaultTooltipPlacement: OverlayProps['placement'],
582-
) => {
583-
// Handle tooltip rendering based on tooltip prop type
584-
if (tooltip) {
585-
// String tooltip - simple case
586-
if (typeof tooltip === 'string') {
587-
return (
588-
<TooltipProvider placement={defaultTooltipPlacement} title={tooltip}>
589-
{(triggerProps, ref) => renderElement(triggerProps, ref)}
590-
</TooltipProvider>
591-
);
592-
}
593-
594-
// Boolean tooltip - auto tooltip on overflow
595-
if (tooltip === true) {
596-
if ((children || labelProps) && (isLabelOverflowed || isDynamicLabel)) {
597-
return (
598-
<TooltipProvider
599-
placement={defaultTooltipPlacement}
600-
title={children}
601-
isDisabled={!isLabelOverflowed && isDynamicLabel}
602-
>
603-
{(triggerProps, ref) => renderElement(triggerProps, ref)}
604-
</TooltipProvider>
605-
);
606-
}
607-
}
608-
609-
// Object tooltip - advanced configuration
610-
if (typeof tooltip === 'object') {
611-
const { auto, ...tooltipProps } = tooltip;
612-
613-
// If title is provided and auto is not explicitly true, always show the tooltip
614-
if (tooltipProps.title && auto !== true) {
615-
return (
616-
<TooltipProvider
617-
placement={defaultTooltipPlacement}
618-
{...tooltipProps}
619-
>
620-
{(triggerProps, ref) => renderElement(triggerProps, ref)}
621-
</TooltipProvider>
622-
);
623-
}
624-
625-
// If title is provided with auto=true, OR no title but auto behavior enabled
626-
if ((children || labelProps) && (isLabelOverflowed || isDynamicLabel)) {
627-
return (
628-
<TooltipProvider
629-
placement={defaultTooltipPlacement}
630-
title={tooltipProps.title ?? children}
631-
isDisabled={
632-
!isLabelOverflowed &&
633-
isDynamicLabel &&
634-
tooltipProps.isDisabled !== true
635-
}
636-
{...tooltipProps}
637-
>
638-
{(triggerProps, ref) => renderElement(triggerProps, ref)}
639-
</TooltipProvider>
640-
);
641-
}
642-
}
643-
}
644-
645-
return renderElement();
646-
};
647-
648-
return {
649-
labelRef: handleLabelElementRef,
650-
labelProps: finalLabelProps,
651-
isLabelOverflowed,
652-
isAutoTooltipEnabled,
653-
hasTooltip: !!tooltip,
654-
renderWithTooltip,
655-
};
656-
}
657-
658451
const Item = <T extends HTMLElement = HTMLDivElement>(
659452
props: CubeItemProps,
660453
ref: ForwardedRef<T>,

src/components/content/Text.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,17 @@ const EmphasisText = tasty(Text, {
133133
preset: 'em',
134134
});
135135

136-
const SelectionText = tasty(Text, {
136+
const PlaceholderText = tasty(Text, {
137137
styles: {
138-
color: '#dark',
139-
fill: '#note.30',
138+
color: '#current.5',
140139
},
141140
});
142141

143-
const PlaceholderText = tasty(Text, {
142+
const HighlightText = tasty(Text, {
143+
as: 'mark',
144144
styles: {
145-
color: '#current.5',
145+
fill: '#dark.15',
146+
color: '#dark',
146147
},
147148
});
148149

@@ -155,8 +156,8 @@ export interface TextComponent
155156
Success: typeof SuccessText;
156157
Strong: typeof StrongText;
157158
Emphasis: typeof EmphasisText;
158-
Selection: typeof SelectionText;
159159
Placeholder: typeof PlaceholderText;
160+
Highlight: typeof HighlightText;
160161
}
161162

162163
const _Text: TextComponent = Object.assign(Text, {
@@ -165,8 +166,8 @@ const _Text: TextComponent = Object.assign(Text, {
165166
Success: SuccessText,
166167
Strong: StrongText,
167168
Emphasis: EmphasisText,
168-
Selection: SelectionText,
169169
Placeholder: PlaceholderText,
170+
Highlight: HighlightText,
170171
});
171172

172173
_Text.displayName = 'Text';

0 commit comments

Comments
 (0)