diff --git a/packages/fluentui/CHANGELOG.md b/packages/fluentui/CHANGELOG.md
index b218d6cc0f68d4..e61e97326c12dc 100644
--- a/packages/fluentui/CHANGELOG.md
+++ b/packages/fluentui/CHANGELOG.md
@@ -31,6 +31,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Features
- Add new Popup prop `closeOnScroll` to close popup when scroll happens outside of the popover element @yuanboxue-amber ([#21453](https://github.com/microsoft/fluentui/pull/21453))
+- Add new `isIntersectingModifier` modifier to `usePopper` @layershifter ([#21829](https://github.com/microsoft/fluentui/pull/21829))
## [v0.60.1](https://github.com/microsoft/fluentui/tree/@fluentui/react-northstar_v0.60.1) (2022-01-17)
diff --git a/packages/fluentui/docs/src/examples/components/Popup/Visual/PopperExampleVisibilityModifiers.steps.ts b/packages/fluentui/docs/src/examples/components/Popup/Visual/PopperExampleVisibilityModifiers.steps.ts
new file mode 100644
index 00000000000000..65e3d7783250f2
--- /dev/null
+++ b/packages/fluentui/docs/src/examples/components/Popup/Visual/PopperExampleVisibilityModifiers.steps.ts
@@ -0,0 +1,18 @@
+import { ScreenerTestsConfig } from '@fluentui/scripts/screener';
+
+const config: ScreenerTestsConfig = {
+ steps: [
+ builder =>
+ builder
+ .click('#message-1')
+ .snapshot('Opened a popup on second message')
+ .executeScript('document.querySelector("#scrollable-area").scrollTop = 50')
+ .snapshot('has "[data-popper-is-intersecting]" when the popover intersects boundaries')
+ .executeScript('document.querySelector("#scrollable-area").scrollTop = 80')
+ .snapshot(`has "[data-popper-escaped]" when the popper escapes the reference element's boundary`)
+ .executeScript('document.querySelector("#scrollable-area").scrollTop = 150')
+ .snapshot('has "[data-popper-reference-hidden]" when the reference is hidden'),
+ ],
+};
+
+export default config;
diff --git a/packages/fluentui/docs/src/examples/components/Popup/Visual/PopperExampleVisibilityModifiers.tsx b/packages/fluentui/docs/src/examples/components/Popup/Visual/PopperExampleVisibilityModifiers.tsx
new file mode 100644
index 00000000000000..cdb43e810fd1ad
--- /dev/null
+++ b/packages/fluentui/docs/src/examples/components/Popup/Visual/PopperExampleVisibilityModifiers.tsx
@@ -0,0 +1,74 @@
+import { Popup, popupContentSlotClassNames } from '@fluentui/react-northstar';
+import * as _ from 'lodash';
+import * as React from 'react';
+
+const PopperExampleVisibilityModifiers = () => (
+ <>
+
+ 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
+
+
+
+ >
+);
+
+export default PopperExampleVisibilityModifiers;
diff --git a/packages/fluentui/docs/src/examples/components/Popup/Visual/index.tsx b/packages/fluentui/docs/src/examples/components/Popup/Visual/index.tsx
index af1a7d84245323..de1dd2d975cf92 100644
--- a/packages/fluentui/docs/src/examples/components/Popup/Visual/index.tsx
+++ b/packages/fluentui/docs/src/examples/components/Popup/Visual/index.tsx
@@ -9,6 +9,7 @@ const Visual = () => (
+
);
diff --git a/packages/fluentui/react-northstar/src/utils/positioner/isIntersectingModifier.ts b/packages/fluentui/react-northstar/src/utils/positioner/isIntersectingModifier.ts
new file mode 100644
index 00000000000000..9f0fcc09938844
--- /dev/null
+++ b/packages/fluentui/react-northstar/src/utils/positioner/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/fluentui/react-northstar/src/utils/positioner/usePopper.ts b/packages/fluentui/react-northstar/src/utils/positioner/usePopper.ts
index ebe8f5627f454a..f0cefa89bf267d 100644
--- a/packages/fluentui/react-northstar/src/utils/positioner/usePopper.ts
+++ b/packages/fluentui/react-northstar/src/utils/positioner/usePopper.ts
@@ -12,6 +12,7 @@ import { getReactFiberFromNode } from '../getReactFiberFromNode';
import { isBrowser } from '../isBrowser';
import { getBoundary } from './getBoundary';
import { getScrollParent } from './getScrollParent';
+import { isIntersectingModifier } from './isIntersectingModifier';
import { applyRtlToOffset, getPlacement } from './positioningHelper';
import { PopperInstance, PopperOptions } from './types';
@@ -102,6 +103,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