@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.
@@ -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 |