Skip to content

Commit d457604

Browse files
committed
chore: add agent file for tooltip with change history
1 parent 9e76741 commit d457604

1 file changed

Lines changed: 306 additions & 0 deletions

File tree

  • packages/pluggableWidgets/tooltip-web
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
# Tooltip Widget - Agent Context Guide
2+
3+
**Purpose:** This document provides AI agents with essential context for working on the Mendix Tooltip widget.
4+
5+
## Widget Overview
6+
7+
The Tooltip widget displays contextual information when users interact with trigger elements. It supports:
8+
9+
- Multiple trigger modes: hover, click, hover+focus
10+
- Text or custom HTML content
11+
- Multiple positioning options with floating-ui
12+
- Full accessibility via screen reader support
13+
14+
**Key Files:**
15+
16+
- `src/components/Tooltip.tsx` - Main component with accessibility logic
17+
- `src/utils/useFloatingUI.ts` - Floating-ui integration for positioning
18+
- `src/ui/Tooltip.scss` - Styles including sr-only class
19+
- `tooltip-accessibility-implementation.md` - Complete implementation guide
20+
- `aria-live-vs-aria-describedby-analysis.md` - ARIA pattern decision rationale
21+
22+
## Critical Architecture Decisions
23+
24+
### 1. Dual-Content Pattern (DO NOT REMOVE)
25+
26+
The widget renders tooltip content TWICE for accessibility:
27+
28+
```tsx
29+
{
30+
/* Sr-only: Always in DOM for screen readers */
31+
}
32+
<div id={contentId} className="sr-only" aria-hidden="true">
33+
{content}
34+
</div>;
35+
36+
{
37+
/* Visual: Conditionally rendered for sighted users */
38+
}
39+
{
40+
showTooltip && <div className="widget-tooltip-content">{content}</div>;
41+
}
42+
```
43+
44+
**Why:** Screen readers need content to be in DOM before interaction, but visual tooltip appears on-demand.
45+
46+
**DO NOT:**
47+
48+
- ❌ Remove sr-only content (breaks screen reader support)
49+
- ❌ Use only `aria-live` (wrong pattern, see aria-live-vs-aria-describedby-analysis.md)
50+
- ❌ Try to conditionally render sr-only content (defeats its purpose)
51+
52+
### 2. DOM Manipulation for aria-describedby (REQUIRED)
53+
54+
We use `useEffect` + `querySelectorAll` to apply `aria-describedby`:
55+
56+
```tsx
57+
useEffect(() => {
58+
const focusableElements = triggerWrapperRef.current.querySelectorAll(
59+
'button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
60+
);
61+
focusableElements.forEach(element => {
62+
element.setAttribute("aria-describedby", contentId);
63+
});
64+
}, [hasTooltipContent, contentId]);
65+
```
66+
67+
**Why:** Trigger can be ANY Mendix widget (ActionButton, custom components). We can't use `cloneElement` because we don't control the widget's internals.
68+
69+
**DO NOT:**
70+
71+
- ❌ Try to use `cloneElement` to pass props to trigger (doesn't work with complex components)
72+
- ❌ Apply `aria-describedby` to wrapper div (doesn't inherit to focusable children)
73+
- ❌ Target only first focusable element (must handle multiple buttons/links)
74+
75+
### 3. Floating-UI Integration
76+
77+
Uses `@floating-ui/react` for positioning with these hooks:
78+
79+
- `useFloating` - position calculation
80+
- `useHover`, `useFocus`, `useClick` - interaction modes
81+
- `useRole` - sets `role="tooltip"` on visible tooltip
82+
- `useDismiss` - handles outside clicks and escape key
83+
84+
**DO NOT:**
85+
86+
- ❌ Remove floating-ui integration (positioning will break)
87+
- ❌ Modify `useRole` to set custom IDs (we override aria-describedby manually)
88+
89+
## Common Tasks
90+
91+
### Adding a New Trigger Mode
92+
93+
1. Add enum value to `OpenOnEnum` in typings
94+
2. Update `useFloatingUI.ts` to add new interaction hook
95+
3. Add tests in `Tooltip.spec.tsx`
96+
4. Update XML with new enum value
97+
98+
### Modifying Tooltip Content Rendering
99+
100+
**CRITICAL:** Always maintain dual rendering (sr-only + visual).
101+
102+
```tsx
103+
// ✅ Correct: Both elements get the same content
104+
const content = renderMethod === "text" ? textMessage : htmlMessage;
105+
106+
<div className="sr-only">{content}</div>;
107+
{
108+
showTooltip && <div className="widget-tooltip-content">{content}</div>;
109+
}
110+
111+
// ❌ Wrong: Different content or conditional sr-only
112+
{
113+
showTooltip && <div className="sr-only">{content}</div>;
114+
} // NO!
115+
```
116+
117+
### Styling Changes
118+
119+
- Visual tooltip styles: `.widget-tooltip-content` in `Tooltip.scss`
120+
- Sr-only styles: `.sr-only` class (DO NOT MODIFY - standard pattern)
121+
- Trigger wrapper: `.widget-tooltip-trigger`
122+
123+
**DO NOT:**
124+
125+
- ❌ Remove or modify `.sr-only` class (breaks accessibility)
126+
- ❌ Add `display: none` or `visibility: hidden` to sr-only (screen readers won't read it)
127+
128+
## Accessibility Requirements (NON-NEGOTIABLE)
129+
130+
### Must Maintain
131+
132+
1. **Sr-only content always in DOM** with `aria-hidden="true"`
133+
2. **All focusable elements** get `aria-describedby` (not just first)
134+
3. **useId() for stable IDs** across renders
135+
4. **Clean up aria-describedby** in useEffect return
136+
137+
### Testing Checklist
138+
139+
Before any commit affecting accessibility:
140+
141+
```bash
142+
cd packages/pluggableWidgets/tooltip-web
143+
pnpm run test # Must pass all 7 accessibility tests
144+
```
145+
146+
**Required tests:**
147+
148+
- ✅ aria-describedby on trigger element
149+
- ✅ Sr-only content always in DOM
150+
- ✅ Content matches (text and HTML)
151+
- ✅ No aria-describedby when no content
152+
- ✅ Multiple focusable elements all get aria-describedby
153+
154+
### Screen Reader Testing
155+
156+
If changing accessibility logic, manually test with:
157+
158+
- **NVDA** (Windows + Firefox/Chrome)
159+
- **VoiceOver** (macOS + Safari)
160+
161+
**Expected behavior:**
162+
163+
1. Tab to trigger → Announces: "Button, [tooltip text]"
164+
2. Browse mode → Sr-only div NOT reachable
165+
3. Multiple triggers → Each announces tooltip text
166+
167+
## Common Pitfalls
168+
169+
### ❌ Anti-Pattern: Removing Duplication
170+
171+
```tsx
172+
// WRONG: Trying to DRY by removing sr-only
173+
{
174+
showTooltip ? <div role="tooltip">{content}</div> : <div className="sr-only">{content}</div>;
175+
}
176+
```
177+
178+
**Problem:** Sr-only must ALWAYS be in DOM, not conditionally rendered.
179+
180+
### ❌ Anti-Pattern: Using aria-live
181+
182+
```tsx
183+
// WRONG: Trying to use aria-live instead of aria-describedby
184+
<div role="tooltip" aria-live="polite">
185+
{content}
186+
</div>
187+
```
188+
189+
**Problem:** See `aria-live-vs-aria-describedby-analysis.md` for detailed explanation. Short version: wrong pattern, causes interruptions, doesn't work with all trigger modes.
190+
191+
### ❌ Anti-Pattern: Simplifying Query Selector
192+
193+
```tsx
194+
// WRONG: Only finding first element
195+
const focusableElement = wrapper.querySelector("button");
196+
focusableElement.setAttribute("aria-describedby", id);
197+
```
198+
199+
**Problem:** Trigger can contain multiple buttons. Must use `querySelectorAll` and loop.
200+
201+
## Debugging Tips
202+
203+
### Issue: Screen reader not announcing tooltip
204+
205+
**Check:**
206+
207+
1. Does trigger element have `aria-describedby` attribute? (inspect DOM)
208+
2. Does `aria-describedby` value match sr-only div's `id`?
209+
3. Is sr-only div in DOM? (should always be present)
210+
4. Does sr-only div have `aria-hidden="true"`?
211+
212+
**Common causes:**
213+
214+
- useEffect not running (check dependencies)
215+
- querySelector not finding elements (check selector)
216+
- Content ID mismatch (check useId() value)
217+
218+
### Issue: Tooltip not appearing visually
219+
220+
**Check:**
221+
222+
1. Is `showTooltip` state true? (React DevTools)
223+
2. Are floating-ui refs attached? (check `refs.setReference`, `refs.setFloating`)
224+
3. Is `getReferenceProps` spread correctly?
225+
4. Check browser console for floating-ui errors
226+
227+
**Common causes:**
228+
229+
- Trigger wrapper missing ref
230+
- OpenOn mode not matching user interaction
231+
- Floating-ui middleware error
232+
233+
## References
234+
235+
### Detailed Documentation
236+
237+
- **`tooltip-accessibility-implementation.md`** - Complete implementation guide
238+
- Problem statement and root cause
239+
- Solution architecture with code walkthroughs
240+
- WCAG compliance details
241+
- Browser/screen reader compatibility
242+
- Testing recommendations
243+
244+
- **`aria-live-vs-aria-describedby-analysis.md`** - Why we chose aria-describedby
245+
- Detailed comparison of approaches
246+
- Limitations of aria-live for tooltips
247+
- Industry standards (GitHub, Bootstrap, Material-UI)
248+
- Real-world testing results
249+
250+
### External Resources
251+
252+
- [ARIA 1.2 - aria-describedby](https://www.w3.org/TR/wai-aria-1.2/#aria-describedby)
253+
- [Floating UI - Tooltip](https://floating-ui.com/docs/tooltip)
254+
- [WCAG 2.1 - 4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html)
255+
256+
## Quick Decision Tree
257+
258+
**"Should I modify the sr-only content rendering?"**
259+
→ Only if also modifying visual tooltip. Keep them in sync.
260+
261+
**"Can I remove the DOM manipulation useEffect?"**
262+
→ No. Required for complex trigger components.
263+
264+
**"Should I switch to aria-live?"**
265+
→ No. Read `aria-live-vs-aria-describedby-analysis.md` first.
266+
267+
**"Can I optimize by caching querySelector results?"**
268+
→ No. Elements can change between renders.
269+
270+
**"Should I apply aria-describedby to the wrapper instead?"**
271+
→ No. Focusable elements inside need the attribute directly.
272+
273+
## Version History
274+
275+
See `CHANGELOG.md` for release notes.
276+
277+
**Current Status:**
278+
279+
- ✅ Screen reader accessible (aria-describedby pattern)
280+
- ✅ Supports multiple focusable elements
281+
- ✅ Works with all trigger modes
282+
- ✅ WCAG 2.1 Level A compliant
283+
- ✅ Compatible with all modern browsers + screen readers
284+
285+
## Code Review Checklist
286+
287+
Before approving changes to this widget:
288+
289+
- [ ] Sr-only content still always in DOM
290+
- [ ] All focusable elements get aria-describedby (not just first)
291+
- [ ] useEffect cleanup removes aria-describedby
292+
- [ ] All 7 accessibility tests pass
293+
- [ ] No use of aria-live for tooltip content
294+
- [ ] floating-ui integration intact
295+
- [ ] CHANGELOG.md updated for user-facing changes
296+
297+
## Emergency Contacts
298+
299+
If you need to make changes but are unsure:
300+
301+
1. Read `tooltip-accessibility-implementation.md` (comprehensive guide)
302+
2. Check `aria-live-vs-aria-describedby-analysis.md` (explains key decisions)
303+
3. Run tests: `cd packages/pluggableWidgets/tooltip-web && pnpm run test`
304+
4. Test manually with screen reader if accessibility affected
305+
306+
**Remember:** This widget's complexity is justified by accessibility requirements. Simplifications often break screen reader support.

0 commit comments

Comments
 (0)