feat(react-headless-components-preview): add Tooltip component#36079
feat(react-headless-components-preview): add Tooltip component#36079dmytrokirpa wants to merge 4 commits intomicrosoft:masterfrom
Conversation
|
Pull request demo site: URL |
8674624 to
6731cf6
Compare
6731cf6 to
8995a01
Compare
📊 Bundle size report
Unchanged fixtures
|
8995a01 to
6ac8163
Compare
6ac8163 to
f0e5f84
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new headless Tooltip entrypoint to @fluentui/react-headless-components-preview, including component implementation (Popover API + CSS Anchor Positioning via the package’s positioning utilities), Storybook stories/docs, tests, and package/export integration.
Changes:
- Introduces
Tooltipcomponent implementation (useTooltip,renderTooltip, types, exports) plus Jest tests. - Adds Storybook stories and markdown documentation for Tooltip usage patterns and accessibility relationships.
- Wires Tooltip into package exports, API extractor report, bundle-size fixture, dependencies, and beachball change file.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react-components/react-headless-components-preview/stories/src/Tooltip/index.stories.tsx | Registers Tooltip stories and docs in Storybook. |
| packages/react-components/react-headless-components-preview/stories/src/Tooltip/TooltipWithArrow.stories.tsx | Adds an arrow styling/positioning example story. |
| packages/react-components/react-headless-components-preview/stories/src/Tooltip/TooltipRelationshipLabel.stories.tsx | Demonstrates relationship="label" behavior and docs text. |
| packages/react-components/react-headless-components-preview/stories/src/Tooltip/TooltipRelationshipDescription.stories.tsx | Demonstrates relationship="description" behavior and docs text. |
| packages/react-components/react-headless-components-preview/stories/src/Tooltip/TooltipPositions.stories.tsx | Demonstrates different positioning shorthand values. |
| packages/react-components/react-headless-components-preview/stories/src/Tooltip/TooltipDescription.md | Component description for docs page. |
| packages/react-components/react-headless-components-preview/stories/src/Tooltip/TooltipDefault.stories.tsx | Default Tooltip example (with args passthrough). |
| packages/react-components/react-headless-components-preview/stories/src/Tooltip/TooltipControlled.stories.tsx | Controlled visible example. |
| packages/react-components/react-headless-components-preview/stories/src/Tooltip/TooltipBestPractices.md | Best-practices + accessibility guidance for tooltips. |
| packages/react-components/react-headless-components-preview/library/src/tooltip.ts | Adds the public ./tooltip entrypoint exports. |
| packages/react-components/react-headless-components-preview/library/src/components/Tooltip/useTooltip.ts | Implements Tooltip behavior (visibility, relationships, positioning refs, Popover API sync). |
| packages/react-components/react-headless-components-preview/library/src/components/Tooltip/renderTooltip.tsx | Renders trigger + tooltip content (and optional arrow). |
| packages/react-components/react-headless-components-preview/library/src/components/Tooltip/index.ts | Barrel exports for Tooltip component files. |
| packages/react-components/react-headless-components-preview/library/src/components/Tooltip/Tooltip.types.ts | Public props/state/type surface for headless Tooltip entrypoint. |
| packages/react-components/react-headless-components-preview/library/src/components/Tooltip/Tooltip.tsx | Tooltip component wrapper using hook + render function. |
| packages/react-components/react-headless-components-preview/library/src/components/Tooltip/Tooltip.test.tsx | Adds unit/conformance coverage for tooltip relationships and state attributes. |
| packages/react-components/react-headless-components-preview/library/package.json | Adds deps and registers ./tooltip export path. |
| packages/react-components/react-headless-components-preview/library/etc/tooltip.api.md | API Extractor report for the new tooltip entrypoint. |
| packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js | Includes tooltip entrypoint in bundle-size fixture. |
| change/@fluentui-react-headless-components-preview-021e6dbb-6618-4217-a7ce-8567198a60ff.json | Beachball change file for publishing the addition. |
| if (visible) { | ||
| el.showPopover(); | ||
| } else { |
There was a problem hiding this comment.
added try/catch with dev warning for unsupported browser
| // stop propagation to avoid conflicting with other elements that listen for `Escape` | ||
| // e,g: Dialog, Popover, Menu and Tooltip |
5ad6afa to
a9bb2bf
Compare
a9bb2bf to
cde85aa
Compare
|
|
||
| // JSDOM does not implement the Popover API yet. | ||
| // Provide a minimal test shim so components using showPopover/hidePopover can run in Jest. | ||
| if (typeof HTMLElement !== 'undefined') { |
There was a problem hiding this comment.
replaced with polyfill
There was a problem hiding this comment.
is it not enough for testing to have minimal shim? not sure if we need to bring that polyfill dependency only for testing
There was a problem hiding this comment.
it's not enough, as if we want to go with this we need to use the same hacks we have in Popover - like setting popover attribute conditionally in useEffect, etc. So I think it's better to have polyfill in tests and simpler/more robust implementation in the component itself
| }), | ||
| }; | ||
|
|
||
| const positioningOptions = resolvePositioningShorthand(positioning); |
cde85aa to
b1905a2
Compare
b1905a2 to
f7e4092
Compare
526d9d5 to
ae3a059
Compare
| /** Jest test setup file. */ | ||
|
|
||
| require('@testing-library/jest-dom'); | ||
| require('@oddbird/popover-polyfill'); |
There was a problem hiding this comment.
polyfills JSDOM with Popover API
| "swc-loader": "^0.2.6", | ||
| "prettier": "2.8.8", | ||
| "puppeteer": "19.6.3", | ||
| "puppeteer": "24.42.0", |
There was a problem hiding this comment.
puppeteer@19.6.3 used previous versions of chrome/chromium where Popover API is not available, which causes test-ssr failures
| }); | ||
| page.on('pageerror', pageError => { | ||
| error = pageError; | ||
| error = pageError as Error; |
There was a problem hiding this comment.
pageError becomes unknown after puppeteer bump
|
|
||
| // JSDOM does not implement the Popover API yet. | ||
| // Provide a minimal test shim so components using showPopover/hidePopover can run in Jest. | ||
| if (typeof HTMLElement !== 'undefined') { |
There was a problem hiding this comment.
is it not enough for testing to have minimal shim? not sure if we need to bring that polyfill dependency only for testing
| import { Tooltip } from './Tooltip'; | ||
|
|
||
| function queryByRoleTooltip(result: RenderResult) { | ||
| const tooltips = result.baseElement.querySelectorAll('*[role="tooltip"]'); |
There was a problem hiding this comment.
can't we just use screen.queryByRole('tooltip') ?
| content: slot.always(content, { | ||
| defaultProps: { | ||
| role: 'tooltip', | ||
| popover: 'manual', |
There was a problem hiding this comment.
are u sure it should be based on popover='manual'? v9 tooltip allowed only one visible Tooltip at a time (it's know based on context similar to v9, but we could remove that in case of non manual). As previously shared, hint seems as dedicated feature for Popovers, since we already use some API beyond baseline (css anchoring properties) maybe we should consider to use hint instead?
https://developer.chrome.com/blog/popover-hint
There was a problem hiding this comment.
- Tooltip doens't have any a11y features other than aria-labels
- we already have a context-based logic in v9's tooltips to show only one tooltip at a time
- It's not clear at this point that we'll be able to use something beyond
Baseline Widely Available. If that would be the case we'll just switch from our customusePositioninghook to floating-ui based on fromreact-positioningand that's it.
| const contentRef = useMergedRefs(state.content.ref, containerRef); | ||
| state.content.ref = contentRef; | ||
|
|
||
| // When this tooltip is visible, hide any other tooltips, and register it |
There was a problem hiding this comment.
we won't need this and can omit this functionality in case if auto/hint will be used
There was a problem hiding this comment.
The reasoning behind manual is:
automight conflict with menus/popovers/etchintis outside of browser's support matrix
| </Tooltip>, | ||
| ); | ||
|
|
||
| const trigger = result.getByRole('button'); |
There was a problem hiding this comment.
I think this can be simplified to:
expect(screen.getByLabelText('Default Tooltip')).toBeInTheDocument();
expect(screen.getByRole('tooltip')).toHaveAttribute('popover', 'manual');| expect(target.hasAttribute('aria-label')).toBe(false); | ||
| expect(target.hasAttribute('aria-labelledby')).toBe(false); | ||
| expect(target.hasAttribute('aria-description')).toBe(false); | ||
| expect(target.hasAttribute('aria-describedby')).toBe(false); |
There was a problem hiding this comment.
| expect(target.hasAttribute('aria-label')).toBe(false); | |
| expect(target.hasAttribute('aria-labelledby')).toBe(false); | |
| expect(target.hasAttribute('aria-description')).toBe(false); | |
| expect(target.hasAttribute('aria-describedby')).toBe(false); | |
| expect(target).not.toHaveAttribute('aria-label'); | |
| expect(target).not.toHaveAttribute('aria-labelledby'); | |
| expect(target).not.toHaveAttribute('aria-description'); | |
| expect(target).not.toHaveAttribute('aria-describedby'); |
|
|
||
| const target = result.getByRole('button'); | ||
| expect(target.getAttribute('aria-label')).toBe('test-label'); | ||
| expect(target.getAttribute('aria-labelledby')).toBe(null); |
There was a problem hiding this comment.
| expect(target.getAttribute('aria-labelledby')).toBe(null); | |
| expect(target).not.toHaveAttribute('aria-labelledby'); |
| ); | ||
|
|
||
| const target = result.getByRole('button'); | ||
| expect(target.getAttribute('aria-description')).toBe(null); |
There was a problem hiding this comment.
| expect(target.getAttribute('aria-description')).toBe(null); | |
| expect(target).not.toHaveAttribute('aria-description'); |

This pull request introduces a new headless
Tooltipcomponent to the@fluentui/react-headless-components-previewpackage, providing a non-modal floating label or description anchored to a trigger element. The implementation leverages CSS Anchor Positioning and the Popover API for improved positioning and visibility management. Supporting types, tests, and exports are added, and the package dependencies and exports are updated accordingly.New Tooltip Component Implementation:
Tooltipcomponent and its supporting files (Tooltip.tsx,Tooltip.types.ts,renderTooltip.tsx,useTooltip.ts) to provide a headless tooltip with customizable positioning, visibility, and accessibility relationships. The implementation uses CSS Anchor Positioning and the Popover API for managing placement and visibility. [1] [2] [3] [4]Testing:
Tooltipcomponent, covering rendering, accessibility attributes, positioning, arrow support, and aria-relationship behaviors.Exports and Package Integration:
Tooltipcomponent, its types, and related utilities from the package entry points, making it available for consumers. [1] [2]Tooltipcomponent in the bundle size fixture for testing and documentation purposes.Dependency and Package Configuration:
@fluentui/react-tooltipand@fluentui/react-positioningas dependencies inpackage.jsonto support the new component, and registered the new entry point fortooltip. [1] [2] [3]