fix(combobox): modal focus issue#21733
Conversation
✅ Deploy Preview for v11-carbon-web-components ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for v11-carbon-react ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for v11-carbon-web-components ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for v11-carbon-react ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #21733 +/- ##
==========================================
- Coverage 95.05% 95.01% -0.04%
==========================================
Files 538 538
Lines 45053 45059 +6
Branches 6550 6542 -8
==========================================
- Hits 42827 42815 -12
- Misses 2097 2115 +18
Partials 129 129
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
adamalston
left a comment
There was a problem hiding this comment.
I reverted the fix, and the test still passes, which suggests that it is not covering the changes.
yarn test packages/react/src/components/ComboBox/ComboBox-test.js
git checkout main packages/react/src/components/ListBox/next/ListBoxSelection.tsx packages/react/src/internal/wrapFocus.ts
yarn test packages/react/src/components/ComboBox/ComboBox-test.js|
Hi @adamalston thankyou for the review. Is it fine if I add a new story in combobox-test to see if it fixed the issue? I can revert the example later, after review. Or if I create a new story in combobox ( just for testing & reviewing ), it shall be moved together with this PR? |
|
Hi, I think there may be a misunderstanding. My point was that if I revert the fix, the existing test still passes, which means the test isn't actually covering the issue this pull request intends to fix. Ideally we should have a test that fails without the fix and passes with it. That way we know the behavior is actually being validated. Adding a Storybook example could help demonstrate the issue, but it wouldn't replace adding a test that reproduces the issue. |
Signed-off-by: gDINESH13 <dinesh13g@gmail.com>
898b31a to
e37e69a
Compare
|
Hi @adamalston sorry it's been a while since ur comment. Initially I have raised this PR only for #21638 but later I saw @maradwan26 pointing the same problem in other issue #21153, did some research and I think the problem is in Modal component, I thought its an issue on combobox initially. I have updated the PR description with new changes. Adding the screen recordings of SlackBits mentioned in issues, Tabs Screen.Recording.2026-03-31.at.2.19.48.PM.movCombo box Screen.Recording.2026-03-31.at.2.21.52.PM.movI haven't added story book examples in the commit, If guys feel it's better to have them in repo, I don't mind adding later. Please let me know what you guys think. Thanks. |
adamalston
left a comment
There was a problem hiding this comment.
Could you add testing?
| // Check if the focused element is a button - buttons should never auto-scroll | ||
| // as clicking them is an explicit user action that shouldn't be interrupted | ||
| const isButton = currentActiveNode.matches( | ||
| 'button, [role="button"], input[type="button"], input[type="submit"]' | ||
| ); | ||
|
|
||
| if (isButton) { | ||
| return; // Never auto-scroll buttons | ||
| } |
There was a problem hiding this comment.
Should this early return be scoped to focus changes caused by clicking or tapping, or otherwise keep keyboard focused buttons scrollable into view? With the blanket button check here, if you tab to a button or [role="button"] inside a long modal body, focus can stay below the visible area under the fade.
There was a problem hiding this comment.
Oh ur right, I din thought of keyboard navigation accessibility.
The issue is distinguishing between:
- Click-initiated focus (button clicked → receives focus → shouldn't scroll)
- Keyboard-initiated focus (Tab key → button receives focus → SHOULD scroll)
Unfortunately, by the time the focus event fires, we can't reliably determine what caused it (click vs keyboard). The FocusEvent doesn't provide this information.
The intention initially for this change was,
When the ComboBox clear button (X) was partially visible at the edge of the modal, clicking it would:
- Give the button focus
- Modal's focus handler would call scrollIntoView
- The scroll would happen during the click event
- The scroll interrupted the click, preventing the clear action
- User had to click again after scrolling to actually clear
The button check was meant to prevent this interruption by never scrolling buttons.
I will check if there are any better way to handle this.
I thought of adding a setTimeout
setTimeout(() => {
currentActiveNode.scrollIntoView({ block: 'center' });
}, 10);
To defer the scroll until after the click event completes. Do you think this would be a better approach?
There was a problem hiding this comment.
Using setTimeout does not seem ideal. Nothing about this issue suggests that it is timing related.
There was a problem hiding this comment.
I have removed this entire button check of stopping the scrollintoview in modal. The combobox does clears its input with the current changes. When the element is completely into view. When combo-box is partially visibile like this, it scrolls the item into view before clearing first.

Do you think, this doesn't match the expected outcome?
| const modalRect = modalContent.getBoundingClientRect(); | ||
| const elementRect = currentActiveNode.getBoundingClientRect(); | ||
|
|
||
| // Check if element is fully visible within the modal viewport | ||
| const isFullyVisible = | ||
| elementRect.top >= modalRect.top && | ||
| elementRect.bottom <= modalRect.bottom; | ||
|
|
||
| if (isFullyVisible) { | ||
| return; // Don't scroll if already fully visible | ||
| } |
There was a problem hiding this comment.
How is the faded bottom area accounted for in this visibility check? ${prefix}--modal-scroll-content fades out the last part of the container, so an input can satisfy elementRect.bottom <= modalRect.bottom while still being visually obscured.
There was a problem hiding this comment.
You are right again 🫡. The current visibility check doesn't account for the fade gradient. An element at the bottom edge can pass the check (elementRect.bottom <= modalRect.bottom) while being partially obscured by the fade.
I added this because even when combobox is completely visible like this,

Clicking the button triggers scroll, rather than showing dropdown options.
Do you think adding some fixed margin or boundary
const FADE_MARGIN = 48; // Adjust based on actual fade height
const isFullyVisible =
elementRect.top >= modalRect.top &&
elementRect.bottom <= (modalRect.bottom - FADE_MARGIN);
This may not be the ideal way to handle this,
but this actually questions me back to the need of scroll into view,
for scenarios like this, where button cancel in combobox is partially visible

if I remove the button check you pointed above, it will scroll the element into view.
Can you point me how much of an element should be visible in the modal for it to avoid scrolling?
There was a problem hiding this comment.
My initial thought is that the entire element should be visible, as scrolling would not be necessary if it is fully in view. However, there may be other considerations (e.g., accessibility) that could influence the display. I didn't find any specific guidance in https://carbondesignsystem.com/components/modal/accessibility/ or https://carbondesignsystem.com/components/modal/style/.
There was a problem hiding this comment.
Hi Adam,
I tried to access the keyboard way of accessing the tabs below the visibility and the tabs which are not visible are actually scrolling into view even when (fade gradient ) is present.
So I guess it does handle elementRect.bottom <= modalRect.bottom those tabs at bottom.
Screen.Recording.2026-04-12.at.2.51.07.PM.mov
There was a problem hiding this comment.
Okay, sounds good. I was going to suggest a change like to the following to fully account for the faded area. However, if what you have works, I think that's fine.
diff --git a/packages/react/src/components/Modal/Modal.tsx b/packages/react/src/components/Modal/Modal.tsx
index 9b978f5c75..1b5cf73047 100644
--- a/packages/react/src/components/Modal/Modal.tsx
+++ b/packages/react/src/components/Modal/Modal.tsx
@@ -463,13 +463,22 @@ const ModalDialog = React.forwardRef(function ModalDialog(
return;
}
+ const hasActiveScrollFade =
+ !modalContent.classList.contains(
+ `${prefix}--modal-scroll-content--no-fade`
+ ) && !modalContent.querySelector(`.${prefix}--autoalign`);
const modalRect = modalContent.getBoundingClientRect();
+ /** Corresponds to the bottom fade distance in `_modal.scss`. */
+ const modalScrollFadeHeight = 80;
+ const visibleModalBottom = hasActiveScrollFade
+ ? modalRect.bottom - modalScrollFadeHeight
+ : modalRect.bottom;
const elementRect = currentActiveNode.getBoundingClientRect();
// Check if element is fully visible within the modal viewport
const isFullyVisible =
elementRect.top >= modalRect.top &&
- elementRect.bottom <= modalRect.bottom;
+ elementRect.bottom <= visibleModalBottom;
if (isFullyVisible) {
return; // Don't scroll if already fully visibleThere was a problem hiding this comment.
Yea thanks I kind of thought that too, if it din worked that was my next step.
Signed-off-by: gDINESH13 <dinesh13g@gmail.com>
tay1orjones
left a comment
There was a problem hiding this comment.
LGTM, I added a playwright e2e test to assert that scroll only happens when necessary. We can cover addl edge cases in subsequent PRs. We've had a couple folks reach out about this looking for a fix.
| const modalRect = modalContent.getBoundingClientRect(); | ||
| const elementRect = currentActiveNode.getBoundingClientRect(); | ||
|
|
||
| // Check if element is fully visible within the modal viewport | ||
| const isFullyVisible = | ||
| elementRect.top >= modalRect.top && | ||
| elementRect.bottom <= modalRect.bottom; | ||
|
|
||
| if (isFullyVisible) { | ||
| return; // Don't scroll if already fully visible | ||
| } |
There was a problem hiding this comment.
Okay, sounds good. I was going to suggest a change like to the following to fully account for the faded area. However, if what you have works, I think that's fine.
diff --git a/packages/react/src/components/Modal/Modal.tsx b/packages/react/src/components/Modal/Modal.tsx
index 9b978f5c75..1b5cf73047 100644
--- a/packages/react/src/components/Modal/Modal.tsx
+++ b/packages/react/src/components/Modal/Modal.tsx
@@ -463,13 +463,22 @@ const ModalDialog = React.forwardRef(function ModalDialog(
return;
}
+ const hasActiveScrollFade =
+ !modalContent.classList.contains(
+ `${prefix}--modal-scroll-content--no-fade`
+ ) && !modalContent.querySelector(`.${prefix}--autoalign`);
const modalRect = modalContent.getBoundingClientRect();
+ /** Corresponds to the bottom fade distance in `_modal.scss`. */
+ const modalScrollFadeHeight = 80;
+ const visibleModalBottom = hasActiveScrollFade
+ ? modalRect.bottom - modalScrollFadeHeight
+ : modalRect.bottom;
const elementRect = currentActiveNode.getBoundingClientRect();
// Check if element is fully visible within the modal viewport
const isFullyVisible =
elementRect.top >= modalRect.top &&
- elementRect.bottom <= modalRect.bottom;
+ elementRect.bottom <= visibleModalBottom;
if (isFullyVisible) {
return; // Don't scroll if already fully visiblec51ee16
Closes #21638
Closes #21153
When clicking interactive elements (ComboBox clear button, tabs) inside a Modal, the Modal's auto-scroll behavior interrupts the click event, requiring users to click twice - once to scroll the element into view, and again to actually trigger the action.
Cause:
The Modal's
wrapFocushandler callsscrollIntoViewwhen an element receives focus, which happens during the click event. This scroll interrupts the click before it completes.Changelog
New
Changed
Removed
Testing / Reviewing
I haven't added test case, it was tricky to emulate test cases, with scrolling scenarios. However I have created stories with the slack bits examples shared in the issues and have tested the functionality after the fix.
I have added screen recoding of testing in the comments, please take a look 😄
PR Checklist
As the author of this PR, before marking ready for review, confirm you:
- [] Updated documentation and storybook examples- [] Wrote passing tests that cover this change- [] Addressed any impact on accessibility (a11y)More details can be found in the pull request guide