elastic / elastic/eui

[EuiToolTip] Tooltip does not dismiss on mouse-out for aria-disabled anchors

Open
#9,997 0 comments 0 reactions 1 assignee Claimed by @weronikaolejniczak View on GitHub
accessibility
Dominant language
TypeScript
Stars
6.4k
Forks
911
Avg merge
2d 11h
Merged PRs (30d)
65

Description

**Reported by**

Found while reviewing https://github.com/elastic/eui/pull/9992.

**Describe the bug**

When an `EuiToolTip` wraps an element that is semantically disabled via `aria-disabled` (i.e. an EUI button with `isDisabled` + `hasAriaDisabled`) rather than the native `disabled` attribute, the tooltip appears on hover but **never dismisses on mouse-out**. It stays on screen until something else forces it to unmount.

`euiToolTipAnchor` neutralises pointer events only for natively disabled elements:

https://github.com/elastic/eui/blob/main/packages/eui/src/components/tool_tip/tool_tip.styles.ts#L86-L97

```css
*[disabled] {
pointer-events: none;
}
```

That rule exists precisely to prevent this failure — its comment reads *"Disabled elements don't fire mouse events, which means leaving a disabled element wouldn't trigger the onMouseOut and hide the tooltip."*

`hasAriaDisabled` swaps `disabled` for `aria-disabled="true"`, so the selector stops matching and `pointer-events` stays `auto`. The element then becomes a live hit-test target, and `useEuiDisabledElement`'s capture-phase listeners call `stopImmediatePropagation()`/`stopPropagation()` on `mouseout`/`mouseover`. React delegates those at the root to synthesise `onMouseEnter`/`onMouseLeave`, so the anchor's `onMouseLeave` → `hideToolTip()` never runs.

**Impact and severity**

Moderate. A stale tooltip can obscure adjacent UI until the user interacts elsewhere. Keyboard focus/blur is unaffected — this is mouse-only.

These call sites already pair `isDisabled` with `hasAriaDisabled` inside a tooltip and should all be affected:

- `packages/eui/src/components/basic_table/default_item_action.tsx`
- `packages/eui/src/components/basic_table/collapsed_item_actions.tsx`
- `packages/eui/src/components/context_menu/context_menu_item.tsx`
- `packages/eui/src/components/button/button_group/button_group.tsx`
- `packages/eui/src/components/list_item_layout/_list_item_layout.tsx`
- `packages/eui/src/components/date_picker/auto_refresh/auto_refresh.tsx`

Workaround: apply `pointer-events: none` to the disabled element yourself, which is what the tooltip docs already advise for custom elements.

**Environment and versions**

- EUI version: `main` (the affected style rule and hook are both on `main`; reproduced against the branch in #9992, which newly surfaces it on flyout menu actions)
- Browser: Chromium (via Playwright)

**To Reproduce**

```tsx

```

1. Move the pointer onto the button — the tooltip appears.
2. Move the pointer away.
3. The tooltip remains visible indefinitely.

Note: the pointer must move across the element in steps. Instantly warping the cursor (as some automated tests do) will not generate the intermediate `mouseover` and can mask the bug.

**Expected behavior**

The tooltip should hide on mouse-out, matching the behavior of a natively `disabled` anchor.

**Additional context**

Verified fix — extending the existing rule restores mouse-out dismissal while leaving keyboard focus/blur working:

```diff
- *[disabled] {
+ *[disabled],
+ *[aria-disabled='true'] {
pointer-events: none;
}
```

A/B measured in-browser on the same story:

| | hover on | hover off |
| --- | --- | --- |
| `pointer-events: auto` (current) | tooltip shown | **tooltip still shown** |
| `pointer-events: none` (fix) | tooltip shown | dismissed |

This also aligns the component with its own documentation, which already tells consumers to set `pointer-events: none` on `aria-disabled` elements:

https://github.com/elastic/eui/blob/main/packages/website/docs/components/display/tooltip.mdx#L138

Existing coverage misses this because `tool_tip.spec.tsx` fires synthetic events directly at the anchor wrapper, bypassing the child entirely:

https://github.com/elastic/eui/blob/main/packages/eui/src/components/tool_tip/tool_tip.spec.tsx#L59-L79

A regression test should use `cy.realHover()` on the child element and then move the pointer away.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.