diff --git a/apps/vr-tests-react-components/src/stories/Positioning.stories.tsx b/apps/vr-tests-react-components/src/stories/Positioning.stories.tsx
index 0e656c6e7e03cc..011ad534f042e3 100644
--- a/apps/vr-tests-react-components/src/stories/Positioning.stories.tsx
+++ b/apps/vr-tests-react-components/src/stories/Positioning.stories.tsx
@@ -11,6 +11,7 @@ import { useMergedRefs } from '@fluentui/react-utilities';
import { tokens } from '@fluentui/react-theme';
import { storiesOf } from '@storybook/react';
import { useFluent } from '@fluentui/react-shared-contexts';
+import Screener from 'screener-storybook/src/screener';
const useStyles = makeStyles({
wrapper: {
@@ -54,6 +55,26 @@ const useStyles = makeStyles({
seeThrough: {
opacity: 0.6,
},
+
+ visibilityModifiers: {
+ backgroundColor: '#ccc',
+ minHeight: '60px',
+ width: '200px',
+
+ '[data-popper-reference-hidden]': {
+ outlineWidth: '5px',
+ outlineStyle: 'solid',
+ outlineColor: 'red',
+ },
+ '[data-popper-escaped]': {
+ backgroundColor: 'yellow',
+ },
+ '[data-popper-is-intersecting]': {
+ outlineWidth: '5px',
+ outlineStyle: 'solid',
+ outlineColor: 'green',
+ },
+ },
});
const positions = [
@@ -454,6 +475,57 @@ const ImperativeTarget = () => {
);
};
+const VisibilityModifiers = () => {
+ const styles = useStyles();
+ const popper = usePopper({ align: 'center', position: 'above' });
+
+ return (
+ <>
+
+ This visual test asserts that visual styles are applied based on popper element's state:
+
+
+ -
+ green when the popper element intersects boundaries
+
+ -
+ red when the reference is hidden
+
+ -
+ yellow when the popper escapes the reference element's boundary
+
+
+
+
+
+ Box with visibility modifiers
+
+ >
+ );
+};
+
storiesOf('Positioning', module)
.addDecorator(story => (
)
.addStory('target property', () =>
)
.addStory('imperative target', () =>
)
+ .addStory('visibility modifiers', () => (
+
+
+
+ ))
.addStory('arrow', () =>
, { includeRtl: true });
diff --git a/change/@fluentui-react-positioning-21fb9bac-746f-4c9a-87ae-5ec9a7915c8a.json b/change/@fluentui-react-positioning-21fb9bac-746f-4c9a-87ae-5ec9a7915c8a.json
new file mode 100644
index 00000000000000..04f06eb7035728
--- /dev/null
+++ b/change/@fluentui-react-positioning-21fb9bac-746f-4c9a-87ae-5ec9a7915c8a.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "feat: add isIntersectingModifier to usePopper",
+ "packageName": "@fluentui/react-positioning",
+ "email": "olfedias@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/react-positioning/src/isIntersectingModifier.ts b/packages/react-positioning/src/isIntersectingModifier.ts
new file mode 100644
index 00000000000000..9f0fcc09938844
--- /dev/null
+++ b/packages/react-positioning/src/isIntersectingModifier.ts
@@ -0,0 +1,27 @@
+import { detectOverflow, Modifier } from '@popperjs/core';
+
+export const isIntersectingModifier: IsIntersectingModifier = {
+ name: 'is-intersecting-modifier',
+ enabled: true,
+ phase: 'main',
+ requires: ['preventOverflow'],
+ fn: ({ state, name }) => {
+ const popperRect = state.rects.popper;
+ const popperAltOverflow = detectOverflow(state, { altBoundary: true });
+
+ const isIntersectingTop = popperAltOverflow.top < popperRect.height && popperAltOverflow.top > 0;
+ const isIntersectingBottom = popperAltOverflow.bottom < popperRect.height && popperAltOverflow.bottom > 0;
+
+ const isIntersecting = isIntersectingTop || isIntersectingBottom;
+
+ state.modifiersData[name] = {
+ isIntersecting,
+ };
+ state.attributes.popper = {
+ ...state.attributes.popper,
+ 'data-popper-is-intersecting': isIntersecting,
+ };
+ },
+};
+
+type IsIntersectingModifier = Modifier<'is-intersecting-modifier', never>;
diff --git a/packages/react-positioning/src/usePopper.ts b/packages/react-positioning/src/usePopper.ts
index f8ebbb67a02b2c..39b9cb6b6c98b0 100644
--- a/packages/react-positioning/src/usePopper.ts
+++ b/packages/react-positioning/src/usePopper.ts
@@ -3,6 +3,7 @@ import { useFluent } from '@fluentui/react-shared-contexts';
import * as PopperJs from '@popperjs/core';
import * as React from 'react';
+import { isIntersectingModifier } from './isIntersectingModifier';
import {
getScrollParent,
applyRtlToOffset,
@@ -101,6 +102,8 @@ function usePopperOptions(options: PopperOptions, popperOriginalPositionRef: Rea
: false;
const modifiers: PopperJs.Options['modifiers'] = [
+ isIntersectingModifier,
+
/**
* We are setting the position to `fixed` in the first effect to prevent scroll jumps in case of the content
* with managed focus. Modifier sets the position to `fixed` before all other modifier effects. Another part of