Skip to content

Commit 13f3394

Browse files
fix(cdk/listbox): avoid resetting scroll position when using mouse
The CDK listbox has some logic that forwards focus to the first item when the host is focused. The problem is that every time the user clicks on the scrollbar, they blur the current item and focus the listbox which then forwards focus back to the first item which in turn causes the scroll position to jump to the top. These changes add some logic to not forward focus when focus comes from a mouse interaction. Fixes #30900
1 parent 0c6494e commit 13f3394

File tree

1 file changed

+23
-9
lines changed

1 file changed

+23
-9
lines changed

src/cdk/listbox/listbox.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {
1010
_IdGenerator,
1111
ActiveDescendantKeyManager,
12+
FocusMonitor,
1213
Highlightable,
1314
ListKeyManagerOption,
1415
} from '../a11y';
@@ -243,7 +244,6 @@ export class CdkOption<T = unknown> implements ListKeyManagerOption, Highlightab
243244
'[attr.aria-multiselectable]': 'multiple',
244245
'[attr.aria-activedescendant]': '_getAriaActiveDescendant()',
245246
'[attr.aria-orientation]': 'orientation',
246-
'(focus)': '_handleFocus()',
247247
'(keydown)': '_handleKeydown($event)',
248248
'(focusout)': '_handleFocusOut($event)',
249249
'(focusin)': '_handleFocusIn()',
@@ -269,6 +269,7 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
269269
}
270270
private _id: string;
271271
private _generatedId = inject(_IdGenerator).getId('cdk-listbox-');
272+
private _focusMonitor = inject(FocusMonitor);
272273

273274
/** The tabindex to use when the listbox is enabled. */
274275
@Input('tabindex')
@@ -462,6 +463,7 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
462463
}
463464

464465
this._initKeyManager();
466+
this._handleFocus();
465467

466468
// Update the internal value whenever the options or the model value changes.
467469
merge(this.selectionModel.changed, this.options.changes)
@@ -477,6 +479,7 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
477479
}
478480

479481
ngOnDestroy() {
482+
this._focusMonitor.stopMonitoring(this.element);
480483
this._cleanupWindowBlur?.();
481484
this.listKeyManager?.destroy();
482485
this.destroyed.next();
@@ -698,15 +701,26 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
698701

699702
/** Called when the listbox receives focus. */
700703
protected _handleFocus() {
701-
if (!this.useActiveDescendant) {
702-
if (this.selectionModel.selected.length > 0) {
703-
this._setNextFocusToSelectedOption();
704-
} else {
705-
this.listKeyManager.setNextItemActive();
706-
}
704+
this._focusMonitor
705+
.monitor(this.element, false)
706+
.pipe(takeUntil(this.destroyed))
707+
.subscribe(focusOrigin => {
708+
// Don't forward focus on mouse interactions, because it can
709+
// mess with the user's scroll position. See #30900.
710+
if (focusOrigin === 'mouse' || focusOrigin === null) {
711+
return;
712+
}
707713

708-
this._focusActiveOption();
709-
}
714+
if (!this.useActiveDescendant) {
715+
if (this.selectionModel.selected.length > 0) {
716+
this._setNextFocusToSelectedOption();
717+
} else {
718+
this.listKeyManager.setNextItemActive();
719+
}
720+
721+
this._focusActiveOption();
722+
}
723+
});
710724
}
711725

712726
/** Called when the user presses keydown on the listbox. */

0 commit comments

Comments
 (0)