Skip to content

fix(QTooltip): show on keyboard focus & dismiss with ESC (fix #18241) - #18318

Merged
rstoenescu merged 1 commit into
quasarframework:devfrom
arbaev:fix/qtooltip-keyboard-focus
Jul 21, 2026
Merged

rstoenescu merged 1 commit into
quasarframework:devfrom
arbaev:fix/qtooltip-keyboard-focus

Conversation

@arbaev

@arbaev arbaev commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Documentation
  • Code style update
  • Refactor
  • Build-related changes
  • Other, please describe:

Does this PR introduce a breaking change?

  • Yes
  • No

The PR fulfills these requirements:

  • It's submitted to the dev branch
  • When resolving a specific issue, it's referenced in the PR's title (fix #18241)
  • It's been tested on a Cordova (iOS, Android) app
  • It's been tested on an Electron app
  • Any necessary documentation has been added or updated, or explained in the PR's description

Other information:

Fixes #18241.

Problem

QTooltip only showed on mouse hover (and touch), so keyboard-only users never saw it when tabbing to the target — a violation of WCAG 1.4.13 (Content on Hover or Focus).

Change

In the tooltip's anchor wiring (desktop):

  • Show on keyboard focus of the target, gated by :focus-visible so the tooltip does not appear on plain pointer clicks (mouse users keep hover).
  • Hide on blur.
  • Dismiss with ESC while shown, via Quasar's shared escape-key stack (same mechanism as QMenu/QDialog) — only the top-most popup reacts and focus is not moved (WCAG "dismissible").

No new props/events; respects the existing noParentEvent and persistent.
The touch/mobile path is unchanged.

Scope / note

A focusable trigger is required (QBtn, links, inputs…). A tooltip on a non-focusable element (a bare <div> without tabindex) still can't receive focus — that remains the app's responsibility.

Verification

Built from source and exercised in headless Chromium (Playwright):

  • idle: no tooltip
  • Tab focuses target → shown
  • ESC → hidden, focus stays on target
  • blur (Tab away) → hidden
  • mouse hover → shown / mouse leave → hidden
  • mouse click (focus without :focus-visible) → not shown

Cordova/Electron not separately tested — the change uses standard DOM focus/blur + :focus-visible and the existing escape-key stack, with no platform-specific code. No unit test added: QTooltip currently has no test file and the spec harness requires full-API coverage for any existing test file; the :focus-visible gating also can't be exercised under jsdom.
Happy to follow up with full QTooltip test coverage separately.

Summary by CodeRabbit

  • New Features
    • Tooltips can now be dismissed with the Escape key.
    • Tooltips appear when keyboard users focus an eligible anchor and hide when focus leaves.
  • Accessibility
    • Improved keyboard navigation and focus-visible behavior for tooltips.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

QTooltip now shows on keyboard-visible focus, hides on blur using the existing delay, and dismisses with ESC when visible and non-persistent. Escape handlers are registered through the shared stack and removed during anchor cleanup.

Changes

Tooltip accessibility

Layer / File(s) Summary
Focus display and ESC dismissal
ui/src/components/tooltip/QTooltip.js
Keyboard focus wiring shows tooltips for :focus-visible anchors, blur uses delayed hiding, and conditional ESC handling registers and cleans up the shared escape-key listener.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant KeyboardUser
  participant AnchorElement
  participant QTooltip
  participant EscapeKeyStack
  KeyboardUser->>AnchorElement: focus
  AnchorElement->>QTooltip: focusShow
  QTooltip->>QTooltip: show when :focus-visible matches
  QTooltip->>EscapeKeyStack: register onEscapeKey
  KeyboardUser->>QTooltip: press ESC
  QTooltip->>QTooltip: hide
  AnchorElement->>QTooltip: blur
  AnchorElement->>QTooltip: delayHide
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: QTooltip now shows on keyboard focus and can be dismissed with ESC.
Linked Issues check ✅ Passed The changes address #18241 by showing the tooltip on keyboard focus, hiding on blur, and adding ESC dismissal without new APIs.
Out of Scope Changes check ✅ Passed The patch stays focused on QTooltip keyboard accessibility and does not introduce unrelated code changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@yusufkandemir

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yusufkandemir

Copy link
Copy Markdown
Member

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
ui/src/components/tooltip/QTooltip.js (1)

191-201: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for the interaction matrix.

Cover keyboard focus display, pointer focus suppression, delayed blur, ESC dismissal without focus movement, persistent, noParentEvent, and repeated show/hide cleanup.

Also applies to: 316-344

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/components/tooltip/QTooltip.js` around lines 191 - 201, Add regression
tests for QTooltip covering keyboard-triggered focus display, pointer-triggered
focus suppression, delayed blur, ESC dismissal without moving focus, persistent
behavior, noParentEvent behavior, and repeated show/hide listener cleanup.
Exercise the existing focus, blur, escape-stack, and visibility paths around the
computed handlesEscape logic and related lines, including interaction
combinations rather than isolated cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ui/src/components/tooltip/QTooltip.js`:
- Around line 341-344: Update the event handling around QTooltip’s anchor
mouseenter/mouseleave/focus/blur listeners to track hover and focus
independently, and only execute delayHide when neither state remains active.
Preserve delayShow behavior while preventing blur from hiding during hover and
mouseleave from hiding during focus.
- Around line 321-326: Update the focus handling around the
el.matches(':focus-visible') check so a selector parsing failure explicitly
treats the event as pointer focus and returns before delayShow(). Preserve the
existing return for elements that do not match :focus-visible, ensuring
unsupported engines do not open the tooltip on click focus.
- Around line 343-344: Update the focus event listeners in the tooltip anchor
handling to use focusin/focusout, or otherwise account for events from nested
focusable elements, so wrapper anchors correctly show on descendant focus and
hide when focus leaves the wrapper while preserving the existing focusShow and
delayHide behavior.

---

Nitpick comments:
In `@ui/src/components/tooltip/QTooltip.js`:
- Around line 191-201: Add regression tests for QTooltip covering
keyboard-triggered focus display, pointer-triggered focus suppression, delayed
blur, ESC dismissal without moving focus, persistent behavior, noParentEvent
behavior, and repeated show/hide listener cleanup. Exercise the existing focus,
blur, escape-stack, and visibility paths around the computed handlesEscape logic
and related lines, including interaction combinations rather than isolated
cases.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c5a2b1c2-11e9-4e69-811d-265b87af0532

📥 Commits

Reviewing files that changed from the base of the PR and between 9e96a29 and acb1369.

📒 Files selected for processing (1)
  • ui/src/components/tooltip/QTooltip.js

Comment on lines +321 to +326
// only react to keyboard focus, not to focus coming from a pointer,
// so the tooltip doesn't pop up when the target is clicked;
// guard the call for engines that don't support :focus-visible
try {
if (el.matches(':focus-visible') === false) return
} catch {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 3 'focusShow|:focus-visible|Cordova|Electron' .

Repository: quasarframework/quasar

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- QTooltip context ---\n'
sed -n '280,360p' ui/src/components/tooltip/QTooltip.js

printf '\n--- focus-related search in QTooltip ---\n'
rg -n -C 3 'delayShow|focusShow|focus-visible|matches\\(' ui/src/components/tooltip/QTooltip.js

Repository: quasarframework/quasar

Length of output: 2620


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '240,340p' ui/src/components/tooltip/QTooltip.js

Repository: quasarframework/quasar

Length of output: 2728


Make the :focus-visible fallback explicit.

If el.matches(':focus-visible') throws, the empty catch still reaches delayShow(), so click focus opens the tooltip in engines that don’t parse the selector. Use an explicit keyboard/pointer fallback instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/components/tooltip/QTooltip.js` around lines 321 - 326, Update the
focus handling around the el.matches(':focus-visible') check so a selector
parsing failure explicitly treats the event as pointer focus and returns before
delayShow(). Preserve the existing return for elements that do not match
:focus-visible, ensuring unsupported engines do not open the tooltip on click
focus.

Comment on lines 341 to +344
[anchorEl.value, 'mouseenter', 'delayShow', 'passive'],
[anchorEl.value, 'mouseleave', 'delayHide', 'passive']
[anchorEl.value, 'mouseleave', 'delayHide', 'passive'],
[anchorEl.value, 'focus', 'focusShow', 'passive'],
[anchorEl.value, 'blur', 'delayHide', 'passive']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Coordinate hover and focus before hiding.

mouseenter/mouseleave and focus/blur now share delayShow/delayHide, but each hide path is unconditional. When hover and keyboard focus overlap, blur can hide a tooltip while the pointer remains over the anchor, and mouseleave can hide it while focus remains. Track both states, or gate hiding until neither trigger is active.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/components/tooltip/QTooltip.js` around lines 341 - 344, Update the
event handling around QTooltip’s anchor mouseenter/mouseleave/focus/blur
listeners to track hover and focus independently, and only execute delayHide
when neither state remains active. Preserve delayShow behavior while preventing
blur from hiding during hover and mouseleave from hiding during focus.

Comment on lines +343 to +344
[anchorEl.value, 'focus', 'focusShow', 'passive'],
[anchorEl.value, 'blur', 'delayHide', 'passive']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 4 'focusin|focusout|focusShow|<button|<input|target=' .

Repository: quasarframework/quasar

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant section of QTooltip.js with line numbers
sed -n '300,380p' ui/src/components/tooltip/QTooltip.js

# Find where anchorEl is assigned and how listeners are attached
rg -n -C 3 'anchorEl|focusShow|delayHide|focusin|focusout|addEventListener|on\(' ui/src/components/tooltip/QTooltip.js

# Check whether nested focusable support is documented or used nearby
rg -n -C 2 'focusin|focusout|nested focusable|anchorEl' ui/src/components/tooltip

Repository: quasarframework/quasar

Length of output: 9880


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the useAnchor composable and inspect how anchorEl is resolved
fd -a 'use-anchor' ui/src
fd -a 'useAnchor' ui/src
rg -n -C 4 'function useAnchor|const anchorEl|anchorEl =|anchorEl.value|configureAnchorEl|canShow' ui/src

# Read the relevant implementation if found
file=$(rg -l 'function useAnchor|const anchorEl|anchorEl.value' ui/src | head -n 1)
if [ -n "${file:-}" ]; then
  sed -n '1,260p' "$file"
fi

Repository: quasarframework/quasar

Length of output: 28803


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,260p' ui/src/composables/private.use-anchor/use-anchor.js
printf '\n--- QTooltip docs/usages ---\n'
rg -n -C 3 'QTooltip|q-tooltip|focusShow|blur|focusin|focusout|target: true|target === true' ui/src/components ui/src/composables ui/src/docs ui/src

Repository: quasarframework/quasar

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect QTooltip API/docs if present
fd -a 'QTooltip.json' ui/src
fd -a 'QTooltip.*' ui/src/components/tooltip

# Read any tooltip API file and nearby slot/docs text
for f in $(fd -a 'QTooltip.json' ui/src/components/tooltip ui/src); do
  echo "--- $f ---"
  sed -n '1,240p' "$f"
done

# Look for explicit target/anchor guidance
rg -n -C 2 '"target"|anchor|parentNode|focusin|focusout|focusShow|delayHide' ui/src/components/tooltip ui/src/composables/private.use-anchor

Repository: quasarframework/quasar

Length of output: 26628


Handle focus transitions on wrapper anchors. focus/blur only fire on anchorEl.value itself, so a tooltip anchored to a wrapper with nested focusable children won’t show/hide when focus moves to the child. Use focusin/focusout or inspect evt.target if that anchor shape is supported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/components/tooltip/QTooltip.js` around lines 343 - 344, Update the
focus event listeners in the tooltip anchor handling to use focusin/focusout, or
otherwise account for events from nested focusable elements, so wrapper anchors
correctly show on descendant focus and hide when focus leaves the wrapper while
preserving the existing focusShow and delayHide behavior.

@yusufkandemir

Copy link
Copy Markdown
Member

@coderabbitai In the ui package, components must not create reactive state that the current platform never uses. In this PR, the new computed prop and its watcher only serve desktop keyboard-focus handling, yet they are also created on mobile where they are never consumed. In future reviews of ui/src, flag computed props, watchers and event listeners that are created unconditionally but only consumed on one platform (desktop-only hover/keyboard logic, mobile-only touch logic); their creation should be gated behind the platform check. This matters because these components run on every Quasar app, so per-instance reactive overhead multiplies quickly.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@yusufkandemir Understood. For future reviews in ui/src, I’ll flag platform-specific computed state, watchers, and event listeners that are instantiated unconditionally despite being used only on desktop or mobile. They should be created within the appropriate platform gate to avoid unnecessary per-instance reactive overhead.

🐇💭


✏️ Learnings added
Learnt from: yusufkandemir
URL: https://github.com/quasarframework/quasar/pull/18318

Timestamp: 2026-07-21T11:00:18.744Z
Learning: In Quasar’s `ui/src` package, components must gate platform-specific reactive state and listeners behind the relevant platform check. During reviews, flag computed properties, watchers, and event listeners created unconditionally when they are only consumed by desktop-only hover/keyboard behavior or mobile-only touch behavior, because per-instance reactive overhead affects every Quasar application.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Failed to generate fixes. The agent execution returned an error and no code changes were found.

@rstoenescu
rstoenescu merged commit b87f366 into quasarframework:dev Jul 21, 2026
1 check passed
@rstoenescu

Copy link
Copy Markdown
Member

@arbaev Thanks for contributing! And sorry for the coderabbit noise. This will go into Quasar v2.22 later today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tool tip information which gets displayed upon hovering is not displayed with keyboard focus.

3 participants