From 6eabf5882b83652eb9522d9b26c137338a6d01c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:01:16 +0000 Subject: [PATCH 1/4] Initial plan From c309dd62e1abb3c9166cf3ee2d20690f864ef052 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:11:45 +0000 Subject: [PATCH 2/4] Change files Co-authored-by: tudorpopams <97875118+tudorpopams@users.noreply.github.com> --- ...eact-combobox-522868f4-3ed8-4e2c-8025-38ecc4c7b6a2.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-combobox-522868f4-3ed8-4e2c-8025-38ecc4c7b6a2.json diff --git a/change/@fluentui-react-combobox-522868f4-3ed8-4e2c-8025-38ecc4c7b6a2.json b/change/@fluentui-react-combobox-522868f4-3ed8-4e2c-8025-38ecc4c7b6a2.json new file mode 100644 index 00000000000000..6b9c3d0e8fc321 --- /dev/null +++ b/change/@fluentui-react-combobox-522868f4-3ed8-4e2c-8025-38ecc4c7b6a2.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: preserve Combobox listbox scrollTop by stabilizing positioning options to prevent manager recreation on every render (#35731)", + "packageName": "@fluentui/react-combobox", + "email": "198982749+Copilot@users.noreply.github.com", + "dependentChangeType": "patch" +} From 9f3906cfdb685ead88d13e5501976e8b8605e140 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:11:59 +0000 Subject: [PATCH 3/4] fix: stabilize Combobox positioning options to preserve listbox scrollTop on re-render (#35731) Agent-Logs-Url: https://github.com/microsoft/fluentui/sessions/f1f92f2a-b1f9-42d9-a861-32b7b3d5f1d7 Co-authored-by: tudorpopams <97875118+tudorpopams@users.noreply.github.com> --- .../src/utils/useComboboxPositioning.ts | 15 +++-- .../ComboboxScrollStability.stories.tsx | 57 +++++++++++++++++++ .../stories/src/Combobox/index.stories.tsx | 1 + 3 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 packages/react-components/react-combobox/stories/src/Combobox/ComboboxScrollStability.stories.tsx diff --git a/packages/react-components/react-combobox/library/src/utils/useComboboxPositioning.ts b/packages/react-components/react-combobox/library/src/utils/useComboboxPositioning.ts index c19ae5f1ec72a5..123b8e08f7e396 100644 --- a/packages/react-components/react-combobox/library/src/utils/useComboboxPositioning.ts +++ b/packages/react-components/react-combobox/library/src/utils/useComboboxPositioning.ts @@ -5,6 +5,14 @@ import { resolvePositioningShorthand, usePositioning } from '@fluentui/react-pos import type { ComboboxBaseProps } from './ComboboxBase.types'; import type * as React from 'react'; +// Stable module-level constants prevent new object/array references on every render. +// Without these constants, new references would cause usePositioningConfigFn's useCallback +// to recreate on every render, which disposes and recreates the position manager, triggering +// autoSize middleware (resetMaxSize) that temporarily removes height constraints from the listbox +// and resets scrollTop to 0. See: https://github.com/microsoft/fluentui/issues/35731 +const DEFAULT_FALLBACK_POSITIONS: PositioningShorthandValue[] = ['above', 'after', 'after-top', 'before', 'before-top']; +const DEFAULT_OFFSET = { crossAxis: 0, mainAxis: 2 } as const; + export function useComboboxPositioning(props: ComboboxBaseProps): [ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-deprecated listboxRef: React.MutableRefObject, @@ -13,15 +21,12 @@ export function useComboboxPositioning(props: ComboboxBaseProps): [ ] { const { positioning } = props; - // Set a default set of fallback positions to try if the dropdown does not fit on screen - const fallbackPositions: PositioningShorthandValue[] = ['above', 'after', 'after-top', 'before', 'before-top']; - // popper options const popperOptions = { position: 'below' as const, align: 'start' as const, - offset: { crossAxis: 0, mainAxis: 2 }, - fallbackPositions, + offset: DEFAULT_OFFSET, + fallbackPositions: DEFAULT_FALLBACK_POSITIONS, matchTargetSize: 'width' as const, autoSize: true, ...resolvePositioningShorthand(positioning), diff --git a/packages/react-components/react-combobox/stories/src/Combobox/ComboboxScrollStability.stories.tsx b/packages/react-components/react-combobox/stories/src/Combobox/ComboboxScrollStability.stories.tsx new file mode 100644 index 00000000000000..e39655a2fd79ce --- /dev/null +++ b/packages/react-components/react-combobox/stories/src/Combobox/ComboboxScrollStability.stories.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import type { JSXElement } from '@fluentui/react-components'; +import { Combobox, makeStyles, Option, useId } from '@fluentui/react-components'; +import type { ComboboxProps } from '@fluentui/react-components'; + +const useStyles = makeStyles({ + root: { + // Stack the label above the field with a gap + display: 'grid', + gridTemplateRows: 'repeat(1fr)', + justifyItems: 'start', + gap: '2px', + maxWidth: '400px', + }, +}); + +const options = Array.from({ length: 50 }, (_, i) => `Option ${i + 1}`); + +/** + * Demonstrates that the listbox scrollTop is preserved when a parent component updates + * state in response to hovering an option (e.g. tracking the currently-hovered item). + * + * Previously, each state update caused the Combobox to re-render, which recreated the + * positioning manager and triggered the autoSize middleware to reset the listbox scrollTop + * to 0, making the last options in a long list unreachable. + * See: https://github.com/microsoft/fluentui/issues/35731 + */ +export const ScrollStability = (props: Partial): JSXElement => { + const comboId = useId('combo-scroll-stability'); + const styles = useStyles(); + const [hoveredOption, setHoveredOption] = React.useState(null); + + return ( +
+ + Hovered: {hoveredOption ?? 'none'} + + {options.map(option => ( + + ))} + +
+ ); +}; + +ScrollStability.parameters = { + docs: { + description: { + story: + 'A Combobox with many options where a parent component tracks the hovered option in state. ' + + 'Scrolling to the bottom of the list and hovering a low option should not cause the listbox ' + + 'to jump back to the top. This validates the fix for https://github.com/microsoft/fluentui/issues/35731.', + }, + }, +}; diff --git a/packages/react-components/react-combobox/stories/src/Combobox/index.stories.tsx b/packages/react-components/react-combobox/stories/src/Combobox/index.stories.tsx index b19334204b9930..2b1869055bd32d 100644 --- a/packages/react-components/react-combobox/stories/src/Combobox/index.stories.tsx +++ b/packages/react-components/react-combobox/stories/src/Combobox/index.stories.tsx @@ -21,6 +21,7 @@ export { Disabled } from './ComboboxDisabled.stories'; export { ComboboxVirtualizer as Virtualizer } from './ComboboxVirtualizer.stories'; export { ActiveOptionChange } from './ComboboxActiveOptionChange.stories'; export { ControllingOpenAndClose } from './ComboboxControllingOpenAndClose.stories'; +export { ScrollStability } from './ComboboxScrollStability.stories'; export default { title: 'Components/Combobox', From 2081a5b01d5a048bb2f97bf69c4c21d33a7b44bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:13:52 +0000 Subject: [PATCH 4/4] fix: address code review feedback - use stable onMouseEnter handler in story Agent-Logs-Url: https://github.com/microsoft/fluentui/sessions/f1f92f2a-b1f9-42d9-a861-32b7b3d5f1d7 Co-authored-by: tudorpopams <97875118+tudorpopams@users.noreply.github.com> --- .../src/Combobox/ComboboxScrollStability.stories.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react-components/react-combobox/stories/src/Combobox/ComboboxScrollStability.stories.tsx b/packages/react-components/react-combobox/stories/src/Combobox/ComboboxScrollStability.stories.tsx index e39655a2fd79ce..18275831ae31f4 100644 --- a/packages/react-components/react-combobox/stories/src/Combobox/ComboboxScrollStability.stories.tsx +++ b/packages/react-components/react-combobox/stories/src/Combobox/ComboboxScrollStability.stories.tsx @@ -30,13 +30,18 @@ export const ScrollStability = (props: Partial): JSXElement => { const styles = useStyles(); const [hoveredOption, setHoveredOption] = React.useState(null); + const onMouseEnter = React.useCallback((e: React.MouseEvent) => { + const value = (e.currentTarget as HTMLElement).dataset.value ?? null; + setHoveredOption(value); + }, []); + return (
Hovered: {hoveredOption ?? 'none'} {options.map(option => ( - ))}