diff --git a/apps/vr-tests-react-components/package.json b/apps/vr-tests-react-components/package.json
index 947b718394ca52..522acf30ebc554 100644
--- a/apps/vr-tests-react-components/package.json
+++ b/apps/vr-tests-react-components/package.json
@@ -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": "*",
diff --git a/change/@fluentui-react-tooltip-b1816358-7b53-4ebc-8579-8308e77a0468.json b/change/@fluentui-react-tooltip-b1816358-7b53-4ebc-8579-8308e77a0468.json
new file mode 100644
index 00000000000000..de21aa6b51dc5a
--- /dev/null
+++ b/change/@fluentui-react-tooltip-b1816358-7b53-4ebc-8579-8308e77a0468.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "tooltip appears outside of overflow",
+ "packageName": "@fluentui/react-tooltip",
+ "email": "paulmardling@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/react-components/react-positioning/library/src/createPositionManager.test.ts b/packages/react-components/react-positioning/library/src/createPositionManager.test.ts
index 943c592c0d8a1f..e21e73dbb5d68c 100644
--- a/packages/react-components/react-positioning/library/src/createPositionManager.test.ts
+++ b/packages/react-components/react-positioning/library/src/createPositionManager.test.ts
@@ -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', () => ({
@@ -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);
+ });
+ });
});
diff --git a/packages/react-components/react-tooltip/library/cypress.config.ts b/packages/react-components/react-tooltip/library/cypress.config.ts
new file mode 100644
index 00000000000000..ca52cf041bbf2c
--- /dev/null
+++ b/packages/react-components/react-tooltip/library/cypress.config.ts
@@ -0,0 +1,3 @@
+import { baseConfig } from '@fluentui/scripts-cypress';
+
+export default baseConfig;
diff --git a/packages/react-components/react-tooltip/library/src/components/Tooltip/Tooltip.cy.tsx b/packages/react-components/react-tooltip/library/src/components/Tooltip/Tooltip.cy.tsx
new file mode 100644
index 00000000000000..0250c954ef7e1f
--- /dev/null
+++ b/packages/react-components/react-tooltip/library/src/components/Tooltip/Tooltip.cy.tsx
@@ -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(
+ Scroll the box below. The tooltip should disappear when the button scrolls out of view, not follow it outside + the container boundary. +
+