Skip to content

fix: scroll the auto-focused item into view when rendered in StrictMode - #10713

Open
kttalley wants to merge 1 commit into
adobe:mainfrom
kttalley:fix-10690-scroll-focused-item-strict-mode
Open

kttalley wants to merge 1 commit into
adobe:mainfrom
kttalley:fix-10690-scroll-focused-item-strict-mode

Conversation

@kttalley

@kttalley kttalley commented Oct 3, 2026

Copy link
Copy Markdown

Closes #10690

Summary

Intent: when a ComboBox opens with the keyboard and focus moves to the first enabled option, that option should be scrolled into view, even when the disabled options above it fill the popover.

What I found: focus does land on the right option (that was fixed for #9239), and the scroll is actually requested too. useSelectableCollection schedules it in a requestAnimationFrame from its "scroll the focused element into view" effect. The problem is the separate unmount cleanup that cancels that frame. The repro in the issue renders inside <StrictMode>, and in StrictMode React runs every effect's cleanup once and then runs the effects again right after mount. So the cleanup cancels the pending scroll. When the scroll effect runs the second time, lastFocusedKey already matches the focused key and didAutoFocusRef has been reset, so it doesn't schedule the scroll again. The option is focused but never scrolled to.

I confirmed this in Chromium (vitest browser mode) with a copy of the issue's example. With a plain createRoot render, ArrowDown opens the list and it scrolls to "Cat". Wrapped in <StrictMode>, it stays at scrollTop 0. This looks like the same cause as the StrictMode report in #9132 (rolled into #9031).

The fix: the rAF callback now clears raf.current when it runs, so raf.current is only set while a scroll is still pending. If the cleanup cancels a pending scroll, it sets didAutoFocusRef.current = true so the next run of the scroll effect schedules it again. This doesn't change anything outside StrictMode: on a real unmount the refs are discarded, and the keyboard and autofocus conditions for when to scroll are untouched. I chose this over adding a cleanup to the scroll effect itself, because that effect runs on every render, so it would also cancel scrolls on ordinary re-renders.

Thanks to @minwookshin for confirming this on the issue: it reproduces with StrictMode on a fresh root in Chromium, Firefox and WebKit, and scrolls correctly without it. That matches what I saw. In #9239 @LFDanLu mentioned a non-StrictMode repro; I couldn't reproduce one on current main, but if there's a case I'm missing I'm happy to dig into it. This should also cover the StrictMode part of #9031.

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices
  • I understand every change in this PR and can explain why it's there.
  • If AI-assisted, I followed our AI contribution guidance and pointed my assistant at AGENTS.md.

AI disclosure: an AI coding assistant (Claude) helped me investigate, reproduce, and draft this change and its test. I reviewed the change before opening this PR.

No docs change is needed since this is a bug fix with no API change. I added a unit test but no story, because the existing ComboBox stories already cover this when they run under StrictMode.

📝 Test Instructions:

Automated:

  • New test in packages/react-aria-components/test/ComboBox.test.js: "should scroll the first enabled option into view when opened with the keyboard". yarn test runs with STRICT_MODE=1, so the test renders in StrictMode. It fails on main (no scrollIntoView call on the option) and passes with this change.
  • yarn lint passes. yarn test passes except for DateRangePicker > labeling > should have selected range description with a time, which also fails on main for me (a whitespace difference in the formatted time, which I think comes from my local Node 25 ICU data).

Manual (any React app, or the sandbox from the issue: https://codesandbox.io/p/sandbox/cv7gyf):

  1. Render a RAC ComboBox inside <React.StrictMode> with about 10 disabled items followed by a few enabled ones, and a ListBox with max-height: 300px; overflow: auto.
  2. Focus the input and press ArrowDown.
  3. Before: "Cat" is focused (highlighted) but the list stays scrolled to the top. After: the list scrolls so "Cat" is visible.
  4. Pressing ArrowDown/ArrowUp after opening still scrolls as before, and opening with the mouse is unchanged (nothing gets focused, so nothing scrolls).

Tested: keyboard, Chromium, LTR, StrictMode and non-StrictMode. I didn't test screen readers, RTL, or touch. The changed code doesn't depend on direction or input type.

🧢 Your Project:

Personal contribution

In StrictMode, React runs effect cleanups and then re-runs effects right
after mount. The unmount cleanup in useSelectableCollection cancelled the
pending requestAnimationFrame that scrolls the focused item into view, and
the re-run scroll effect did not schedule it again, so a ComboBox opened
with the keyboard focused the first enabled option without scrolling to it.

Only treat the frame as pending until it runs, and if the cleanup cancels a
pending scroll, let the scroll effect retry it.

Closes adobe#10690
@kttalley

kttalley commented Oct 3, 2026

Copy link
Copy Markdown
Author

The test-ssr failures look unrelated to this change: all 60 SSR suites fail on browserslist's "caniuse-lite is 7 months old" warning, which setupTests turns into an error. Other PRs passed test-ssr earlier today, so I think the data just crossed browserslist's 6-month threshold. Happy to rebase once that's updated, or I can open a small separate PR bumping caniuse-lite in yarn.lock if that's helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ComboBox/ListBox does not scroll to the first non-disabled option when opening

1 participant