Skip to content

Dropdown with inlinePopup scrolls parent container to top when opened inside a scrollable Dialog #35921

@layershifter

Description

@layershifter

Environment

  • Package: @fluentui/react-aria (scrollIntoView), @fluentui/react-positioning (createPositionManager)
  • Version: @fluentui/react-combobox@9.16.16, @fluentui/react-aria@9.17.10
  • React: 18 with createRoot

Description

When a Dropdown with inlinePopup is inside a scrollable container within a Dialog, opening the dropdown scrolls the parent container to the top. This is caused by a race condition between positioning initialization and active descendant focus.

Repro

Repository: https://github.com/layershifter/fluent-dropdown-scroll-repro

git clone https://github.com/layershifter/fluent-dropdown-scroll-repro
cd fluent-dropdown-scroll-repro
npm install
npm run dev
  1. Scroll down to the "Allow Copilot" dropdown at the bottom
  2. Click it
  3. The scrollable container jumps to the top

Note: The repro includes a `useForcePositionRace` hook that holds the listbox's initial `position: fixed` for two animation frames. This simulates the timing in complex apps (e.g., Microsoft Teams with 100+ React component layers) where the React passive effect fires before `computePosition` resolves. Without this hook, the race doesn't trigger in a minimal repro because `computePosition` resolves fast enough.

Root Cause

Race condition between `createPositionManager` and `useActiveDescendant`:

  1. `createPositionManager.js:42-43` initially sets `position: fixed` on the listbox container to "avoid scroll jumps":

    Object.assign(container.style, {
        position: 'fixed',
        left: 0,
        top: 0,
        margin: 0
    });
  2. `computePosition()` runs async — the `.then()` callback later sets the actual strategy (`position: absolute`):

    Object.assign(container.style, {
        position: strategy  // 'absolute'
    });
  3. React passive effect fires `activeDescendantController.focus(selectedOption.id)` (in `useComboboxBaseState.js:127`) → calls `scrollIntoView()` while the listbox still has `position: fixed`.

  4. `scrollIntoView` (`scrollIntoView.ts`) → `findScrollableParent` walks from the option element up past the fixed-positioned listbox (which isn't scrollable) to the outer scrollable container.

  5. `getTotalOffsetTop` walks the `offsetParent` chain:

    option.offsetTop (4)
    + listbox.offsetTop (0 — position:fixed always reports 0)
    + (-scrollParent.offsetTop) (-113 — via element.contains(scrollParent) branch)
    = -109
    
  6. `scrollTo(0, -109 - 0 - 2)` = `scrollTo(0, -111)` → browser clamps to 0 → container jumps to top.

Debugger Evidence

Values captured at the `scrollIntoView` breakpoint in the Microsoft Teams app:

Variable Value
`offsetTop` `-109`
`scrollTop` `1977.5`
`isAbove` `true`
`offsetHeight` `32`
`parentOffsetHeight` `550`
`scrollMarginTop` `0`

`getTotalOffsetTop` recursion:

Step Element `position` `offsetTop` Running Total
0 `div#option1.fui-Option` `relative` `4` `4`
1 `div#fluent-listbox.fui-Listbox` `fixed` `0` `4`
2 `div.fui-DialogSurface` `fixed` `4 + (-113) = -109`

Suggested Fix

The `getTotalOffsetTop` function doesn't account for `position: fixed` elements in the `offsetParent` chain. Their `offsetTop` is always 0 regardless of visual position, making the additive calculation incorrect.

Options:

  1. Skip `position: fixed` elements in `getTotalOffsetTop` or use `getBoundingClientRect()` for them
  2. Don't set initial `position: fixed` in `createPositionManager` — or defer the `scrollIntoView` call until after positioning resolves
  3. Have `findScrollableParent` stop at the Dropdown's own container boundary instead of walking up to the Dialog

Workaround

Remove `inlinePopup` from the `Dropdown`. Without it, the listbox renders in a Portal at `document.body`, outside the scrollable container's DOM tree, so `findScrollableParent` never reaches it.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions