Skip to content

[EuiToolTip] Refactor to a functional component, memoize style and improve tests#9570

Merged
weronikaolejniczak merged 25 commits intoelastic:mainfrom
weronikaolejniczak:chore/refactor-tooltip
Apr 28, 2026
Merged

[EuiToolTip] Refactor to a functional component, memoize style and improve tests#9570
weronikaolejniczak merged 25 commits intoelastic:mainfrom
weronikaolejniczak:chore/refactor-tooltip

Conversation

@weronikaolejniczak
Copy link
Copy Markdown
Contributor

@weronikaolejniczak weronikaolejniczak commented Apr 7, 2026

Tip

There's not a lot of changes but I do recommend following commits.

Warning

Revert 68bbc21 before merging!

Summary

API Changes

🟢 No API changes.

Screenshots

🟢 No visual changes.

Impact Assessment

Note: Most PRs should be tested in Kibana to help gauge their Impact before merging.

  • 🔴 Breaking changes — What will break? How many usages in Kibana/Cloud UI are impacted?
  • 💅 Visual changes — May impact style overrides; could require visual testing. Explain and estimate impact.
  • 🧪 Test impact — May break functional or snapshot tests (e.g., HTML structure, class names, default values).
  • 🔧 Hard to integrate — If changes require substantial updates to Kibana, please stage the changes and link them here.

Impact level: 🟢 Low

(WIP - will update snapshots for Kibana before merging)

Release Readiness

  • Documentation: {link to docs page(s)}
  • Figma: {link to Figma or issue}
  • Migration guide: {steps or link, for breaking/visual changes or deprecations}
  • Adoption plan (new features): {link to issue/doc or outline who will integrate this and where}

QA instructions for reviewer

  • Verify test cases are meaningful (do not rely on CI being 🟢)

EuiToolTip

Story: Playground

  • Hover over the trigger button → tooltip appears after delay (~250ms)
  • Mouse out → tooltip hides
  • Tab to the trigger button (keyboard focus) → tooltip appears
  • Tab away (blur) → tooltip hides
  • While tooltip is focused with keyboard, press Escape → tooltip hides
  • Change position prop → tooltip appears on correct side (top/right/bottom/left)
  • Change delay to long → tooltip appears after ~1250ms
  • Change display to block → anchor wrapper becomes block-level
  • Provide title prop → title appears above content with a divider
  • Set disableScreenReaderOutput=truearia-describedby is NOT added to the trigger (note: aria-describedby is added dynamically when tooltip is shown)
  • Change offset → tooltip moves closer/further from anchor (in all directions, change position)
  • Set repositionOnScroll=true and scroll the page → tooltip follows anchor (e.g. set the container to have a large height in DevTools)

Pre-existing issue:

  • Resize the window while tooltip is visible → tooltip repositions correctly (prod, d226eb5)

EuiIconTip

Story: Playground

  • Icon renders with default question-mark type
  • Tooltip appears on hover/focus
  • aria-label is read by screen reader (set aria-label prop and use VoiceOver/NVDA)
  • Custom type, color, size props affect the icon

EuiFilterSelectItem

Story: FilterGroup / Multiple Popovers

  • Clicking on the items shows a tooltip (programmatically: showToolTip())
  • Clicking outside hides the tooltip (programmatically: hideToolTip())

EuiSelectableListItem

Story: Selectable / With Tooltip

Use Arrow keys to navigate within the selectable list.

  • Keyboard-focus on a selectable item that has a tooltip → tooltip shows (programmatically: showToolTip())
  • Blur → tooltip hides (programmatically: hideToolTip())
  • aria-describedby on the .euiSelectableListItem__content is set to the tooltip's id (check with browser inspector)

EuiCollapsedNavButton

Story: CollapsibleNav (Beta) / Playground

  • Collapsed nav item shows tooltip on hover (in collapsed mode)

EuiContextMenuItem

Story: ContextMenu / Playground

  • Context menu item with toolTipContent shows tooltip on hover

Escape key propagation

Inside a flyout

  • Escape closes tooltip only, does not close the flyout
  • After closing the tooltip, Escape closes the flyout

Inside a modal

  • Escape closes tooltip only, does not close the modal
  • After closing the tooltip, Escape closes the modal

Inside a popover

  • Escape closes tooltip only, does not close the popover
  • After closing the tooltip, Escape closes the popover

Strict Mode

  • Verify tooltip wrapped with <StrictMode> renders correctly

Checklist before marking Ready for Review

VRT passed:

Screenshot 2026-04-07 at 13 47 01

There were unrelated image reference updates. I added them on 451f7a5

Reviewer checklist

  • Approved Impact Assessment — Acceptable to merge given the consumer impact.
  • Approved Release Readiness — Docs, Figma, and migration info are sufficient to ship.

@weronikaolejniczak weronikaolejniczak self-assigned this Apr 7, 2026
@weronikaolejniczak weronikaolejniczak added the skip-changelog Use on PRs to skip changelog requirement (Don't delete - used for automation) label Apr 7, 2026
Copy link
Copy Markdown
Contributor Author

@weronikaolejniczak weronikaolejniczak Apr 7, 2026

Choose a reason for hiding this comment

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

This is an expected change. Previously, position leaked to the DOM element but it's not a valid HTML attribute. Same as offset.

@weronikaolejniczak weronikaolejniczak changed the title [EuiToolTip] Refactor to a functional component, memoize style and improve test suite [EuiToolTip] Refactor to a functional component, memoize style and improve tests Apr 8, 2026
const setPopoverRef = useCallback(
(el: HTMLElement) => {
popoverRef.current = el;
if (el) positionToolTip();
Copy link
Copy Markdown
Contributor Author

@weronikaolejniczak weronikaolejniczak Apr 8, 2026

Choose a reason for hiding this comment

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

In commit: a38e23d

This line is the only change from the diff. Otherwise, we are just moving positionToolTip to be before the setPopoverRef that calls it.

if (el) positionToolTip(); fixes tooltip not showing in StrictMode because EuiResizeObserver (class component) has ResizeObserver disconnected by StrictMode's simulated unmount before the initial callback fires, so positionToolTip is never called. Calling it directly from setPopoverRef every mount ensures that it gets positioned correctly.

This doesn't affect anything detrimentally.

};
const setPopoverRef = useCallback(
(ref: HTMLDivElement) => {
popover.current = ref;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adding this line fixes pre-existing bug where the tooltip wouldn't reposition on resizing the window.

@weronikaolejniczak weronikaolejniczak marked this pull request as ready for review April 8, 2026 15:36
@weronikaolejniczak weronikaolejniczak requested a review from a team as a code owner April 8, 2026 15:36
Copilot AI review requested due to automatic review settings April 8, 2026 15:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors EuiToolTip from a class component to a functional component with hooks, updates related components/usages, and rewrites/expands tests to use more meaningful assertions while also addressing tooltip repositioning behavior.

Changes:

  • Migrate EuiToolTip to forwardRef + hooks and expose an imperative ref API (EuiToolTipRef).
  • Memoize tooltip/anchor styles via useEuiMemoizedStyles and adjust tooltip popover behavior.
  • Rewrite and expand Jest tests + update snapshots across tooltip consumers.

Reviewed changes

Copilot reviewed 16 out of 25 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/eui/src/components/tool_tip/tool_tip.tsx Converts EuiToolTip to a hook-based functional component and introduces EuiToolTipRef.
packages/eui/src/components/tool_tip/tool_tip.test.tsx Rewrites tooltip unit tests to rely less on snapshots and add behavioral assertions.
packages/eui/src/components/tool_tip/tool_tip.stories.tsx Adds new QA-focused stories (currently flagged as TODO to remove).
packages/eui/src/components/tool_tip/tool_tip_popover.tsx Switches to memoized styles and adds resize-driven repositioning logic.
packages/eui/src/components/tool_tip/tool_tip_manager.test.ts Improves test isolation and expands assertions around singleton behavior.
packages/eui/src/components/tool_tip/tool_tip_anchor.tsx Memoizes anchor styles via useEuiMemoizedStyles.
packages/eui/src/components/tool_tip/index.ts Re-exports EuiToolTipRef type from the public tooltip entrypoint.
packages/eui/src/components/tool_tip/icon_tip.test.tsx Updates IconTip tests away from prop-specific snapshots toward behavioral checks.
packages/eui/src/components/tool_tip/snapshots/tool_tip.test.tsx.snap Updates snapshots for tooltip rendering changes.
packages/eui/src/components/tool_tip/snapshots/icon_tip.test.tsx.snap Removes outdated snapshots tied to previous IconTip prop rendering expectations.
packages/eui/src/components/selectable/selectable_list/selectable_list_item.tsx Updates tooltip ref usage to the new EuiToolTipRef API.
packages/eui/src/components/selectable/selectable_list/snapshots/selectable_list_item.test.tsx.snap Updates snapshots for tooltip attribute changes in selectable list items.
packages/eui/src/components/filter_group/filter_select_item.tsx Updates tooltip ref typing to EuiToolTipRef.
packages/eui/src/components/filter_group/filter_select_item.test.tsx Adds/updates tests for programmatic tooltip show/hide behavior.
packages/eui/src/components/context_menu/snapshots/context_menu_item.test.tsx.snap Updates tooltip snapshot output (removes invalid DOM attrs).
packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsed/snapshots/collapsed_nav_button.test.tsx.snap Updates tooltip snapshot output (removes invalid DOM attrs).
packages/eui/.loki/reference/chrome_mobile_Forms_EuiFilePicker_Controlled_With_Files.png Adds/updates Loki reference image (appears unrelated to tooltip refactor).
packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayoutDelimited_Kitchen_Sink.png Adds/updates Loki reference image (appears unrelated to tooltip refactor).
packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayoutDelimited_High_Contrast.png Adds/updates Loki reference image (appears unrelated to tooltip refactor).
packages/eui/.loki/reference/chrome_desktop_Forms_EuiFilePicker_Controlled_With_Files.png Adds/updates Loki reference image (appears unrelated to tooltip refactor).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/eui/src/components/tool_tip/tool_tip.tsx Outdated
Comment thread packages/eui/src/components/tool_tip/tool_tip.tsx
Comment thread packages/eui/src/components/tool_tip/tool_tip_popover.tsx
Comment on lines 94 to +110
display?: (typeof DISPLAYS)[number];
/**
* Delay before showing tooltip. Good for repeatable items.
*/
delay: ToolTipDelay;
delay?: ToolTipDelay;
/**
* An optional title for your tooltip.
*/
title?: ReactNode;
/**
* Unless you provide one, this will be randomly generated.
*/
id?: string;
/**
* Suggested position. If there is not enough room for it this will be changed.
*/
position: ToolTipPositions;
position?: ToolTipPositions;
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

PR description states "No API changes", but this diff makes delay optional in the public EuiToolTipProps type. Even though it's non-breaking, it's still an exported type surface change and should be reflected in the PR's API section (or reverted if unintended).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

EuiToolTip can absolutely be used without providing delay and position. position is set to top and delay to regular by default.

So no, this is not an API change. delay and position are in fact typed as optional.

Comment thread packages/eui/src/components/tool_tip/tool_tip.stories.tsx Outdated
Comment on lines 9 to 11
export type { ToolTipPositions } from './tool_tip_popover';
export type { EuiToolTipProps } from './tool_tip';
export type { EuiToolTipProps, EuiToolTipRef } from './tool_tip';
export { EuiToolTip } from './tool_tip';
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

PR description states "No API changes", but this diff re-exports a new public type (EuiToolTipRef) from the tool_tip index. Even if it's additive, please update the PR's API section to reflect the exported surface change (or keep it internal if not intended as public API).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is an undocumented public API. Let's leave this be.

Comment thread packages/eui/src/components/tool_tip/tool_tip_popover.tsx Outdated
Copy link
Copy Markdown
Member

@tkajtoch tkajtoch left a comment

Choose a reason for hiding this comment

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

Code changes look great! I'm proceeding to manual testing

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 25 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

packages/eui/src/components/tool_tip/tool_tip.tsx:396

  • When neither content nor title are provided, showToolTip can still set visible=true, which causes the trigger to receive aria-describedby (via isVisible) even though no tooltip element is rendered (visible && (content || title) gate). This produces an aria-describedby that points to a non-existent element. Consider short-circuiting showToolTip/onFocus/onMouseOver (and/or the id/isVisible wiring) when there is no tooltip content so that visible never becomes true in this case.
    return (
      <>
        <EuiToolTipAnchor
          {...anchorProps}
          ref={setAnchorRef}
          onBlur={onBlur}
          onFocus={onFocus}
          onKeyDown={onEscapeKey}
          onMouseOver={showToolTip}
          onMouseOut={onMouseOut}
          // `id` defines if the trigger and tooltip are automatically linked via `aria-describedby`.
          id={!disableScreenReaderOutput ? id : undefined}
          className={anchorClasses}
          display={display}
          isVisible={visible}
        >
          {children}
        </EuiToolTipAnchor>
        {visible && (content || title) && (
          <EuiPortal>

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/eui/src/components/tool_tip/tool_tip.tsx Outdated
Comment thread packages/eui/src/components/tool_tip/tool_tip.stories.tsx Outdated
Copy link
Copy Markdown
Member

@tkajtoch tkajtoch left a comment

Choose a reason for hiding this comment

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

Code changes look good. I went through all QA steps and tested some more combinations - everything works as expected.

While testing StrictMode, I've noticed that the tooltip doesn't appear on initial focus via autoFocus, only on subsequent hover/focus events.

@weronikaolejniczak
Copy link
Copy Markdown
Contributor Author

@tkajtoch I added a unit test and checked manually, indeed autoFocus didn't show the tooltip in StrictMode, good catch! I added a small fix on bb46282 which passed the tests. Also, "fixed" that typo you mentioned earlier on d827e24 and addressed the edge case Copilot shared above on 38163a8.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 25 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/eui/src/components/tool_tip/tool_tip.stories.tsx Outdated
@tkajtoch
Copy link
Copy Markdown
Member

I tested the StrictMode autoFocus fix locally and can confirm it works as expected. Thank you for addressing it!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 24 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@weronikaolejniczak weronikaolejniczak enabled auto-merge (squash) April 28, 2026 10:05
@elasticmachine
Copy link
Copy Markdown
Collaborator

💚 Build Succeeded

History

cc @weronikaolejniczak

@elasticmachine
Copy link
Copy Markdown
Collaborator

💚 Build Succeeded

History

cc @weronikaolejniczak

@weronikaolejniczak weronikaolejniczak merged commit 842820d into elastic:main Apr 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog Use on PRs to skip changelog requirement (Don't delete - used for automation)

Projects

None yet

4 participants