laif-ds
Version:
Design System di Laif con componenti React basati su principi di Atomic Design
343 lines (266 loc) • 11.7 kB
Markdown
# TruncatedCell
The TruncatedCell component is designed to display text that may be too long for its container. It automatically detects when text is truncated and provides a popover to show the full content.
## Features
- **Flexible Content**: Accepts plain text or rich ReactNode content
- **Automatic Truncation Detection**: Uses scrollWidth vs clientWidth comparison to detect if content is truncated
- **Responsive**: Re-checks truncation on resize using ResizeObserver for better performance
- **Controlled Popover**: Manages popover state programmatically for better UX
- **Eye Icon Hint**: Displays an eye icon hint only when content is truncated, with smooth fade-in animation
- **Button Visibility Control**: Optional `showButton` prop to control eye button visibility
- **Custom Popover Styling**: `popoverClassName` prop for custom popover content styling
- **Keyboard Accessibility**: Full keyboard navigation support (Enter/Space to open popover)
- **Screen Reader Support**: Proper ARIA labels and roles for accessibility
- **Empty State**: Shows "-" with muted styling when no content is provided
- **Custom Empty Fallback**: Supports a custom placeholder for empty values
- **Customizable Styling**: Supports custom CSS classes for container, cell, and popover
- **Dialog Title**: Optional title for the popover header
- **Custom Content**: Full JSX support for custom popover content
- **Hover Effects**: Smooth underline on content hover and button appearance
- **Scrollable Content**: Long content in popover is scrollable with max height
- **DX-first API**: Rich children work out of the box, and `text` is only needed when you want explicit popover/accessibility text
## Usage
```tsx
import { TruncatedCell } from "laif-ds";
// Basic usage with string content
<TruncatedCell text="Short text" />
// Long text that will be truncated
<TruncatedCell
text="This is a very long text that will be truncated because it exceeds the container width"
wrapperClassName="w-56"
/>
// String children also work without text
<TruncatedCell wrapperClassName="w-56">
This is a string content
</TruncatedCell>
// Rich content works without duplicating text
<TruncatedCell wrapperClassName="w-56">
<div className="flex items-center gap-2">
<Avatar src="/avatar.jpg" />
<div className="min-w-0">
<p className="font-semibold">John Doe</p>
<p className="text-sm text-gray-600">john@example.com</p>
</div>
</div>
</TruncatedCell>
// Provide text when you want explicit popover or accessibility content
<TruncatedCell
text="John Doe - john@example.com"
title="User details"
wrapperClassName="w-56"
>
<UserSummary />
</TruncatedCell>
// With custom styling and title
<TruncatedCell
text="Custom styled truncated text"
title="Full Content"
wrapperClassName="w-40"
className="max-w-40"
/>
// With custom popover content and styling
<TruncatedCell
text="Text with custom popover content"
title="Custom Content"
popoverContent={
<div className="space-y-3">
<p>Custom JSX content here</p>
<button>Action Button</button>
</div>
}
popoverClassName="max-w-md"
showButton={true}
/>
// Hide the eye hint
<TruncatedCell
text="Text without eye button"
showButton={false}
/>
// Empty text
<TruncatedCell text="" />
// Custom empty fallback
<TruncatedCell text="" emptyFallback="No value" />
```
## Props
| Prop | Type | Default | Description |
| ------------------ | ----------- | ----------- | ---------------------------------------------------------------------------- |
| `children` | `ReactNode` | `undefined` | Content to display |
| `text` | `string` | `undefined` | Optional text used for truncation detection, accessibility, and popover copy |
| `title` | `string` | `undefined` | Optional title for the popover dialog |
| `popoverContent` | `ReactNode` | `undefined` | Custom popover content (JSX) |
| `wrapperClassName` | `string` | `""` | Preferred CSS classes for the wrapper container |
| `className` | `string` | `""` | CSS classes for the cell content |
| `popoverClassName` | `string` | `""` | CSS classes for the popover content |
| `showButton` | `boolean` | `true` | Flag used to show/hide the eye hint |
| `emptyFallback` | `ReactNode` | `"-"` | Custom placeholder rendered when no content is available |
## Behavior
### Truncation Detection
- Component monitors the text element's scrollWidth vs clientWidth
- If scrollWidth > clientWidth, the text is considered truncated
- Detection runs on mount and on resize
- When `text` is not provided, the component also extracts `textContent` from the rendered node to power the default popover content
### Visual States
1. **Normal State**: Text is displayed with truncation (CSS `truncate`)
2. **Truncated State**: Text is truncated + eye icon hint appears
3. **Empty State**: Shows "-" when text is empty/undefined
4. **Popover State**: Opens when there is content to inspect, and the eye icon hint appears only when the rendered value is truncated
### Styling
- Container: `flex max-w-60 gap-2` + custom classes
- Text container: `flex min-w-0 flex-1`
- Text element: `w-full min-w-0 truncate whitespace-nowrap`
- Popover: `w-96` with `whitespace-pre-wrap` for multiline support
### Content Types
The TruncatedCell component supports two main usage patterns:
#### 1. String Content (Simple)
```tsx
// Direct text prop
<TruncatedCell text="Simple text content" />
// String children (text prop optional)
<TruncatedCell wrapperClassName="w-56">
String content as children
</TruncatedCell>
```
#### 2. ReactNode Content (Advanced)
```tsx
// Rich children work without text
<TruncatedCell wrapperClassName="w-56">
<div className="flex items-center gap-2">
<Avatar src="/avatar.jpg" />
<div className="min-w-0">
<p className="font-semibold">John Doe</p>
<p className="text-sm text-gray-600">john@example.com</p>
</div>
</div>
</TruncatedCell>
// Add text when you want explicit popover content
<TruncatedCell text="User profile - John Doe" wrapperClassName="w-56">
<UserSummary />
</TruncatedCell>
```
### Choosing Between `children` and `text`
- Use `text` for the simplest plain-text case
- Use `children` when you need custom rendering inside the truncated area
- Add `text` together with `children` when the rendered content is visual or abbreviated and you want more descriptive text in the popover
```tsx
// Plain text
<TruncatedCell text="Contract pending signature" />
// Rich UI with automatic text extraction
<TruncatedCell wrapperClassName="w-56">
<div>Complex content</div>
</TruncatedCell>
// Rich UI with explicit popover and a11y text
<TruncatedCell text="Complex content description" wrapperClassName="w-56">
<div>Complex content</div>
</TruncatedCell>
```
### Custom Popover Content
The `popoverContent` prop allows you to override the default text display with custom JSX content:
- **Full Flexibility**: Any React components can be used as popover content
- **Backward Compatible**: When not provided, defaults to displaying the text with Typo component
- **Title Support**: Works seamlessly with the `title` prop for popover headers
- **Custom Styling**: Use `popoverClassName` to style the popover container
- **Button Control**: Use `showButton` to control eye hint visibility
- **Open Behavior**: Custom popover content can open even when the displayed value itself is not truncated
```tsx
<TruncatedCell
text="User profile"
title="User Details"
popoverContent={
<div className="space-y-2">
<div className="flex items-center gap-2">
<Avatar src="/avatar.jpg" />
<div>
<p className="font-semibold">John Doe</p>
<p className="text-sm text-gray-600">john@example.com</p>
</div>
</div>
<div className="flex gap-2">
<Button size="sm">Edit</Button>
<Button size="sm" variant="outline">
View Profile
</Button>
</div>
</div>
}
popoverClassName="max-w-md"
showButton={true}
/>
```
### Button Visibility Control
The `showButton` prop gives you control over the eye hint visibility:
```tsx
// Show eye hint (default)
<TruncatedCell text="Long text that will be truncated" showButton={true} />
// Hide eye hint
<TruncatedCell text="Long text without eye hint" showButton={false} />
```
### Empty Fallback
```tsx
// Default placeholder
<TruncatedCell text="" />
// Custom placeholder
<TruncatedCell text="" emptyFallback="No value" />
```
### Popover Styling
The `popoverClassName` prop allows custom styling of the popover container:
```tsx
<TruncatedCell
text="Custom styled popover"
popoverClassName="max-w-md bg-blue-50 border-blue-200"
/>
```
## Technical Details
### Dependencies
- React hooks: `useState`, `useEffect`, `useRef`, `useCallback`
- Laif-DS components: `Popover`, `PopoverContent`, `PopoverTrigger`, `Typo`, `Icon`
- Utility: `cn` for class merging
### Performance Optimizations
- **ResizeObserver**: Uses modern ResizeObserver API for better performance than window resize events
- **useCallback**: Memoizes truncation check function to prevent unnecessary re-renders
- **Controlled State**: Manages popover state programmatically to avoid prop drilling
- **Cleanup**: Proper cleanup of observers and event listeners
- **Optimized Re-renders**: Only re-checks truncation when content changes
### Event Handling
- ResizeObserver for element-specific resize detection
- Fallback window resize listener for older browsers
- Click on the cell trigger opens the popover when available
- Keyboard support (Enter/Space keys)
- Hover effects with smooth transitions
### Accessibility Features
- **Semantic Trigger**: Uses a real `button` element as the interactive trigger
- **Keyboard Navigation**: Full support for Enter and Space keys
- **Screen Reader Labels**: Dynamic `aria-label` based on truncation state
- **Focus Management**: Native button focus behavior
- **Semantic HTML**: Popover content remains accessible even for multiline values
## Accessibility
- Text container has cursor pointer to indicate interactivity
- Hover state provides visual feedback
- Popover provides full text access for screen readers
- Eye icon provides a clear visual hint that more content is available
## Examples
### In a Table Cell
```tsx
const TableCell = ({ value }: { value: string }) => (
<TruncatedCell text={value} wrapperClassName="w-56" className="py-2" />
);
```
### With Maximum Width Constraint
```tsx
<TruncatedCell
text="Constrained width text that will truncate"
wrapperClassName="w-32"
/>
```
### Multiline Text Support
```tsx
<TruncatedCell text="Line 1\nLine 2\nLine 3" className="max-w-48" />
```
## Comparison with Alternatives
### vs Tooltip
- **TruncatedCell**: Supports click-to-inspect content, and visually hints with the eye icon when text is actually truncated
- **Tooltip**: Always shows on hover regardless of truncation
### vs CSS text-overflow alone
- **TruncatedCell**: Provides access to full content
- **CSS only**: Content is inaccessible when truncated
### vs Expandable Text
- **TruncatedCell**: Compact, popover-based approach
- **Expandable**: Takes more space, inline expansion