Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/vr-tests-react-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"start": "storybook dev",
"test": "just-scripts test",
"type-check": "tsc -p . --noEmit --baseUrl .",
"test-vr": "storywright --browsers chromium --url dist/storybook --destpath dist/screenshots --waitTimeScreenshot 500 --concurrency 4 --headless true --bailOnStoriesError --stepsApi parameters"
"test-vr": "storywright --browsers chromium --url dist/storybook --destpath dist/screenshots --waitTimeScreenshot 1000 --concurrency 2 --headless true --bailOnStoriesError --stepsApi parameters"
},
"dependencies": {
"@fluentui/react-accordion": "*",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{

@github-actions github-actions Bot Jul 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Menu Converged - submenuIndicator slotted content 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default - RTL.submenus open.chromium.png 404 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default.submenus open.chromium.png 605 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 857 Changed
vr-tests-react-components/Positioning.Positioning end.chromium.png 502 Changed
vr-tests-react-components/ProgressBar converged 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - High Contrast.default.chromium.png 74 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png 123 Changed
vr-tests-react-components/TagPicker 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - Dark Mode.disabled input hover.chromium.png 658 Changed
vr-tests-react-components/TagPicker.disabled - High Contrast.disabled input hover.chromium.png 1319 Changed
vr-tests-react-components/TagPicker.disabled.disabled input hover.chromium.png 677 Changed

There were 3 duplicate changes discarded. Check the build logs for more information.

"type": "patch",
"comment": "tooltip appears outside of overflow",
"packageName": "@fluentui/react-tooltip",
"email": "paulmardling@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { computePosition } from '@floating-ui/dom';
import type { Placement } from '@floating-ui/dom';
import { createPositionManager } from './createPositionManager';
import { POSITIONING_END_EVENT } from './constants';
import { DATA_POSITIONING_ESCAPED, DATA_POSITIONING_HIDDEN, POSITIONING_END_EVENT } from './constants';
import type { OnPositioningEndEvent } from './types';

jest.mock('@floating-ui/dom', () => ({
Expand Down Expand Up @@ -171,4 +171,109 @@ describe('createPositionManager', () => {

expect(listener).not.toHaveBeenCalled();
});

describe('hide middleware data attributes', () => {
it('sets data-popper-reference-hidden when referenceHidden is true', async () => {
computePositionMock.mockResolvedValue({
x: 0,
y: 0,
placement: 'bottom',
strategy: 'absolute',
middlewareData: {
...mockMiddlewareData,
hide: { escaped: false, referenceHidden: true },
},
});

const { container, target } = createTestElements();
createPositionManager({
container,
target,
arrow: null,
strategy: 'absolute',
middleware: [],
placement: 'bottom',
disableUpdateOnResize: true,
});

await flushMicrotasks();

expect(container.hasAttribute(DATA_POSITIONING_HIDDEN)).toBe(true);
expect(container.hasAttribute(DATA_POSITIONING_ESCAPED)).toBe(false);
});

it('removes data-popper-reference-hidden when referenceHidden becomes false', async () => {
const { container, target } = createTestElements();

// First position update: reference is hidden
computePositionMock.mockResolvedValueOnce({
x: 0,
y: 0,
placement: 'bottom',
strategy: 'absolute',
middlewareData: {
...mockMiddlewareData,
hide: { escaped: false, referenceHidden: true },
},
});

const manager = createPositionManager({
container,
target,
arrow: null,
strategy: 'absolute',
middleware: [],
placement: 'bottom',
disableUpdateOnResize: true,
});

await flushMicrotasks();
expect(container.hasAttribute(DATA_POSITIONING_HIDDEN)).toBe(true);

// Second position update: reference is back in view
computePositionMock.mockResolvedValueOnce({
x: 0,
y: 0,
placement: 'bottom',
strategy: 'absolute',
middlewareData: {
...mockMiddlewareData,
hide: { escaped: false, referenceHidden: false },
},
});

manager.updatePosition();
await flushMicrotasks();
expect(container.hasAttribute(DATA_POSITIONING_HIDDEN)).toBe(false);
});

it('sets data-popper-escaped when escaped is true', async () => {
computePositionMock.mockResolvedValue({
x: 0,
y: 0,
placement: 'bottom',
strategy: 'absolute',
middlewareData: {
...mockMiddlewareData,
hide: { escaped: true, referenceHidden: false },
},
});

const { container, target } = createTestElements();
createPositionManager({
container,
target,
arrow: null,
strategy: 'absolute',
middleware: [],
placement: 'bottom',
disableUpdateOnResize: true,
});

await flushMicrotasks();

expect(container.hasAttribute(DATA_POSITIONING_ESCAPED)).toBe(true);
expect(container.hasAttribute(DATA_POSITIONING_HIDDEN)).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { baseConfig } from '@fluentui/scripts-cypress';

export default baseConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import 'cypress-real-events';
import * as React from 'react';
import { mount as mountBase } from '@fluentui/scripts-cypress';
import { FluentProvider } from '@fluentui/react-provider';
import { teamsLightTheme } from '@fluentui/react-theme';
import { Button } from '@fluentui/react-button';
import { Tooltip } from './index';

const mount = (element: React.ReactElement) => {
mountBase(<FluentProvider theme={teamsLightTheme}>{element}</FluentProvider>);
};

// The data attribute the positioning middleware writes when the reference is hidden
const DATA_POSITIONING_HIDDEN = 'data-popper-reference-hidden';

describe('Tooltip', () => {
describe('overflow behavior (regression: #32882)', () => {
/**
* Renders a tooltip trigger inside an overflow:hidden container tall enough
* to scroll the trigger out of view, then verifies the tooltip content element
* gets `data-popper-reference-hidden` set once scrolled, and loses it once
* scrolled back.
*
* This is the E2E complement to the unit tests in createPositionManager.test.ts
* and the CSS fix in useTooltipStyles.styles.ts.
*/
it('sets data-popper-reference-hidden on the tooltip when the trigger scrolls out of view', () => {
mount(
<div
id="scroll-container"
style={{
height: '100px',
width: '200px',
overflow: 'hidden scroll',
position: 'relative',
}}
>
<div style={{ height: '400px', paddingTop: '8px' }}>
<Tooltip content="Overflow tooltip" relationship="label" data-testid="tooltip-content">
<Button id="trigger">Hover me</Button>
</Tooltip>
</div>
</div>,
);

cy.get('#trigger').realHover();
cy.get('[role="tooltip"]').should('be.visible');
cy.get('#scroll-container').scrollTo(0, 300);
cy.get('[role="tooltip"]').should('have.attr', DATA_POSITIONING_HIDDEN);
});

it('removes data-popper-reference-hidden when the trigger scrolls back into view', () => {
mount(
<div
id="scroll-container"
style={{
height: '100px',
width: '200px',
overflow: 'hidden scroll',
position: 'relative',
}}
>
<div style={{ height: '400px', paddingTop: '8px' }}>
<Tooltip content="Overflow tooltip" relationship="label">
<Button id="trigger">Hover me</Button>
</Tooltip>
</div>
</div>,
);

cy.get('#trigger').realHover();
cy.get('[role="tooltip"]').should('be.visible');

cy.get('#scroll-container').scrollTo(0, 300);
cy.get('[role="tooltip"]').should('have.attr', DATA_POSITIONING_HIDDEN);

cy.get('#scroll-container').scrollTo(0, 0);
cy.get('[role="tooltip"]').should('not.have.attr', DATA_POSITIONING_HIDDEN);
});

it('applies visibility:hidden CSS when data-popper-reference-hidden is present', () => {
mount(
<div
id="scroll-container"
style={{
height: '100px',
width: '200px',
overflow: 'hidden scroll',
position: 'relative',
}}
>
<div style={{ height: '400px', paddingTop: '8px' }}>
<Tooltip content="Overflow tooltip" relationship="label">
<Button id="trigger">Hover me</Button>
</Tooltip>
</div>
</div>,
);

cy.get('#trigger').realHover();
cy.get('[role="tooltip"]').should('be.visible');

cy.get('#scroll-container').scrollTo(0, 300);
cy.get('[role="tooltip"]').should('have.css', 'visibility', 'hidden');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ const useStyles = makeStyles({

visible: {
display: 'block',

// Hide when the trigger has scrolled out of view (referenceHidden) or when
// the tooltip itself has overflowed its clipping boundary (escaped).
// The positioning middleware writes these data attributes automatically.
// See https://github.com/microsoft/fluentui/issues/32882
'&[data-popper-reference-hidden]': {
visibility: 'hidden',
pointerEvents: 'none',
},
'&[data-popper-escaped]': {
visibility: 'hidden',
pointerEvents: 'none',
},
},

inverted: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"isolatedModules": false,
"types": ["node", "cypress", "cypress-real-events"],
"typeRoots": ["../../../../node_modules", "../../../../node_modules/@types"]
},
"include": ["**/*.cy.ts", "**/*.cy.tsx"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.cy.json"
Comment thread
PaulGMardling marked this conversation as resolved.
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as React from 'react';
import type { JSXElement } from '@fluentui/react-components';
import { Button, Tooltip } from '@fluentui/react-components';

/**
* A tooltip whose trigger is inside an `overflow: hidden` container.
* When the trigger is scrolled out of view, the tooltip should become hidden
* (via `data-popper-reference-hidden` set by the positioning middleware).
*
* Regression story for https://github.com/microsoft/fluentui/issues/32882
*/
export const OverflowHidden = (): JSXElement => {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', alignItems: 'flex-start' }}>
<p style={{ margin: 0, fontSize: '14px' }}>
Scroll the box below. The tooltip should disappear when the button scrolls out of view, not follow it outside
the container boundary.
</p>
<div
style={{
height: '120px',
width: '240px',
overflow: 'hidden scroll',
border: '1px solid #ccc',
borderRadius: '4px',
position: 'relative',
}}
>
<div style={{ height: '300px', paddingTop: '8px', paddingLeft: '8px' }}>
<Tooltip content="I should hide when scrolled out of view" relationship="label">
<Button>Hover me, then scroll</Button>
</Tooltip>
</div>
</div>
</div>
);
};

OverflowHidden.parameters = {
docs: {
description: {
story: `When a tooltip trigger is inside an \`overflow: hidden\` container and the trigger scrolls out
of view, the tooltip must be hidden rather than appearing outside the overflow boundary.

The positioning middleware sets \`data-popper-reference-hidden\` on the tooltip element when the trigger
is clipped by its scroll container. The Tooltip styles respond to this attribute with \`visibility: hidden\`.`,
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { Controlled } from './TooltipControlled.stories';
export { Positioning } from './TooltipPositioning.stories';
export { Target } from './TooltipTarget.stories';
export { Icon } from './TooltipIcon.stories';
export { OverflowHidden } from './TooltipOverflowHidden.stories';

export default {
title: 'Components/Tooltip',
Expand Down
Loading