@discourser/design-system 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guidelines/Guidelines.md +195 -0
- package/guidelines/components/accordion.md +639 -0
- package/guidelines/components/avatar.md +945 -0
- package/guidelines/components/badge.md +667 -0
- package/guidelines/components/button.md +314 -0
- package/guidelines/components/card.md +353 -0
- package/guidelines/components/checkbox.md +583 -0
- package/guidelines/components/dialog.md +465 -0
- package/guidelines/components/drawer.md +961 -0
- package/guidelines/components/heading.md +505 -0
- package/guidelines/components/icon-button.md +417 -0
- package/guidelines/components/input.md +499 -0
- package/guidelines/components/popover.md +1200 -0
- package/guidelines/components/progress.md +773 -0
- package/guidelines/components/radio-group.md +757 -0
- package/guidelines/components/select.md +1155 -0
- package/guidelines/components/skeleton.md +726 -0
- package/guidelines/components/switch.md +457 -0
- package/guidelines/components/tabs.md +834 -0
- package/guidelines/components/textarea.md +425 -0
- package/guidelines/components/toast.md +707 -0
- package/guidelines/components/tooltip.md +832 -0
- package/guidelines/design-tokens/colors.md +187 -0
- package/guidelines/design-tokens/elevation.md +274 -0
- package/guidelines/design-tokens/spacing.md +289 -0
- package/guidelines/design-tokens/typography.md +226 -0
- package/guidelines/overview-components.md +204 -0
- package/package.json +3 -2
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
# Tooltip
|
|
2
|
+
|
|
3
|
+
**Purpose:** Contextual information overlay that appears on hover or focus, providing supplementary details without cluttering the interface.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Tooltip } from '@discourser/design-system';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Component API
|
|
12
|
+
|
|
13
|
+
The Tooltip component uses a simplified API that wraps Ark UI's compound component pattern:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
interface TooltipProps {
|
|
17
|
+
showArrow?: boolean; // Display arrow pointing to trigger
|
|
18
|
+
portalled?: boolean; // Render in portal (default: true)
|
|
19
|
+
portalRef?: React.RefObject; // Custom portal container
|
|
20
|
+
children: React.ReactNode; // Trigger element
|
|
21
|
+
content: React.ReactNode; // Tooltip content
|
|
22
|
+
contentProps?: ContentProps; // Additional content styling
|
|
23
|
+
disabled?: boolean; // Disable tooltip
|
|
24
|
+
positioning?: PositioningOptions; // Placement and positioning
|
|
25
|
+
openDelay?: number; // Delay before showing (ms)
|
|
26
|
+
closeDelay?: number; // Delay before hiding (ms)
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Props
|
|
31
|
+
|
|
32
|
+
| Prop | Type | Default | Description |
|
|
33
|
+
| -------------- | ------------------------------ | ---------------------- | -------------------------------------------- |
|
|
34
|
+
| `children` | `ReactNode` | Required | Element that triggers the tooltip |
|
|
35
|
+
| `content` | `ReactNode \| string` | Required | Content displayed in tooltip |
|
|
36
|
+
| `showArrow` | `boolean` | `false` | Show arrow pointing to trigger element |
|
|
37
|
+
| `portalled` | `boolean` | `true` | Render tooltip in portal for proper layering |
|
|
38
|
+
| `portalRef` | `React.RefObject<HTMLElement>` | - | Custom container for portal rendering |
|
|
39
|
+
| `contentProps` | `ContentProps` | - | Additional props for content styling |
|
|
40
|
+
| `disabled` | `boolean` | `false` | Disable tooltip (returns children only) |
|
|
41
|
+
| `positioning` | `PositioningOptions` | `{ placement: 'top' }` | Tooltip placement and positioning |
|
|
42
|
+
| `openDelay` | `number` | `700` | Delay in ms before tooltip appears |
|
|
43
|
+
| `closeDelay` | `number` | `500` | Delay in ms before tooltip disappears |
|
|
44
|
+
|
|
45
|
+
### Positioning Options
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
positioning={{
|
|
49
|
+
placement: 'top' | 'right' | 'bottom' | 'left' |
|
|
50
|
+
'top-start' | 'top-end' | 'right-start' | 'right-end' |
|
|
51
|
+
'bottom-start' | 'bottom-end' | 'left-start' | 'left-end',
|
|
52
|
+
gutter: number, // Distance from trigger (default: 8px)
|
|
53
|
+
offset: { x, y }, // Fine-tune position
|
|
54
|
+
flip: boolean, // Auto-flip on edge collision (default: true)
|
|
55
|
+
slide: boolean, // Slide along edge (default: true)
|
|
56
|
+
}}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Visual Characteristics
|
|
60
|
+
|
|
61
|
+
- **Background**: Gray solid background with subtle shadow
|
|
62
|
+
- **Typography**: Extra small, semibold text
|
|
63
|
+
- **Border Radius**: Large (l2)
|
|
64
|
+
- **Padding**: Compact (8px horizontal, 6px vertical)
|
|
65
|
+
- **Max Width**: 320px (xs size)
|
|
66
|
+
- **Animation**: Scale-fade in/out with fast timing
|
|
67
|
+
- **Shadow**: Small shadow for elevation
|
|
68
|
+
|
|
69
|
+
## Examples
|
|
70
|
+
|
|
71
|
+
### Basic Usage
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { Tooltip } from '@discourser/design-system';
|
|
75
|
+
import { Button } from '@discourser/design-system';
|
|
76
|
+
|
|
77
|
+
// Simple text tooltip
|
|
78
|
+
<Tooltip content="Click to save your changes">
|
|
79
|
+
<Button>Save</Button>
|
|
80
|
+
</Tooltip>
|
|
81
|
+
|
|
82
|
+
// With arrow indicator
|
|
83
|
+
<Tooltip content="This action cannot be undone" showArrow>
|
|
84
|
+
<Button variant="filled">Delete</Button>
|
|
85
|
+
</Tooltip>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Icon Tooltips
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { InfoIcon, HelpIcon, SettingsIcon } from 'your-icon-library';
|
|
92
|
+
import { IconButton } from '@discourser/design-system';
|
|
93
|
+
|
|
94
|
+
// Information icon
|
|
95
|
+
<Tooltip content="Additional information about this field" showArrow>
|
|
96
|
+
<IconButton aria-label="More info">
|
|
97
|
+
<InfoIcon />
|
|
98
|
+
</IconButton>
|
|
99
|
+
</Tooltip>
|
|
100
|
+
|
|
101
|
+
// Help icon
|
|
102
|
+
<Tooltip content="Click for help documentation" showArrow>
|
|
103
|
+
<IconButton aria-label="Help">
|
|
104
|
+
<HelpIcon />
|
|
105
|
+
</IconButton>
|
|
106
|
+
</Tooltip>
|
|
107
|
+
|
|
108
|
+
// Settings icon
|
|
109
|
+
<Tooltip content="Open settings panel">
|
|
110
|
+
<IconButton aria-label="Settings">
|
|
111
|
+
<SettingsIcon />
|
|
112
|
+
</IconButton>
|
|
113
|
+
</Tooltip>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Positioning
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// Top (default)
|
|
120
|
+
<Tooltip content="Appears above" positioning={{ placement: 'top' }} showArrow>
|
|
121
|
+
<Button>Top</Button>
|
|
122
|
+
</Tooltip>
|
|
123
|
+
|
|
124
|
+
// Right
|
|
125
|
+
<Tooltip content="Appears to the right" positioning={{ placement: 'right' }} showArrow>
|
|
126
|
+
<Button>Right</Button>
|
|
127
|
+
</Tooltip>
|
|
128
|
+
|
|
129
|
+
// Bottom
|
|
130
|
+
<Tooltip content="Appears below" positioning={{ placement: 'bottom' }} showArrow>
|
|
131
|
+
<Button>Bottom</Button>
|
|
132
|
+
</Tooltip>
|
|
133
|
+
|
|
134
|
+
// Left
|
|
135
|
+
<Tooltip content="Appears to the left" positioning={{ placement: 'left' }} showArrow>
|
|
136
|
+
<Button>Left</Button>
|
|
137
|
+
</Tooltip>
|
|
138
|
+
|
|
139
|
+
// Advanced positioning
|
|
140
|
+
<Tooltip
|
|
141
|
+
content="Custom positioned tooltip"
|
|
142
|
+
positioning={{
|
|
143
|
+
placement: 'bottom-start',
|
|
144
|
+
gutter: 12,
|
|
145
|
+
offset: { x: 0, y: 4 }
|
|
146
|
+
}}
|
|
147
|
+
showArrow
|
|
148
|
+
>
|
|
149
|
+
<Button>Custom Position</Button>
|
|
150
|
+
</Tooltip>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Complex Content
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// Multi-line text
|
|
157
|
+
<Tooltip
|
|
158
|
+
content="This is a longer tooltip with more detailed information that wraps across multiple lines to provide comprehensive context."
|
|
159
|
+
showArrow
|
|
160
|
+
contentProps={{ style: { maxWidth: '250px' } }}
|
|
161
|
+
>
|
|
162
|
+
<Button>Detailed Info</Button>
|
|
163
|
+
</Tooltip>
|
|
164
|
+
|
|
165
|
+
// Rich content with JSX
|
|
166
|
+
<Tooltip
|
|
167
|
+
content={
|
|
168
|
+
<div>
|
|
169
|
+
<strong>Keyboard Shortcut</strong>
|
|
170
|
+
<div>Press Cmd+S to save</div>
|
|
171
|
+
</div>
|
|
172
|
+
}
|
|
173
|
+
showArrow
|
|
174
|
+
>
|
|
175
|
+
<Button>Save</Button>
|
|
176
|
+
</Tooltip>
|
|
177
|
+
|
|
178
|
+
// With custom styling
|
|
179
|
+
<Tooltip
|
|
180
|
+
content="Custom styled tooltip"
|
|
181
|
+
contentProps={{
|
|
182
|
+
className: css({
|
|
183
|
+
maxWidth: '400px',
|
|
184
|
+
textAlign: 'center'
|
|
185
|
+
})
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
<Button>Custom Style</Button>
|
|
189
|
+
</Tooltip>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Timing Control
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// Instant tooltip (no delay)
|
|
196
|
+
<Tooltip content="Appears immediately" openDelay={0} showArrow>
|
|
197
|
+
<Button>Instant</Button>
|
|
198
|
+
</Tooltip>
|
|
199
|
+
|
|
200
|
+
// Longer delay
|
|
201
|
+
<Tooltip content="Takes longer to appear" openDelay={1000} showArrow>
|
|
202
|
+
<Button>Delayed</Button>
|
|
203
|
+
</Tooltip>
|
|
204
|
+
|
|
205
|
+
// Quick close
|
|
206
|
+
<Tooltip content="Closes quickly" openDelay={500} closeDelay={0} showArrow>
|
|
207
|
+
<Button>Quick Close</Button>
|
|
208
|
+
</Tooltip>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Disabled State
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
const [showTooltip, setShowTooltip] = useState(true);
|
|
215
|
+
|
|
216
|
+
// Conditionally disable
|
|
217
|
+
<Tooltip content="This tooltip can be disabled" disabled={!showTooltip}>
|
|
218
|
+
<Button>Toggle Tooltip</Button>
|
|
219
|
+
</Tooltip>
|
|
220
|
+
|
|
221
|
+
// Disabled returns children only
|
|
222
|
+
<Tooltip content="Never shown" disabled>
|
|
223
|
+
<Button>No Tooltip</Button>
|
|
224
|
+
</Tooltip>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Non-Portalled Tooltip
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// Render in local DOM (useful for specific layout contexts)
|
|
231
|
+
<div style={{ position: 'relative', overflow: 'hidden' }}>
|
|
232
|
+
<Tooltip content="Stays within parent" portalled={false} showArrow>
|
|
233
|
+
<Button>Local Tooltip</Button>
|
|
234
|
+
</Tooltip>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
// Custom portal container
|
|
238
|
+
const portalRef = useRef<HTMLDivElement>(null);
|
|
239
|
+
|
|
240
|
+
<div>
|
|
241
|
+
<div ref={portalRef} />
|
|
242
|
+
|
|
243
|
+
<Tooltip content="Renders in custom container" portalRef={portalRef} showArrow>
|
|
244
|
+
<Button>Custom Portal</Button>
|
|
245
|
+
</Tooltip>
|
|
246
|
+
</div>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Common Patterns
|
|
250
|
+
|
|
251
|
+
### Truncated Text Helper
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Show full text in tooltip when truncated
|
|
255
|
+
<Tooltip content="This is the complete text that gets truncated in the UI">
|
|
256
|
+
<div className={css({
|
|
257
|
+
maxWidth: '200px',
|
|
258
|
+
overflow: 'hidden',
|
|
259
|
+
textOverflow: 'ellipsis',
|
|
260
|
+
whiteSpace: 'nowrap'
|
|
261
|
+
})}>
|
|
262
|
+
This is the complete text that gets truncated in the UI
|
|
263
|
+
</div>
|
|
264
|
+
</Tooltip>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Form Field Help
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { Input } from '@discourser/design-system';
|
|
271
|
+
|
|
272
|
+
// Help text for form inputs
|
|
273
|
+
<div className={css({ display: 'flex', alignItems: 'center', gap: 'sm' })}>
|
|
274
|
+
<Input label="Password" type="password" />
|
|
275
|
+
<Tooltip
|
|
276
|
+
content="Password must be at least 8 characters with 1 uppercase, 1 lowercase, and 1 number"
|
|
277
|
+
showArrow
|
|
278
|
+
positioning={{ placement: 'right' }}
|
|
279
|
+
>
|
|
280
|
+
<IconButton aria-label="Password requirements">
|
|
281
|
+
<InfoIcon />
|
|
282
|
+
</IconButton>
|
|
283
|
+
</Tooltip>
|
|
284
|
+
</div>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Action Confirmation
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// Explain consequences of actions
|
|
291
|
+
<div className={css({ display: 'flex', gap: 'sm' })}>
|
|
292
|
+
<Tooltip content="Permanently remove this item" showArrow>
|
|
293
|
+
<Button variant="tonal">Delete</Button>
|
|
294
|
+
</Tooltip>
|
|
295
|
+
|
|
296
|
+
<Tooltip content="Restore default settings" showArrow>
|
|
297
|
+
<Button variant="outlined">Reset</Button>
|
|
298
|
+
</Tooltip>
|
|
299
|
+
</div>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Status Indicators
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// Explain status with tooltips
|
|
306
|
+
<div className={css({ display: 'flex', alignItems: 'center', gap: 'sm' })}>
|
|
307
|
+
<Tooltip content="All systems operational" positioning={{ placement: 'bottom' }}>
|
|
308
|
+
<div className={css({
|
|
309
|
+
w: '3',
|
|
310
|
+
h: '3',
|
|
311
|
+
borderRadius: 'full',
|
|
312
|
+
bg: 'success.solid'
|
|
313
|
+
})} />
|
|
314
|
+
</Tooltip>
|
|
315
|
+
|
|
316
|
+
<span>System Status</span>
|
|
317
|
+
</div>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Disabled Actions
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// Explain why action is disabled
|
|
324
|
+
const canDelete = hasPermission && itemsSelected > 0;
|
|
325
|
+
|
|
326
|
+
<Tooltip
|
|
327
|
+
content={
|
|
328
|
+
!hasPermission
|
|
329
|
+
? "You don't have permission to delete items"
|
|
330
|
+
: itemsSelected === 0
|
|
331
|
+
? "Select at least one item to delete"
|
|
332
|
+
: "Delete selected items"
|
|
333
|
+
}
|
|
334
|
+
showArrow
|
|
335
|
+
>
|
|
336
|
+
<Button disabled={!canDelete}>Delete Selected</Button>
|
|
337
|
+
</Tooltip>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Keyboard Shortcuts
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// Display keyboard shortcuts
|
|
344
|
+
<div className={css({ display: 'flex', gap: 'sm' })}>
|
|
345
|
+
<Tooltip content={<>Save <kbd>⌘S</kbd></>} showArrow>
|
|
346
|
+
<Button>Save</Button>
|
|
347
|
+
</Tooltip>
|
|
348
|
+
|
|
349
|
+
<Tooltip content={<>Open <kbd>⌘O</kbd></>} showArrow>
|
|
350
|
+
<Button>Open</Button>
|
|
351
|
+
</Tooltip>
|
|
352
|
+
|
|
353
|
+
<Tooltip content={<>Search <kbd>⌘K</kbd></>} showArrow>
|
|
354
|
+
<Button>Search</Button>
|
|
355
|
+
</Tooltip>
|
|
356
|
+
</div>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Data Visualization
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// Explain chart data points
|
|
363
|
+
<Tooltip content={`Revenue: $${dataPoint.revenue.toLocaleString()}`}>
|
|
364
|
+
<circle cx={dataPoint.x} cy={dataPoint.y} r="4" />
|
|
365
|
+
</Tooltip>
|
|
366
|
+
|
|
367
|
+
// Table cell details
|
|
368
|
+
<Tooltip content={`Last updated: ${formatDate(item.updatedAt)}`} showArrow>
|
|
369
|
+
<td>{item.name}</td>
|
|
370
|
+
</Tooltip>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## DO NOT
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// ❌ Don't use for critical information
|
|
377
|
+
<Tooltip content="Click Save to prevent data loss!">
|
|
378
|
+
<Button>Continue</Button>
|
|
379
|
+
</Tooltip>
|
|
380
|
+
// Tooltips are easily missed - use visible text or Dialog for critical info
|
|
381
|
+
|
|
382
|
+
// ❌ Don't put interactive elements in tooltips
|
|
383
|
+
<Tooltip content={
|
|
384
|
+
<div>
|
|
385
|
+
<a href="/learn-more">Learn more</a>
|
|
386
|
+
</div>
|
|
387
|
+
}>
|
|
388
|
+
<Button>Info</Button>
|
|
389
|
+
</Tooltip>
|
|
390
|
+
// Tooltip disappears on hover out - use Popover for interactive content
|
|
391
|
+
|
|
392
|
+
// ❌ Don't use overly long text
|
|
393
|
+
<Tooltip content="This tooltip has way too much text that goes on and on explaining everything in extreme detail which makes it hard to read and defeats the purpose of a quick contextual hint...">
|
|
394
|
+
<Button>Info</Button>
|
|
395
|
+
</Tooltip>
|
|
396
|
+
// Keep tooltips concise (1-2 short sentences max)
|
|
397
|
+
|
|
398
|
+
// ❌ Don't duplicate visible text
|
|
399
|
+
<Button>Save Changes</Button>
|
|
400
|
+
<Tooltip content="Save Changes"> // Redundant!
|
|
401
|
+
<IconButton><SaveIcon /></IconButton>
|
|
402
|
+
</Tooltip>
|
|
403
|
+
// Only use if adding helpful context
|
|
404
|
+
|
|
405
|
+
// ❌ Don't nest tooltips
|
|
406
|
+
<Tooltip content="Outer">
|
|
407
|
+
<Tooltip content="Inner">
|
|
408
|
+
<Button>Nested</Button>
|
|
409
|
+
</Tooltip>
|
|
410
|
+
</Tooltip>
|
|
411
|
+
// Creates confusing UX
|
|
412
|
+
|
|
413
|
+
// ❌ Don't use on disabled elements without wrapper
|
|
414
|
+
<Tooltip content="This button is disabled">
|
|
415
|
+
<Button disabled>Action</Button> // Tooltip won't show!
|
|
416
|
+
</Tooltip>
|
|
417
|
+
|
|
418
|
+
// ✅ Wrap disabled elements
|
|
419
|
+
<Tooltip content="This action requires admin privileges">
|
|
420
|
+
<span>
|
|
421
|
+
<Button disabled>Admin Action</Button>
|
|
422
|
+
</span>
|
|
423
|
+
</Tooltip>
|
|
424
|
+
|
|
425
|
+
// ✅ Keep content concise and helpful
|
|
426
|
+
<Tooltip content="Save your changes" showArrow>
|
|
427
|
+
<Button>Save</Button>
|
|
428
|
+
</Tooltip>
|
|
429
|
+
|
|
430
|
+
// ✅ Use Popover for interactive content
|
|
431
|
+
<Popover>
|
|
432
|
+
<PopoverTrigger>
|
|
433
|
+
<Button>More Info</Button>
|
|
434
|
+
</PopoverTrigger>
|
|
435
|
+
<PopoverContent>
|
|
436
|
+
<a href="/docs">View Documentation</a>
|
|
437
|
+
</PopoverContent>
|
|
438
|
+
</Popover>
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Accessibility
|
|
442
|
+
|
|
443
|
+
The Tooltip component follows WCAG 2.1 Level AA standards:
|
|
444
|
+
|
|
445
|
+
- **ARIA Attributes**: Proper `role="tooltip"` and `aria-describedby` relationships
|
|
446
|
+
- **Keyboard Support**: Tooltips appear on focus, dismiss on Escape
|
|
447
|
+
- **Focus Management**: Does not trap focus (use Popover for interactive content)
|
|
448
|
+
- **Dismiss Behavior**: Automatically dismisses on Escape, blur, or scroll
|
|
449
|
+
- **Screen Readers**: Content is announced when tooltip trigger receives focus
|
|
450
|
+
- **Pointer Events**: Supports both hover and focus triggers
|
|
451
|
+
|
|
452
|
+
### Accessibility Best Practices
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
// ✅ Provide aria-label on icon buttons with tooltips
|
|
456
|
+
<Tooltip content="Delete item" showArrow>
|
|
457
|
+
<IconButton aria-label="Delete"> // Screen readers use this, not tooltip
|
|
458
|
+
<TrashIcon />
|
|
459
|
+
</IconButton>
|
|
460
|
+
</Tooltip>
|
|
461
|
+
|
|
462
|
+
// ✅ Ensure tooltips don't contain essential information
|
|
463
|
+
// (Some users may not be able to trigger hover/focus)
|
|
464
|
+
<div>
|
|
465
|
+
<Button>Submit Form</Button>
|
|
466
|
+
<Tooltip content="This will send your data to our servers">
|
|
467
|
+
<IconButton aria-label="More information">
|
|
468
|
+
<InfoIcon />
|
|
469
|
+
</IconButton>
|
|
470
|
+
</Tooltip>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
// ✅ Use appropriate delays for different contexts
|
|
474
|
+
// Instant for toolbar icons (users expect quick feedback)
|
|
475
|
+
<Tooltip content="Bold" openDelay={0}>
|
|
476
|
+
<IconButton aria-label="Bold">
|
|
477
|
+
<BoldIcon />
|
|
478
|
+
</IconButton>
|
|
479
|
+
</Tooltip>
|
|
480
|
+
|
|
481
|
+
// Default delay for regular buttons (prevents tooltip spam)
|
|
482
|
+
<Tooltip content="Save changes" openDelay={700}>
|
|
483
|
+
<Button>Save</Button>
|
|
484
|
+
</Tooltip>
|
|
485
|
+
|
|
486
|
+
// ✅ Wrap disabled buttons to show explanation
|
|
487
|
+
<Tooltip content="Complete required fields to enable">
|
|
488
|
+
<span>
|
|
489
|
+
<Button disabled={!isValid}>Submit</Button>
|
|
490
|
+
</span>
|
|
491
|
+
</Tooltip>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Common Accessibility Issues
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
// ❌ Missing aria-label on icon button
|
|
498
|
+
<Tooltip content="Settings">
|
|
499
|
+
<IconButton> // Screen reader has no context!
|
|
500
|
+
<SettingsIcon />
|
|
501
|
+
</IconButton>
|
|
502
|
+
</Tooltip>
|
|
503
|
+
|
|
504
|
+
// ✅ Provide both aria-label and tooltip
|
|
505
|
+
<Tooltip content="Configure application settings">
|
|
506
|
+
<IconButton aria-label="Settings">
|
|
507
|
+
<SettingsIcon />
|
|
508
|
+
</IconButton>
|
|
509
|
+
</Tooltip>
|
|
510
|
+
|
|
511
|
+
// ❌ Essential information only in tooltip
|
|
512
|
+
<Tooltip content="Required field">
|
|
513
|
+
<Input label="Email" />
|
|
514
|
+
</Tooltip>
|
|
515
|
+
|
|
516
|
+
// ✅ Show required indicator visibly
|
|
517
|
+
<Input label="Email *" required />
|
|
518
|
+
<Tooltip content="We'll never share your email">
|
|
519
|
+
<IconButton aria-label="Privacy information">
|
|
520
|
+
<InfoIcon />
|
|
521
|
+
</IconButton>
|
|
522
|
+
</Tooltip>
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Content Guidelines
|
|
526
|
+
|
|
527
|
+
### Keep It Concise
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
// ❌ Too verbose
|
|
531
|
+
<Tooltip content="This button will save all of your changes to the database and then redirect you to the dashboard page where you can view your saved data">
|
|
532
|
+
<Button>Save</Button>
|
|
533
|
+
</Tooltip>
|
|
534
|
+
|
|
535
|
+
// ✅ Concise and clear
|
|
536
|
+
<Tooltip content="Save changes and return to dashboard">
|
|
537
|
+
<Button>Save</Button>
|
|
538
|
+
</Tooltip>
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Be Specific
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
// ❌ Vague
|
|
545
|
+
<Tooltip content="Click here">
|
|
546
|
+
<Button>Export</Button>
|
|
547
|
+
</Tooltip>
|
|
548
|
+
|
|
549
|
+
// ✅ Specific and actionable
|
|
550
|
+
<Tooltip content="Download data as CSV file">
|
|
551
|
+
<Button>Export</Button>
|
|
552
|
+
</Tooltip>
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Use Title Case for Actions
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
// ❌ Inconsistent capitalization
|
|
559
|
+
<Tooltip content="save your changes">
|
|
560
|
+
<IconButton aria-label="Save"><SaveIcon /></IconButton>
|
|
561
|
+
</Tooltip>
|
|
562
|
+
|
|
563
|
+
// ✅ Consistent title case
|
|
564
|
+
<Tooltip content="Save Your Changes">
|
|
565
|
+
<IconButton aria-label="Save"><SaveIcon /></IconButton>
|
|
566
|
+
</Tooltip>
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Explain Why, Not Just What
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
// ❌ States the obvious
|
|
573
|
+
<Button disabled>Delete</Button>
|
|
574
|
+
<Tooltip content="Delete button"> // User can see it's a delete button
|
|
575
|
+
<span><Button disabled>Delete</Button></span>
|
|
576
|
+
</Tooltip>
|
|
577
|
+
|
|
578
|
+
// ✅ Explains constraint
|
|
579
|
+
<Tooltip content="Select items to delete">
|
|
580
|
+
<span><Button disabled>Delete</Button></span>
|
|
581
|
+
</Tooltip>
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## Positioning Best Practices
|
|
585
|
+
|
|
586
|
+
### Optimal Placement by Context
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
// Icon toolbar - bottom placement (doesn't obscure content above)
|
|
590
|
+
<Tooltip content="Bold text" positioning={{ placement: 'bottom' }}>
|
|
591
|
+
<IconButton aria-label="Bold"><BoldIcon /></IconButton>
|
|
592
|
+
</Tooltip>
|
|
593
|
+
|
|
594
|
+
// Form help - right placement (keeps form labels visible)
|
|
595
|
+
<div className={css({ display: 'flex', gap: 'sm' })}>
|
|
596
|
+
<Input label="Username" />
|
|
597
|
+
<Tooltip content="4-20 characters, letters and numbers only" positioning={{ placement: 'right' }}>
|
|
598
|
+
<IconButton aria-label="Username requirements"><InfoIcon /></IconButton>
|
|
599
|
+
</Tooltip>
|
|
600
|
+
</div>
|
|
601
|
+
|
|
602
|
+
// Table actions - left placement (prevents overflow)
|
|
603
|
+
<Tooltip content="Delete row" positioning={{ placement: 'left' }}>
|
|
604
|
+
<IconButton aria-label="Delete"><TrashIcon /></IconButton>
|
|
605
|
+
</Tooltip>
|
|
606
|
+
|
|
607
|
+
// Top navigation - bottom placement (natural reading order)
|
|
608
|
+
<Tooltip content="User profile" positioning={{ placement: 'bottom' }}>
|
|
609
|
+
<IconButton aria-label="Profile"><UserIcon /></IconButton>
|
|
610
|
+
</Tooltip>
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Edge Detection
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
// Let tooltip auto-flip near edges (default behavior)
|
|
617
|
+
<Tooltip
|
|
618
|
+
content="This tooltip will flip to stay on screen"
|
|
619
|
+
positioning={{
|
|
620
|
+
placement: 'top',
|
|
621
|
+
flip: true, // Default
|
|
622
|
+
slide: true // Default
|
|
623
|
+
}}
|
|
624
|
+
>
|
|
625
|
+
<Button>Near Edge</Button>
|
|
626
|
+
</Tooltip>
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
## Responsive Considerations
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
// Disable tooltips on touch devices (hover isn't reliable)
|
|
633
|
+
const isTouchDevice = 'ontouchstart' in window;
|
|
634
|
+
|
|
635
|
+
<Tooltip content="Hover for info" disabled={isTouchDevice}>
|
|
636
|
+
<Button>Info</Button>
|
|
637
|
+
</Tooltip>
|
|
638
|
+
|
|
639
|
+
// Alternative: Use openDelay={0} for touch-friendly instant feedback
|
|
640
|
+
<Tooltip content="Quick info" openDelay={0}>
|
|
641
|
+
<Button>Info</Button>
|
|
642
|
+
</Tooltip>
|
|
643
|
+
|
|
644
|
+
// Mobile: Consider using Dialog instead for complex information
|
|
645
|
+
const [showInfo, setShowInfo] = useState(false);
|
|
646
|
+
|
|
647
|
+
<>
|
|
648
|
+
<Button onClick={() => setShowInfo(true)}>Info</Button>
|
|
649
|
+
{isMobile && (
|
|
650
|
+
<Dialog open={showInfo} onOpenChange={setShowInfo}>
|
|
651
|
+
<DialogContent>
|
|
652
|
+
<DialogTitle>Information</DialogTitle>
|
|
653
|
+
<DialogDescription>
|
|
654
|
+
Detailed information that would be in a tooltip
|
|
655
|
+
</DialogDescription>
|
|
656
|
+
</DialogContent>
|
|
657
|
+
</Dialog>
|
|
658
|
+
)}
|
|
659
|
+
{!isMobile && (
|
|
660
|
+
<Tooltip content="Brief info">
|
|
661
|
+
<Button>Info</Button>
|
|
662
|
+
</Tooltip>
|
|
663
|
+
)}
|
|
664
|
+
</>
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
## Performance Considerations
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// ✅ Tooltip uses lazy mounting (only renders when opened)
|
|
671
|
+
// ✅ Unmounts on exit by default (cleans up DOM)
|
|
672
|
+
<Tooltip content="Efficiently rendered">
|
|
673
|
+
<Button>Optimized</Button>
|
|
674
|
+
</Tooltip>
|
|
675
|
+
|
|
676
|
+
// For frequently toggled tooltips, you can disable unmounting
|
|
677
|
+
<Tooltip
|
|
678
|
+
content="Stays mounted for faster reopening"
|
|
679
|
+
lazyMount={false}
|
|
680
|
+
unmountOnExit={false}
|
|
681
|
+
>
|
|
682
|
+
<Button>Frequently Hovered</Button>
|
|
683
|
+
</Tooltip>
|
|
684
|
+
|
|
685
|
+
// Portalling (default) ensures proper z-index stacking
|
|
686
|
+
// Only disable if you have specific layout requirements
|
|
687
|
+
<Tooltip content="Portalled by default" portalled={true}>
|
|
688
|
+
<Button>Default Behavior</Button>
|
|
689
|
+
</Tooltip>
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
## Testing
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
696
|
+
import userEvent from '@testing-library/user-event';
|
|
697
|
+
|
|
698
|
+
test('tooltip appears on hover', async () => {
|
|
699
|
+
const user = userEvent.setup();
|
|
700
|
+
|
|
701
|
+
render(
|
|
702
|
+
<Tooltip content="Helpful information">
|
|
703
|
+
<button>Hover me</button>
|
|
704
|
+
</Tooltip>
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
const trigger = screen.getByRole('button', { name: 'Hover me' });
|
|
708
|
+
|
|
709
|
+
// Tooltip should not be visible initially
|
|
710
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
|
711
|
+
|
|
712
|
+
// Hover over trigger
|
|
713
|
+
await user.hover(trigger);
|
|
714
|
+
|
|
715
|
+
// Tooltip should appear
|
|
716
|
+
await waitFor(() => {
|
|
717
|
+
expect(screen.getByRole('tooltip')).toBeInTheDocument();
|
|
718
|
+
expect(screen.getByText('Helpful information')).toBeVisible();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// Unhover
|
|
722
|
+
await user.unhover(trigger);
|
|
723
|
+
|
|
724
|
+
// Tooltip should disappear
|
|
725
|
+
await waitFor(() => {
|
|
726
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test('tooltip appears on focus for keyboard users', async () => {
|
|
731
|
+
const user = userEvent.setup();
|
|
732
|
+
|
|
733
|
+
render(
|
|
734
|
+
<Tooltip content="Keyboard accessible">
|
|
735
|
+
<button>Focus me</button>
|
|
736
|
+
</Tooltip>
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
const trigger = screen.getByRole('button');
|
|
740
|
+
|
|
741
|
+
// Tab to focus
|
|
742
|
+
await user.tab();
|
|
743
|
+
expect(trigger).toHaveFocus();
|
|
744
|
+
|
|
745
|
+
// Tooltip should appear
|
|
746
|
+
await waitFor(() => {
|
|
747
|
+
expect(screen.getByRole('tooltip')).toBeInTheDocument();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// Tab away
|
|
751
|
+
await user.tab();
|
|
752
|
+
|
|
753
|
+
// Tooltip should disappear
|
|
754
|
+
await waitFor(() => {
|
|
755
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test('disabled tooltip returns children only', () => {
|
|
760
|
+
render(
|
|
761
|
+
<Tooltip content="Never shown" disabled>
|
|
762
|
+
<button>Child element</button>
|
|
763
|
+
</Tooltip>
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
const button = screen.getByRole('button', { name: 'Child element' });
|
|
767
|
+
expect(button).toBeInTheDocument();
|
|
768
|
+
|
|
769
|
+
// Tooltip wrapper should not exist
|
|
770
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test('tooltip with custom positioning', async () => {
|
|
774
|
+
const user = userEvent.setup();
|
|
775
|
+
|
|
776
|
+
render(
|
|
777
|
+
<Tooltip
|
|
778
|
+
content="Positioned tooltip"
|
|
779
|
+
positioning={{ placement: 'bottom' }}
|
|
780
|
+
showArrow
|
|
781
|
+
>
|
|
782
|
+
<button>Trigger</button>
|
|
783
|
+
</Tooltip>
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
await user.hover(screen.getByRole('button'));
|
|
787
|
+
|
|
788
|
+
await waitFor(() => {
|
|
789
|
+
const tooltip = screen.getByRole('tooltip');
|
|
790
|
+
expect(tooltip).toBeInTheDocument();
|
|
791
|
+
expect(tooltip).toHaveAttribute('data-placement', 'bottom');
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test('instant tooltip with no delay', async () => {
|
|
796
|
+
const user = userEvent.setup({ delay: null });
|
|
797
|
+
|
|
798
|
+
render(
|
|
799
|
+
<Tooltip content="Instant tooltip" openDelay={0}>
|
|
800
|
+
<button>Hover</button>
|
|
801
|
+
</Tooltip>
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
await user.hover(screen.getByRole('button'));
|
|
805
|
+
|
|
806
|
+
// Should appear immediately
|
|
807
|
+
expect(screen.getByRole('tooltip')).toBeInTheDocument();
|
|
808
|
+
});
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
## Related Components
|
|
812
|
+
|
|
813
|
+
- **Popover** - For interactive content that requires user interaction (clicks, form inputs)
|
|
814
|
+
- **Dialog** - For critical information or complex interactions requiring focus
|
|
815
|
+
- **IconButton** - Common trigger element for tooltips (provide aria-label!)
|
|
816
|
+
- **Button** - Standard trigger element for action tooltips
|
|
817
|
+
- **HoverCard** - For rich preview content (larger, more complex than tooltips)
|
|
818
|
+
|
|
819
|
+
## When to Use Tooltip vs. Alternatives
|
|
820
|
+
|
|
821
|
+
| Scenario | Use | Reasoning |
|
|
822
|
+
| ------------------------ | ---------------------------- | --------------------------------- |
|
|
823
|
+
| Icon button explanation | Tooltip | Brief, supplementary info |
|
|
824
|
+
| Form field help text | Tooltip or visible help text | Critical help should be visible |
|
|
825
|
+
| Interactive content | Popover | Tooltips disappear on unhover |
|
|
826
|
+
| Critical information | Visible text or Dialog | Tooltips are easily missed |
|
|
827
|
+
| Truncated text preview | Tooltip | Shows full text on hover |
|
|
828
|
+
| Keyboard shortcuts | Tooltip | Supplementary, not essential |
|
|
829
|
+
| Complex data details | HoverCard or Popover | Richer layout needed |
|
|
830
|
+
| Mobile-primary UI | Visible text or Dialog | Touch doesn't have reliable hover |
|
|
831
|
+
| Required field indicator | Visible asterisk | Must be immediately visible |
|
|
832
|
+
| Error messages | Visible text | Critical for form validation |
|