diff --git a/.changeset/add-text-item-component.md b/.changeset/add-text-item-component.md new file mode 100644 index 000000000..eb0f62b29 --- /dev/null +++ b/.changeset/add-text-item-component.md @@ -0,0 +1,15 @@ +--- +"@cube-dev/ui-kit": minor +--- + +Added `TextItem` component for displaying text with automatic overflow handling and tooltips. Features include: +- Auto-tooltip on text overflow (enabled by default) +- Text highlighting with `highlight` prop for search results +- Customizable highlight styles via `highlightStyles` prop +- Case-sensitive/insensitive highlight matching +- Inherits all `Text` component props + +Added `Text.Highlight` sub-component for semantic text highlighting (uses `` element). + +**Breaking:** Removed `Text.Selection` in favor of `Text.Highlight`. + diff --git a/src/components/content/Item/Item.tsx b/src/components/content/Item/Item.tsx index 7cbfab538..1a305f724 100644 --- a/src/components/content/Item/Item.tsx +++ b/src/components/content/Item/Item.tsx @@ -7,11 +7,7 @@ import { PointerEvent, ReactNode, RefObject, - useCallback, - useEffect, useMemo, - useRef, - useState, } from 'react'; import { OverlayProps } from 'react-aria'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -60,12 +56,10 @@ import { import { mergeProps } from '../../../utils/react'; import { ItemAction } from '../../actions/ItemAction'; import { ItemActionProvider } from '../../actions/ItemActionContext'; -import { - CubeTooltipProviderProps, - TooltipProvider, -} from '../../overlays/Tooltip/TooltipProvider'; +import { CubeTooltipProviderProps } from '../../overlays/Tooltip/TooltipProvider'; import { HotKeys } from '../HotKeys'; import { ItemBadge } from '../ItemBadge'; +import { useAutoTooltip } from '../use-auto-tooltip'; export interface CubeItemProps extends BaseProps, ContainerStyleProps { icon?: ReactNode | 'checkbox'; @@ -454,207 +448,6 @@ const ItemElement = tasty({ styleProps: CONTAINER_STYLES, }); -export function useAutoTooltip({ - tooltip, - children, - labelProps, - isDynamicLabel = false, // if actions are set -}: { - tooltip: CubeItemProps['tooltip']; - children: ReactNode; - labelProps?: Props; - isDynamicLabel?: boolean; -}) { - // Determine if auto tooltip is enabled - // Auto tooltip only works when children is a string (overflow detection needs text) - const isAutoTooltipEnabled = useMemo(() => { - if (typeof children !== 'string') return false; - - // Boolean true enables auto overflow detection - if (tooltip === true) return true; - if (typeof tooltip === 'object') { - // If title is provided and auto is explicitly true, enable auto overflow detection - if (tooltip.title) { - return tooltip.auto === true; - } - - // If no title is provided, default to auto=true unless explicitly disabled - const autoValue = tooltip.auto !== undefined ? tooltip.auto : true; - return !!autoValue; - } - return false; - }, [tooltip, children]); - - // Track label overflow for auto tooltip (only when enabled) - const externalLabelRef = (labelProps as any)?.ref; - const [isLabelOverflowed, setIsLabelOverflowed] = useState(false); - const elementRef = useRef(null); - const resizeObserverRef = useRef(null); - - const checkLabelOverflow = useCallback(() => { - const label = elementRef.current; - if (!label) { - setIsLabelOverflowed(false); - return; - } - - const hasOverflow = label.scrollWidth > label.clientWidth; - setIsLabelOverflowed(hasOverflow); - }, []); - - useEffect(() => { - if (isAutoTooltipEnabled) { - checkLabelOverflow(); - } - }, [isAutoTooltipEnabled, checkLabelOverflow]); - - // Attach ResizeObserver via callback ref to handle DOM node changes - const handleLabelElementRef = useCallback( - (element: HTMLElement | null) => { - // Call external callback ref to notify external refs - if (externalLabelRef) { - if (typeof externalLabelRef === 'function') { - externalLabelRef(element); - } else { - (externalLabelRef as any).current = element; - } - } - - // Disconnect previous observer - if (resizeObserverRef.current) { - try { - resizeObserverRef.current.disconnect(); - } catch { - // do nothing - } - resizeObserverRef.current = null; - } - - elementRef.current = element; - - if (element && isAutoTooltipEnabled) { - // Create a fresh observer to capture the latest callback - const obs = new ResizeObserver(() => { - checkLabelOverflow(); - }); - resizeObserverRef.current = obs; - obs.observe(element); - // Initial check - checkLabelOverflow(); - } else { - setIsLabelOverflowed(false); - } - }, - [externalLabelRef, isAutoTooltipEnabled, checkLabelOverflow], - ); - - // Cleanup on unmount - useEffect(() => { - return () => { - if (resizeObserverRef.current) { - try { - resizeObserverRef.current.disconnect(); - } catch { - // do nothing - } - resizeObserverRef.current = null; - } - elementRef.current = null; - }; - }, []); - - const finalLabelProps = useMemo(() => { - const props = { - ...(labelProps || {}), - }; - - delete props.ref; - - return props; - }, [labelProps]); - - const renderWithTooltip = ( - renderElement: ( - tooltipTriggerProps?: HTMLAttributes, - tooltipRef?: RefObject, - ) => ReactNode, - defaultTooltipPlacement: OverlayProps['placement'], - ) => { - // Handle tooltip rendering based on tooltip prop type - if (tooltip) { - // String tooltip - simple case - if (typeof tooltip === 'string') { - return ( - - {(triggerProps, ref) => renderElement(triggerProps, ref)} - - ); - } - - // Boolean tooltip - auto tooltip on overflow - if (tooltip === true) { - if ((children || labelProps) && (isLabelOverflowed || isDynamicLabel)) { - return ( - - {(triggerProps, ref) => renderElement(triggerProps, ref)} - - ); - } - } - - // Object tooltip - advanced configuration - if (typeof tooltip === 'object') { - const { auto, ...tooltipProps } = tooltip; - - // If title is provided and auto is not explicitly true, always show the tooltip - if (tooltipProps.title && auto !== true) { - return ( - - {(triggerProps, ref) => renderElement(triggerProps, ref)} - - ); - } - - // If title is provided with auto=true, OR no title but auto behavior enabled - if ((children || labelProps) && (isLabelOverflowed || isDynamicLabel)) { - return ( - - {(triggerProps, ref) => renderElement(triggerProps, ref)} - - ); - } - } - } - - return renderElement(); - }; - - return { - labelRef: handleLabelElementRef, - labelProps: finalLabelProps, - isLabelOverflowed, - isAutoTooltipEnabled, - hasTooltip: !!tooltip, - renderWithTooltip, - }; -} - const Item = ( props: CubeItemProps, ref: ForwardedRef, diff --git a/src/components/content/Text.tsx b/src/components/content/Text.tsx index 18938d0ad..d63632d96 100644 --- a/src/components/content/Text.tsx +++ b/src/components/content/Text.tsx @@ -133,16 +133,17 @@ const EmphasisText = tasty(Text, { preset: 'em', }); -const SelectionText = tasty(Text, { +const PlaceholderText = tasty(Text, { styles: { - color: '#dark', - fill: '#note.30', + color: '#current.5', }, }); -const PlaceholderText = tasty(Text, { +const HighlightText = tasty(Text, { + as: 'mark', styles: { - color: '#current.5', + fill: '#dark.15', + color: '#dark', }, }); @@ -155,8 +156,8 @@ export interface TextComponent Success: typeof SuccessText; Strong: typeof StrongText; Emphasis: typeof EmphasisText; - Selection: typeof SelectionText; Placeholder: typeof PlaceholderText; + Highlight: typeof HighlightText; } const _Text: TextComponent = Object.assign(Text, { @@ -165,8 +166,8 @@ const _Text: TextComponent = Object.assign(Text, { Success: SuccessText, Strong: StrongText, Emphasis: EmphasisText, - Selection: SelectionText, Placeholder: PlaceholderText, + Highlight: HighlightText, }); _Text.displayName = 'Text'; diff --git a/src/components/content/TextItem/TextItem.docs.mdx b/src/components/content/TextItem/TextItem.docs.mdx new file mode 100644 index 000000000..8e9e286b5 --- /dev/null +++ b/src/components/content/TextItem/TextItem.docs.mdx @@ -0,0 +1,239 @@ +import { Meta, Canvas, Story, Controls } from '@storybook/addon-docs/blocks'; +import { TextItem } from './TextItem'; +import * as TextItemStories from './TextItem.stories'; + + + +# TextItem + +A text component with built-in overflow handling and auto-tooltips. When text is truncated, a tooltip automatically appears on hover showing the full content. Also supports text highlighting for search results. + +## When to Use + +- Display text that may overflow its container with automatic tooltip fallback +- Show search results with highlighted matching terms +- Labels or descriptions in constrained layouts (sidebars, tables, lists) +- Any text that needs ellipsis truncation with accessible full-text disclosure + +## Component + + + +--- + +### Properties + + + +### Base Properties + +Supports [Base properties](/docs/base-properties--docs) and all [Text](/docs/content-text--docs) properties including `preset`, `color`, `weight`, `transform`, `ellipsis`, `nowrap`. + +### Styling Properties + +#### styles + +Customizes the root text element. Inherits all styling capabilities from the Text component. + +#### highlightStyles + +Customizes the appearance of highlighted text portions. + +### Style Properties + +These properties allow direct style application: `preset`, `color`, `weight`, `transform`, and all standard text/color style props. + +## Examples + +### Basic Usage + +```jsx +Simple text content +``` + +### Overflow with Auto-Tooltip + +When text overflows its container, hovering reveals a tooltip with the full content: + +```jsx + + + This is a very long text that will be truncated with ellipsis + and show a tooltip on hover + + +``` + + + +### Text Highlighting + +Highlight matching text within the content - useful for search results: + +```jsx + + Search results with highlighted text matching your query + +``` + + + +### Custom Highlight Styles + +Customize the appearance of highlighted text with `highlightStyles`: + +```jsx + + This important text has custom highlight styling + +``` + +### Case-Sensitive Highlighting + +By default, highlighting is case-insensitive. Enable case-sensitive matching with `highlightCaseSensitive`: + +```jsx +{/* Case-insensitive (default) - matches TEXT, text, Text */} + + TEXT and text and Text are all highlighted + + +{/* Case-sensitive - only matches exact case */} + + TEXT and text and Text - only lowercase is highlighted + +``` + + + +### Multiple Highlights + +All occurrences of the highlight string are highlighted: + +```jsx + + The quick brown fox jumps over the lazy fox + +``` + + + +### Custom Tooltip + +Override the auto-tooltip with custom content: + +```jsx + + Short text + +``` + + + +### Disabled Tooltip + +Disable the tooltip entirely: + +```jsx + + This text will truncate but won't show a tooltip + +``` + + + +### Tooltip Placement + +Control where the tooltip appears: + +```jsx + + Tooltip appears below on hover + +``` + + + +### With Text Styles + +Apply typography and color styles: + +```jsx +Heading style +Colored text +Bold text +``` + + + +### Search Results List + +Common pattern for filterable lists with highlighted search terms: + +```jsx +const items = ['Apple', 'Application', 'Banana', 'Apply', 'Grape']; +const searchTerm = 'app'; + + + {items.map((item) => ( + + {item} + + ))} + +``` + + + +## Accessibility + +### Keyboard Navigation + +- `Tab` - Moves focus to the component (when tooltip is enabled) +- Tooltip content is accessible via focus, not just hover + +### Screen Reader Support + +- Truncated text is fully accessible via tooltip +- Highlighted text uses semantic `` element for screen reader announcement +- Tooltip content is announced when triggered + +### ARIA Properties + +- Tooltip trigger automatically manages `aria-describedby` for tooltip association + +## Best Practices + +1. **Do**: Use for text that may exceed container width + ```jsx + + Long filename-that-might-overflow.tsx + + ``` + +2. **Do**: Provide meaningful highlight terms for search + ```jsx + {result.title} + ``` + +3. **Don't**: Use for multi-line text (this component enforces single-line with ellipsis) + ```jsx + {/* Use Text component instead for multi-line content */} + Multi-line paragraph content... + ``` + +4. **Don't**: Disable tooltips for important truncated content + ```jsx + {/* Users won't be able to see the full text */} + Important information that gets cut off... + ``` + +## Related Components + +- [Text](/docs/content-text--docs) - Base text component without overflow handling +- [Item](/docs/content-item--docs) - Interactive list item with similar overflow behavior +- [TooltipProvider](/docs/overlays-tooltip--docs) - Standalone tooltip wrapper + diff --git a/src/components/content/TextItem/TextItem.stories.tsx b/src/components/content/TextItem/TextItem.stories.tsx new file mode 100644 index 000000000..be6693a70 --- /dev/null +++ b/src/components/content/TextItem/TextItem.stories.tsx @@ -0,0 +1,237 @@ +import { Block } from '../../Block'; +import { Space } from '../../layout/Space'; + +import { TextItem } from './TextItem'; + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +const meta = { + title: 'Content/TextItem', + component: TextItem, + args: { + children: 'Text item with overflow handling', + }, + argTypes: { + /* Content */ + children: { + control: 'text', + description: 'Text content', + table: { + type: { summary: 'ReactNode' }, + }, + }, + highlight: { + control: 'text', + description: 'String to highlight within children', + table: { + type: { summary: 'string' }, + }, + }, + highlightCaseSensitive: { + control: 'boolean', + description: 'Whether highlight matching is case-sensitive', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'false' }, + }, + }, + + /* Tooltip */ + tooltip: { + control: 'text', + description: + 'Tooltip configuration. Use true for auto tooltip on overflow, string for custom tooltip text.', + table: { + type: { summary: 'string | boolean | TooltipProviderProps' }, + defaultValue: { summary: 'true' }, + }, + }, + tooltipPlacement: { + control: 'select', + options: [ + 'top', + 'bottom', + 'left', + 'right', + 'top start', + 'top end', + 'bottom start', + 'bottom end', + ], + description: 'Default tooltip placement', + table: { + type: { summary: 'Placement' }, + defaultValue: { summary: 'top' }, + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'Simple text item', + }, +}; + +export const OverflowWithTooltip: Story = { + render: () => ( + + + This is a very long text that will be truncated with ellipsis and show a + tooltip on hover + + + ), +}; + +export const WithHighlight: Story = { + args: { + children: 'Search results with highlighted text matching your query', + highlight: 'highlight', + }, +}; + +export const HighlightCaseSensitive: Story = { + render: () => ( + +
+ Case-insensitive (default): + + TEXT and text and Text are all highlighted + +
+
+ Case-sensitive: + + TEXT and text and Text - only lowercase is highlighted + +
+
+ ), +}; + +export const MultipleHighlights: Story = { + args: { + children: 'The quick brown fox jumps over the lazy fox', + highlight: 'fox', + }, +}; + +export const HighlightWithOverflow: Story = { + render: () => ( + + + This is a very long text with important information that will be + truncated + + + ), +}; + +export const CustomTooltip: Story = { + render: () => ( + + + Short text + + + ), +}; + +export const DisabledTooltip: Story = { + render: () => ( + + + This is a very long text but tooltip is disabled even on overflow + + + ), +}; + +export const TooltipPlacements: Story = { + render: () => ( + + + + Tooltip on top - hover to see long text tooltip + + + + + Tooltip on bottom - hover to see long text tooltip + + + + + Tooltip on left - hover to see long text tooltip + + + + + Tooltip on right - hover to see long text tooltip + + + + ), +}; + +export const WithTextStyles: Story = { + render: () => ( + + Heading style text item + + Colored text item + + + Bold text item + + + ), +}; + +export const InConstrainedContainer: Story = { + render: () => ( + + + Very narrow container + + + Medium width container with more space for text + + + + Wide container that can fit longer text without truncation + + + + ), +}; + +export const HighlightInList: Story = { + render: () => { + const items = [ + 'Apple', + 'Application', + 'Banana', + 'Apply', + 'Grape', + 'Pineapple', + ]; + const searchTerm = 'app'; + + return ( + + + {items.map((item) => ( + + {item} + + ))} + + + ); + }, +}; diff --git a/src/components/content/TextItem/TextItem.tsx b/src/components/content/TextItem/TextItem.tsx new file mode 100644 index 000000000..38aa6e727 --- /dev/null +++ b/src/components/content/TextItem/TextItem.tsx @@ -0,0 +1,170 @@ +import { + forwardRef, + HTMLAttributes, + ReactNode, + RefObject, + useMemo, +} from 'react'; +import { OverlayProps } from 'react-aria'; + +import { + BASE_STYLES, + COLOR_STYLES, + extractStyles, + filterBaseProps, + Styles, + tasty, + TEXT_STYLES, +} from '../../../tasty'; +import { CubeTextProps, Text, TEXT_PROP_MAP } from '../Text'; +import { AutoTooltipValue, useAutoTooltip } from '../use-auto-tooltip'; + +const STYLE_LIST = [...BASE_STYLES, ...TEXT_STYLES, ...COLOR_STYLES] as const; + +export interface CubeTextItemProps extends CubeTextProps { + /** + * String to highlight within children. + * Only works when children is a plain string. + */ + highlight?: string; + /** + * Whether highlight matching is case-sensitive. + * @default false + */ + highlightCaseSensitive?: boolean; + /** + * Custom styles for highlighted text. + */ + highlightStyles?: Styles; + /** + * Tooltip content and configuration: + * - string: simple tooltip text + * - true: auto tooltip on overflow (shows children as tooltip when truncated) + * - object: advanced configuration with optional auto property + * @default true + */ + tooltip?: AutoTooltipValue; + /** + * Default tooltip placement. + * @default "top" + */ + tooltipPlacement?: OverlayProps['placement']; +} + +const TextItemElement = tasty(Text, { + qa: 'TextItem', + styles: { + display: 'inline-block', + verticalAlign: 'bottom', + width: 'max 100%', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, +}); + +/** + * Highlights occurrences of a search string within text. + * Returns an array of ReactNodes with highlighted portions wrapped in Text.Highlight. + */ +function highlightText( + text: string, + highlight: string, + caseSensitive: boolean, + highlightStyles?: Styles, +): ReactNode[] { + if (!highlight) { + return [text]; + } + + const flags = caseSensitive ? 'g' : 'gi'; + const escapedHighlight = highlight.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(`(${escapedHighlight})`, flags); + const parts = text.split(regex); + + return parts.map((part, index) => { + const isMatch = caseSensitive + ? part === highlight + : part.toLowerCase() === highlight.toLowerCase(); + + if (isMatch) { + return ( + + {part} + + ); + } + return part; + }); +} + +export const TextItem = forwardRef( + function TextItem(props, ref) { + const { + children, + highlight, + highlightCaseSensitive = false, + highlightStyles, + tooltip = true, + tooltipPlacement = 'top', + ...restProps + } = props; + + // Extract style props (preset, color, etc.) to pass via styles prop + const styles = extractStyles(restProps, STYLE_LIST, {}, TEXT_PROP_MAP); + + const { labelRef, renderWithTooltip } = useAutoTooltip({ + tooltip, + children, + }); + + // Process children with highlight if applicable + const processedChildren = useMemo(() => { + if (typeof children === 'string' && highlight) { + return highlightText( + children, + highlight, + highlightCaseSensitive, + highlightStyles, + ); + } + return children; + }, [children, highlight, highlightCaseSensitive, highlightStyles]); + + const renderElement = ( + tooltipTriggerProps?: HTMLAttributes, + tooltipRef?: RefObject, + ) => { + // Merge refs + const handleRef = (element: HTMLElement | null) => { + // Set component forwarded ref + if (typeof ref === 'function') { + ref(element); + } else if (ref) { + (ref as any).current = element; + } + // Set tooltip ref + if (tooltipRef) { + (tooltipRef as any).current = element; + } + // Set label ref for overflow detection + labelRef(element); + }; + + return ( + + {processedChildren} + + ); + }; + + return renderWithTooltip(renderElement, tooltipPlacement); + }, +); + +TextItem.displayName = 'TextItem'; diff --git a/src/components/content/TextItem/index.ts b/src/components/content/TextItem/index.ts new file mode 100644 index 000000000..1e2573dd4 --- /dev/null +++ b/src/components/content/TextItem/index.ts @@ -0,0 +1,2 @@ +export { TextItem } from './TextItem'; +export type { CubeTextItemProps } from './TextItem'; diff --git a/src/components/content/use-auto-tooltip.tsx b/src/components/content/use-auto-tooltip.tsx new file mode 100644 index 000000000..916a26c57 --- /dev/null +++ b/src/components/content/use-auto-tooltip.tsx @@ -0,0 +1,225 @@ +import { + HTMLAttributes, + ReactNode, + RefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { OverlayProps } from 'react-aria'; + +import { Props } from '../../tasty'; +import { + CubeTooltipProviderProps, + TooltipProvider, +} from '../overlays/Tooltip/TooltipProvider'; + +export type AutoTooltipValue = + | string + | boolean + | (Omit & { auto?: boolean }); + +export interface UseAutoTooltipOptions { + tooltip: AutoTooltipValue | undefined; + children: ReactNode; + labelProps?: Props; + isDynamicLabel?: boolean; +} + +export function useAutoTooltip({ + tooltip, + children, + labelProps, + isDynamicLabel = false, +}: UseAutoTooltipOptions) { + // Determine if auto tooltip is enabled + // Auto tooltip only works when children is a string (overflow detection needs text) + const isAutoTooltipEnabled = useMemo(() => { + if (typeof children !== 'string') return false; + + // Boolean true enables auto overflow detection + if (tooltip === true) return true; + if (typeof tooltip === 'object') { + // If title is provided and auto is explicitly true, enable auto overflow detection + if (tooltip.title) { + return tooltip.auto === true; + } + + // If no title is provided, default to auto=true unless explicitly disabled + const autoValue = tooltip.auto !== undefined ? tooltip.auto : true; + return !!autoValue; + } + return false; + }, [tooltip, children]); + + // Track label overflow for auto tooltip (only when enabled) + const externalLabelRef = (labelProps as any)?.ref; + const [isLabelOverflowed, setIsLabelOverflowed] = useState(false); + const elementRef = useRef(null); + const resizeObserverRef = useRef(null); + + const checkLabelOverflow = useCallback(() => { + const label = elementRef.current; + if (!label) { + setIsLabelOverflowed(false); + return; + } + + const hasOverflow = label.scrollWidth > label.clientWidth; + setIsLabelOverflowed(hasOverflow); + }, []); + + useEffect(() => { + if (isAutoTooltipEnabled) { + checkLabelOverflow(); + } + }, [isAutoTooltipEnabled, checkLabelOverflow]); + + // Attach ResizeObserver via callback ref to handle DOM node changes + const handleLabelElementRef = useCallback( + (element: HTMLElement | null) => { + // Call external callback ref to notify external refs + if (externalLabelRef) { + if (typeof externalLabelRef === 'function') { + externalLabelRef(element); + } else { + (externalLabelRef as any).current = element; + } + } + + // Disconnect previous observer + if (resizeObserverRef.current) { + try { + resizeObserverRef.current.disconnect(); + } catch { + // do nothing + } + resizeObserverRef.current = null; + } + + elementRef.current = element; + + if (element && isAutoTooltipEnabled) { + // Create a fresh observer to capture the latest callback + const obs = new ResizeObserver(() => { + checkLabelOverflow(); + }); + resizeObserverRef.current = obs; + obs.observe(element); + // Initial check + checkLabelOverflow(); + } else { + setIsLabelOverflowed(false); + } + }, + [externalLabelRef, isAutoTooltipEnabled, checkLabelOverflow], + ); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (resizeObserverRef.current) { + try { + resizeObserverRef.current.disconnect(); + } catch { + // do nothing + } + resizeObserverRef.current = null; + } + elementRef.current = null; + }; + }, []); + + const finalLabelProps = useMemo(() => { + const props = { + ...(labelProps || {}), + }; + + delete props.ref; + + return props; + }, [labelProps]); + + const renderWithTooltip = ( + renderElement: ( + tooltipTriggerProps?: HTMLAttributes, + tooltipRef?: RefObject, + ) => ReactNode, + defaultTooltipPlacement: OverlayProps['placement'], + ) => { + // Handle tooltip rendering based on tooltip prop type + if (tooltip) { + // String tooltip - simple case + if (typeof tooltip === 'string') { + return ( + + {(triggerProps, ref) => renderElement(triggerProps, ref)} + + ); + } + + // Boolean tooltip - auto tooltip on overflow + if (tooltip === true) { + if ((children || labelProps) && (isLabelOverflowed || isDynamicLabel)) { + return ( + + {(triggerProps, ref) => renderElement(triggerProps, ref)} + + ); + } + } + + // Object tooltip - advanced configuration + if (typeof tooltip === 'object') { + const { auto, ...tooltipProps } = tooltip; + + // If title is provided and auto is not explicitly true, always show the tooltip + if (tooltipProps.title && auto !== true) { + return ( + + {(triggerProps, ref) => renderElement(triggerProps, ref)} + + ); + } + + // If title is provided with auto=true, OR no title but auto behavior enabled + if ((children || labelProps) && (isLabelOverflowed || isDynamicLabel)) { + return ( + + {(triggerProps, ref) => renderElement(triggerProps, ref)} + + ); + } + } + } + + return renderElement(); + }; + + return { + labelRef: handleLabelElementRef, + labelProps: finalLabelProps, + isLabelOverflowed, + isAutoTooltipEnabled, + hasTooltip: !!tooltip, + renderWithTooltip, + }; +} diff --git a/src/index.ts b/src/index.ts index 66913cef9..21113f947 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { CubeParagraphProps, Paragraph } from './components/content/Paragraph'; import { CubeTextProps, Text } from './components/content/Text'; +import { CubeTextItemProps, TextItem } from './components/content/TextItem'; import { CubeTitleProps, Title } from './components/content/Title'; import './version'; @@ -172,8 +173,13 @@ export const Typography = { Paragraph, }; -export { Text, Title, Paragraph }; -export type { CubeTextProps, CubeTitleProps, CubeParagraphProps }; +export { Text, TextItem, Title, Paragraph }; +export type { + CubeTextProps, + CubeTextItemProps, + CubeTitleProps, + CubeParagraphProps, +}; export { Provider, useProviderProps } from './provider'; export type { ProviderProps } from './provider'; diff --git a/src/stories/Result.stories.tsx b/src/stories/Result.stories.tsx index 7b4c8577d..c95e72087 100644 --- a/src/stories/Result.stories.tsx +++ b/src/stories/Result.stories.tsx @@ -103,9 +103,9 @@ export const CustomTitle = { ), subtitle: ( - + Complete your profile to increase search relevancy. - + ), icon: , diff --git a/src/stories/Typography.stories.tsx b/src/stories/Typography.stories.tsx index 3398ae91a..295e3db87 100644 --- a/src/stories/Typography.stories.tsx +++ b/src/stories/Typography.stories.tsx @@ -108,7 +108,7 @@ export const Text = { Danger label Strong label Emphasis label - Selection label + Highlight label ), };