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 37034935a7d596..69dcb49de2b1d3 100644 --- a/apps/vr-tests-react-components/src/stories/Positioning.stories.tsx +++ b/apps/vr-tests-react-components/src/stories/Positioning.stories.tsx @@ -1084,6 +1084,63 @@ const ScrollJumpContext = () => { ); }; +const MultiScrollParent = () => { + const { targetRef, containerRef } = usePositioning({}); + const scrollContainerRef = React.useRef(null); + + const scroll = () => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollBy({ top: 100 }); + } + }; + + return ( + <> + The popover should stay attached to the trigger + +
+
+
+ +
+
+
+
+
+ Popover +
+ + ); +}; + storiesOf('Positioning', module) .addDecorator(story => (
, { includeRtl: true }, - ); + ) + .addStory('Multiple scroll parents', () => ( + + + + )); storiesOf('Positioning (no decorator)', module) .addStory('scroll jumps', () => ( diff --git a/change/@fluentui-react-positioning-938da831-77f1-4eaa-9251-f9b1fc6c6d7a.json b/change/@fluentui-react-positioning-938da831-77f1-4eaa-9251-f9b1fc6c6d7a.json new file mode 100644 index 00000000000000..e0ebd7a47d3430 --- /dev/null +++ b/change/@fluentui-react-positioning-938da831-77f1-4eaa-9251-f9b1fc6c6d7a.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: Consider all parents as scroll parents", + "packageName": "@fluentui/react-positioning", + "email": "lingfan.gao@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-positioning/src/createPositionManager.ts b/packages/react-components/react-positioning/src/createPositionManager.ts index 347a164131f4ca..0b7a354162874f 100644 --- a/packages/react-components/react-positioning/src/createPositionManager.ts +++ b/packages/react-components/react-positioning/src/createPositionManager.ts @@ -1,8 +1,9 @@ import { computePosition } from '@floating-ui/dom'; import type { Middleware, Placement, Strategy } from '@floating-ui/dom'; import type { PositionManager, TargetElement } from './types'; -import { debounce, writeArrowUpdates, writeContainerUpdates, getScrollParent } from './utils'; +import { debounce, writeArrowUpdates, writeContainerUpdates } from './utils'; import { isHTMLElement } from '@fluentui/react-utilities'; +import { listScrollParents } from './utils/listScrollParents'; interface PositionManagerOptions { /** @@ -67,9 +68,9 @@ export function createPositionManager(options: PositionManagerOptions): Position } if (isFirstUpdate) { - scrollParents.add(getScrollParent(container)); + listScrollParents(container).forEach(scrollParent => scrollParents.add(scrollParent)); if (isHTMLElement(target)) { - scrollParents.add(getScrollParent(target)); + listScrollParents(target).forEach(scrollParent => scrollParents.add(scrollParent)); } scrollParents.forEach(scrollParent => { @@ -127,6 +128,7 @@ export function createPositionManager(options: PositionManagerOptions): Position scrollParents.forEach(scrollParent => { scrollParent.removeEventListener('scroll', updatePosition); }); + scrollParents.clear(); }; if (targetWindow) { diff --git a/packages/react-components/react-positioning/src/utils/listScrollParents.test.ts b/packages/react-components/react-positioning/src/utils/listScrollParents.test.ts new file mode 100644 index 00000000000000..865fd02a38c5a0 --- /dev/null +++ b/packages/react-components/react-positioning/src/utils/listScrollParents.test.ts @@ -0,0 +1,30 @@ +import { listScrollParents } from './listScrollParents'; + +describe('listScrollParents', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + const createScrollParent = () => { + const el = document.createElement('div'); + el.style.overflow = 'auto'; + return el; + }; + + it('should return all scroll parents include and up to body', () => { + const start = document.createElement('div'); + const scrollParent1 = createScrollParent(); + const scrollParent2 = createScrollParent(); + + scrollParent1.appendChild(start); + scrollParent2.appendChild(scrollParent1); + document.body.append(scrollParent2); + + const scrollParents = listScrollParents(start); + + expect(scrollParents.length).toBe(3); + expect(scrollParents).toContain(scrollParent1); + expect(scrollParents).toContain(scrollParent2); + expect(scrollParents).toContain(document.body); + }); +}); diff --git a/packages/react-components/react-positioning/src/utils/listScrollParents.ts b/packages/react-components/react-positioning/src/utils/listScrollParents.ts new file mode 100644 index 00000000000000..afc08a00aee67f --- /dev/null +++ b/packages/react-components/react-positioning/src/utils/listScrollParents.ts @@ -0,0 +1,20 @@ +import { getScrollParent } from './getScrollParent'; + +export function listScrollParents(node: HTMLElement): HTMLElement[] { + const scrollParents: HTMLElement[] = []; + + let cur: HTMLElement | null = node; + while (cur) { + const scrollParent = getScrollParent(cur); + + if (node.ownerDocument.body === scrollParent) { + scrollParents.push(scrollParent); + break; + } + + scrollParents.push(scrollParent); + cur = scrollParent; + } + + return scrollParents; +}