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