Skip to content

fix(combobox): modal focus issue#21733

Merged
riddhybansal merged 7 commits into
carbon-design-system:mainfrom
gDINESH13:21638_combobox_fix
Apr 28, 2026
Merged

fix(combobox): modal focus issue#21733
riddhybansal merged 7 commits into
carbon-design-system:mainfrom
gDINESH13:21638_combobox_fix

Conversation

@gDINESH13

@gDINESH13 gDINESH13 commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

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 wrapFocus handler calls scrollIntoView when an element receives focus, which happens during the click event. This scroll interrupts the click before it completes.

Changelog

New

  • None

Changed

  1. Skip auto-scroll for buttons: Buttons receive focus from explicit user clicks and should never auto-scroll
  2. Skip auto-scroll for visible elements: If an element is already fully visible, scrolling is unnecessary and disruptive

Removed

  • None

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:

  • Reviewed every line of the diff
    - [] Updated documentation and storybook examples
    - [] Wrote passing tests that cover this change
    - [] Addressed any impact on accessibility (a11y)
  • Tested for cross-browser consistency
  • Validated that this code is ready for review and status checks should pass

More details can be found in the pull request guide

@gDINESH13 gDINESH13 requested a review from a team as a code owner March 10, 2026 06:36
@netlify

netlify Bot commented Mar 10, 2026

Copy link
Copy Markdown

Deploy Preview for v11-carbon-web-components ready!

Name Link
🔨 Latest commit a2a640e
🔍 Latest deploy log https://app.netlify.com/projects/v11-carbon-web-components/deploys/69afbbe5b021120008248b6c
😎 Deploy Preview https://deploy-preview-21733--v11-carbon-web-components.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Mar 10, 2026

Copy link
Copy Markdown

Deploy Preview for v11-carbon-react ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit a2a640e
🔍 Latest deploy log https://app.netlify.com/projects/v11-carbon-react/deploys/69afbbe5cef73a00085331c3
😎 Deploy Preview https://deploy-preview-21733--v11-carbon-react.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Mar 10, 2026

Copy link
Copy Markdown

Deploy Preview for v11-carbon-web-components ready!

Name Link
🔨 Latest commit e8a9862
🔍 Latest deploy log https://app.netlify.com/projects/v11-carbon-web-components/deploys/69eb434247527d0008b2d022
😎 Deploy Preview https://deploy-preview-21733--v11-carbon-web-components.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Mar 10, 2026

Copy link
Copy Markdown

Deploy Preview for v11-carbon-react ready!

Name Link
🔨 Latest commit e8a9862
🔍 Latest deploy log https://app.netlify.com/projects/v11-carbon-react/deploys/69eb43423574f700087924f6
😎 Deploy Preview https://deploy-preview-21733--v11-carbon-react.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov

codecov Bot commented Mar 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.01%. Comparing base (8ddef80) to head (e8a9862).

Files with missing lines Patch % Lines
packages/react/src/components/Modal/Modal.tsx 0.00% 6 Missing ⚠️
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              
Flag Coverage Δ
main-packages 89.05% <0.00%> (-0.04%) ⬇️
web-components 97.86% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@adamalston adamalston left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@gDINESH13

Copy link
Copy Markdown
Contributor Author

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?

@adamalston

adamalston commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

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>
@gDINESH13

Copy link
Copy Markdown
Contributor Author

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.mov

Combo box

Screen.Recording.2026-03-31.at.2.21.52.PM.mov

I 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 adamalston left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add testing?

Comment on lines +466 to +474
// 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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ur right, I din thought of keyboard navigation accessibility.

The issue is distinguishing between:

  1. Click-initiated focus (button clicked → receives focus → shouldn't scroll)
  2. 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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using setTimeout does not seem ideal. Nothing about this issue suggests that it is timing related.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
image

Do you think, this doesn't match the expected outcome?

Comment on lines +476 to +486
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
image

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
image

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@adamalston adamalston Apr 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 visible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 tay1orjones left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@heloiselui heloiselui added this pull request to the merge queue Apr 24, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Apr 24, 2026
Comment on lines +476 to +486
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
}

@adamalston adamalston Apr 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 visible

@riddhybansal riddhybansal added this pull request to the merge queue Apr 28, 2026
Merged via the queue into carbon-design-system:main with commit c51ee16 Apr 28, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Combobox within modal focus issue [Bug]: Tabs do not activate in Modal sometimes

5 participants