From be8f9a30c06b056ba22296031e830f2f73c87cf6 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 10 Jan 2025 17:13:35 -0600 Subject: [PATCH] scroll list view on keyboard focus (#232644) --- src/vs/base/browser/ui/list/listView.ts | 38 ++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index e305b7de4b9a6..4e065bf3539d9 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DataTransfers, IDragAndDropData } from '../../dnd.js'; -import { $, addDisposableListener, animate, Dimension, getContentHeight, getContentWidth, getDocument, getTopLeftOffset, getWindow, isAncestor, isHTMLElement, isSVGElement, scheduleAtNextAnimationFrame } from '../../dom.js'; +import { $, addDisposableListener, animate, Dimension, getActiveElement, getContentHeight, getContentWidth, getDocument, getTopLeftOffset, getWindow, isAncestor, isHTMLElement, isSVGElement, scheduleAtNextAnimationFrame } from '../../dom.js'; import { DomEmitter } from '../../event.js'; import { IMouseWheelEvent } from '../../mouseEvent.js'; import { EventType as TouchEventType, Gesture, GestureEvent } from '../../touch.js'; @@ -324,6 +324,7 @@ export class ListView implements IListView { private onDragLeaveTimeout: IDisposable = Disposable.None; private currentSelectionDisposable: IDisposable = Disposable.None; private currentSelectionBounds: IRange | undefined; + private activeElement: HTMLElement | undefined; private readonly disposables: DisposableStore = new DisposableStore(); @@ -439,9 +440,13 @@ export class ListView implements IListView { this.scrollableElement.onScroll(this.onScroll, this, this.disposables); this.disposables.add(addDisposableListener(this.rowsContainer, TouchEventType.Change, e => this.onTouchChange(e as GestureEvent))); - // Prevent the monaco-scrollable-element from scrolling - // https://github.com/microsoft/vscode/issues/44181 - this.disposables.add(addDisposableListener(this.scrollableElement.getDomNode(), 'scroll', e => (e.target as HTMLElement).scrollTop = 0)); + this.disposables.add(addDisposableListener(this.scrollableElement.getDomNode(), 'scroll', e => { + // Make sure the active element is scrolled into view + const element = (e.target as HTMLElement); + const scrollValue = element.scrollTop; + element.scrollTop = 0; + this.setScrollTop(this.scrollTop + scrollValue); + })); this.disposables.add(addDisposableListener(this.domNode, 'dragover', e => this.onDragOver(this.toDragEvent(e)))); this.disposables.add(addDisposableListener(this.domNode, 'drop', e => this.onDrop(this.toDragEvent(e)))); @@ -460,6 +465,31 @@ export class ListView implements IListView { this.dnd = options.dnd ?? this.disposables.add(DefaultOptions.dnd); this.layout(options.initialSize?.height, options.initialSize?.width); + this._setupFocusObserver(container); + } + + private _setupFocusObserver(container: HTMLElement): void { + this.disposables.add(addDisposableListener(container, 'focus', () => { + const element = getActiveElement() as HTMLElement | null; + if (this.activeElement !== element && element !== null) { + this.activeElement = element; + this._scrollToActiveElement(this.activeElement, container); + } + }, true)); + } + + private _scrollToActiveElement(element: HTMLElement, container: HTMLElement) { + // The scroll event on the list only fires when scrolling down. + // If the active element is above the viewport, we need to scroll up. + const containerRect = container.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + + const topOffset = elementRect.top - containerRect.top; + + if (topOffset < 0) { + // Scroll up + this.setScrollTop(this.scrollTop + topOffset); + } } updateOptions(options: IListViewOptionsUpdate) {