-
-
Notifications
You must be signed in to change notification settings - Fork 46
Add tooltips to buttons and icons for better UX #122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
b398585
Add tooltips to buttons and icons for better UX
sarvesh0415 d8af22e
Fixes #103: add tooltips to buttons and links across credits and home…
sarvesh0415 8fb9b6b
Add tooltips to author links on About page
sarvesh0415 2b091c4
Add tooltip to Launch Img2Num CTA link
sarvesh0415 973eb51
Fix security: add rel noopener noreferrer to external links
sarvesh0415 b9f251d
Merge remote-tracking branch 'upstream/main' into add-tooltips
sarvesh0415 9764226
Fix tooltip usage, spacing, and Hero prop issues
sarvesh0415 17b09f2
Fix Tooltip: remove runtime error and use stable useId
sarvesh0415 ee0c8e4
Add Tooltip tests and documentation
sarvesh0415 26511e1
Fix Tooltip test import path
sarvesh0415 7746058
test: fix tooltip hide behavior
sarvesh0415 445da51
chore(WasmImageProcessor): add back important information - was comme…
Ryan-Millard f699601
docs(React Component: Tooltip): add docs for Tooltips & its tests
Ryan-Millard f077653
feat(tooltip): improve focus handling and support for external links
Ryan-Millard 4ae912f
docs(React Component: Tooltip): update docs with changes from prev co…
Ryan-Millard File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| { | ||
| "label": "📏 Guidelines", | ||
| "label": "Guidelines", | ||
| "position": 4, | ||
| "link": { | ||
| "type": "generated-index", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| { | ||
| "label": "⚡ Project Scripts", | ||
| "label": "Project Scripts", | ||
| "position": 5, | ||
| "link": { | ||
| "type": "generated-index", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| --- | ||
| title: Tooltip.jsx | ||
| --- | ||
|
|
||
| **What this file covers (quick):** | ||
| - How to use the `Tooltip` component | ||
| - Props & defaults | ||
| - Accessibility | ||
| - Short implementation caveats | ||
|
|
||
| ## Dependencies | ||
| - [`react-tooltip`](https://www.npmjs.com/package/react-tooltip). | ||
|
|
||
| ## Basic usage | ||
| ```jsx | ||
| import Tooltip from '@components/Tooltip' | ||
|
|
||
| export default function Example() { | ||
| return ( | ||
| <Tooltip content="Helpful hint"> | ||
| <button>Hover or focus me</button> | ||
| </Tooltip> | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| The `Tooltip` will attach the attributes `data-tooltip-id` and `data-tooltip-content` to the element you pass as `children` when possible. | ||
| If you pass a non-element child (plain text or multiple nodes), the component wraps them in a focusable `<span tabIndex={0}>` | ||
| so keyboard users can discover the tooltip. The actual tooltip element is rendered by `react-tooltip` and appended to `document.body` as a portal. | ||
|
|
||
| ## Props | ||
| | Prop | Type | Required | Default | Notes | | ||
| | -------------------- | -------: | -------: | --------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `content` | `string` | Yes | | Text shown inside the tooltip. Keep it short - tooltips are for hints. | | ||
| | `children` | `node` | Yes | | Element that triggers the tooltip. Prefer a single React element (`<button>`, `<a>`, `<Link>`, etc.). If a non-element is passed (string / fragment) the component wraps it in a focusable `<span>`. | | ||
| | `id` | `string` | No | generated | Optional ID to control multiple tooltips. | | ||
| | `dynamicPositioning` | `bool` | No | `true` | When `true`, fallback placements `['bottom','top','left']` will be tried if the preferred placement (`place="right"`) doesn't fit. When `false` no fallbacks are provided. | | ||
| :::note | ||
| `children` remains typed as `node` so you can pass text, small fragments or an element, | ||
| but the component behaves best when given **a single element** so attributes can be attached directly. | ||
| ::: | ||
|
|
||
| ## Accessibility & Link behaviour | ||
| - `react-tooltip` renders a node with `role="tooltip"`; screen-readers can discover the tooltip content through that node. | ||
| - **Keyboard support:** the component enables focus-triggered tooltips using `openOnFocus`. | ||
| For the tooltip to open on keyboard navigation, the element that receives focus must be the same element the tooltip is attached to: | ||
| - If you pass a focusable element such as `<button>`, `<a>`, or a `<Link>` component, | ||
| `Tooltip` will attach the necessary attributes to that element and `openOnFocus` will work as expected. | ||
| - If you pass plain text or a non-focusable element, `Tooltip` will wrap it in a `<span tabIndex={0}>` so it becomes keyboard focusable. | ||
| - **Important:** Do **not** wrap a focusable child inside an extra `tabIndex={0}` element - this creates two tab stops (double focus). | ||
| Prefer giving the tooltip attributes directly to the interactive element. | ||
| For example, wrap the `<a>` with `Tooltip` rather than putting `Tooltip` inside the `<a>` with a nested focusable wrapper. | ||
|
|
||
| ### Good: attach tooltip to the interactive element | ||
| ```jsx | ||
| <Tooltip content="Open project on GitHub (opens in new tab)"> | ||
| <a href="https://github.com/..." target="_blank" rel="noopener noreferrer"> | ||
| GitHub | ||
| </a> | ||
| </Tooltip> | ||
| ``` | ||
|
|
||
| ### Bad: wrapping the interactive element inside a focusable wrapper (creates duplicate focus targets) | ||
| ```jsx | ||
| /* avoid this */ | ||
| <a href="..." target="_blank" rel="noopener noreferrer"> | ||
| <Tooltip content="..."> | ||
| <span>GitHub</span> {/* the span might be focusable and compete with the link */} | ||
| </Tooltip> | ||
| </a> | ||
| ``` | ||
|
|
||
| ## Implementation notes (what the component does) | ||
| - The component tries to attach `data-tooltip-id` and `data-tooltip-content` directly to the single React child you pass by cloning it. | ||
| This preserves semantics for `<a>`, `<button>` and `<Link>` components and avoids double tab stops. | ||
| If `children` is not a valid single element, the component renders a `<span tabIndex={0}>` wrapper and attaches the attributes there. | ||
| - `appendTo={document.body}` and `positionStrategy="fixed"` - the tooltip is rendered as a portal to the document body so it sits above layout and isn’t clipped by scroll/overflow. | ||
| - `useId()` is used to generate a stable id at runtime; you can pass your own `id` prop if you need deterministic IDs. | ||
| - `dynamicPositioning` default `true` provides fallback placements when the preferred placement doesn't fit. Set to `false` to force a single placement. | ||
| - `openOnFocus` is enabled so keyboard users can open the tooltip when the trigger element receives focus. Make sure the trigger element is focusable (native element or wrapper with `tabIndex={0}`). | ||
| - Styling & animations: `react-tooltip` adds classes and may apply show/hide transitions. If you change global CSS or reset transitions you may affect tooltip visibility timing and tests. | ||
|
|
||
| ## Examples | ||
| ```jsx title="Button (works out of the box)" | ||
| <Tooltip content="Do the thing"> | ||
| <button type="button">Action</button> | ||
| </Tooltip> | ||
| ``` | ||
| ```jsx title="Internal navigation (react-router Link)" | ||
| <Tooltip content="Go to profile"> | ||
| <Link to="/profile">Profile</Link> | ||
| </Tooltip> | ||
| ``` | ||
| ```jsx title="External link (attach tooltip to the <a> itself — target & rel recommended)" | ||
| <Tooltip content="Open on GitHub (opens in a new tab)"> | ||
| <a href="https://github.com/..." target="_blank" rel="noopener noreferrer">GitHub</a> | ||
| </Tooltip> | ||
| ``` | ||
| ```jsx title="Plain text / complex non-focusable nodes (gets wrapped in a focusable span)" | ||
| <Tooltip content="Short hint"> | ||
| Some inline text or an icon-only element | ||
| </Tooltip> | ||
| ``` | ||
|
|
||
| ## Testing tips | ||
| - `react-tooltip` mounts the tooltip node into `document.body`. | ||
| In the `jsdom` environment `screen` queries will still find it. | ||
| - Portal timing & animations can make tests flaky. | ||
| Wrap assertions in `await waitFor()` or use `findBy*` queries which retry until the element appears. Example patterns: | ||
|
|
||
| ```js title="Hover" | ||
| await user.hover(screen.getByText('Hover me')) | ||
| expect(await screen.findByText('Hello tooltip')).toBeVisible() | ||
| ``` | ||
| ```js title="Focus" | ||
| await user.tab() | ||
| expect(await screen.findByText('Hello tooltip')).toBeVisible() | ||
| ``` | ||
| ```js title="Hide with waitFor to accommodate transition" | ||
| await waitFor(() => expect(screen.queryByText('Hello tooltip')).not.toBeInTheDocument()) | ||
| ``` | ||
| - In tests prefer passing an actual element as `children` (button, Link, or anchor) | ||
| so the library attributes are attached directly and keyboard focus works reliably. | ||
| If you need to assert behavior for plain text triggers, test the wrapped `<span>` behavior explicitly. | ||
| - If you see intermittent failures due to CSS transitions, disable transitions in your test setup | ||
| (for example, add a small global CSS rule to turn off transitions during tests) - this makes timing deterministic. | ||
|
|
||
| ## Summary | ||
| - The `Tooltip` prefers to attach attributes directly to a single React child (preserves semantics for `<a>`, `<button>`, `<Link>`). | ||
| - If you pass non-element children, the component wraps them in a focusable `<span>` so keyboard users can reveal the tooltip. | ||
| - For external links (anchors), wrap the anchor with `Tooltip` (so tooltip attributes are attached to the anchor) - | ||
| do **not** make an extra focusable wrapper inside the anchor. | ||
| - `openOnFocus` + focusable trigger = keyboard-accessible tooltip. |
|
Ryan-Millard marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| --- | ||
| title: Tests | ||
| --- | ||
|
|
||
| This page documents the Vitest test suite for the `Tooltip` component and provides guidance for writing reliable tests, | ||
| including for keyboard accessibility and portal behavior. | ||
|
|
||
| ## Individual Test Explanations | ||
| 1. **does not show tooltip content by default** | ||
| - Ensures the tooltip is not rendered until user interaction. | ||
| `react-tooltip` appends the tooltip node to `document.body`, accessible via `screen` queries in jsdom. | ||
| 2. **shows tooltip on hover** | ||
| - `userEvent.hover` triggers pointer events. The tooltip library mounts the tooltip node in response. | ||
| 3. **hides tooltip when mouse leaves** | ||
| - Uses `waitFor()` to accommodate any hide delays or CSS transitions. | ||
| 4. **shows tooltip on keyboard focus** | ||
| - Tests accessibility: the tooltip appears when the trigger element receives keyboard focus. | ||
| 5. **hides tooltip when focus leaves** | ||
| - Confirms the tooltip hides when the trigger loses focus. `waitFor()` ensures the test accounts for any hide transition delays. | ||
|
|
||
| ## Common Flakiness and Fixes | ||
| 1. **Portal timing / animations** | ||
| - `react-tooltip` may delay mounting/unmounting for animations. | ||
| Wrap visibility assertions in `await waitFor()` or use `findBy*` queries to retry until the tooltip appears/disappears: | ||
| ```js | ||
| expect(await screen.findByText('Hello tooltip')).toBeVisible() | ||
| await waitFor(() => expect(screen.queryByText('Hello tooltip')).not.toBeInTheDocument()) | ||
| ``` | ||
| - You can also disable CSS transitions in test setup for deterministic results. | ||
| 2. **Missing jsdom environment** | ||
| - `Tooltip` renders portals to `document.body`. Ensure Vitest is running with `environment: 'jsdom'`. | ||
| 3. **Event ordering** | ||
| - Always use `userEvent.setup()` and `await` interactions. Avoid manually firing low-level events unless necessary. | ||
| 4. **Double focus / tab issues** | ||
| - When testing tooltips on interactive elements, pass a single focusable element (e.g., `<button>` or `<Link>`) | ||
| as `children`. If the component wraps non-element children in a `<span tabIndex={0}>`, account for that extra tab stop in tests. | ||
| 5. **React strict mode / act warnings** | ||
| - Vitest + Testing Library usually handles `act()` automatically. Ensure `globals: true` in your test setup and keep dependencies updated. | ||
|
|
||
| ## Testing Recommendations | ||
| - Always test tooltips attached to **real interactive elements** (buttons, links, `<Link>` from react-router-dom) to avoid duplicate tab stops. | ||
| - When testing external links or non-focusable nodes, account for the optional wrapper `<span tabIndex={0}>`. | ||
| - Use `findBy*` for asserting tooltip visibility; it retries until the tooltip appears. | ||
| - Wrap hide assertions in `waitFor()` to accommodate CSS transitions or delayed unmounts. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "label": "Components", | ||
| "position": 1, | ||
| "link": { | ||
| "type": "generated-index", | ||
| "title": "React Components", | ||
| "description": "Reference for React components in Img2Num", | ||
| "slug": "/reference/react/components" | ||
| }, | ||
| } | ||
|
Ryan-Millard marked this conversation as resolved.
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.