Merge pull request #303511 from microsoft/benibenj/presidential-ox

Multi-select adoption for compressed tree
This commit is contained in:
Benjamin Christopher Simmonds
2026-03-20 15:36:11 +01:00
committed by GitHub
3 changed files with 104 additions and 5 deletions

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IDragAndDropData } from '../../dnd.js';
import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, IListMouseEvent, IListTouchEvent, IListVirtualDelegate } from '../list/list.js';
import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, IListMouseEvent, IListTouchEvent, IListVirtualDelegate, NotSelectableGroupIdType } from '../list/list.js';
import { ElementsDragAndDropData, ListViewTargetSector } from '../list/listView.js';
import { IListStyles } from '../list/listWidget.js';
import { ComposedTreeDelegate, TreeFindMode, IAbstractTreeOptions, IAbstractTreeOptionsUpdate, TreeFindMatchType, AbstractTreePart, LabelFuzzyScore, FindFilter, FindController, ITreeFindToggleChangeEvent, IFindControllerOptions, IStickyScrollDelegate, AbstractTree } from './abstractTree.js';
@@ -1309,7 +1309,10 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
diffIdentityProvider: options.diffIdentityProvider && {
getId(node: IAsyncDataTreeNode<TInput, T>): { toString(): string } {
return options.diffIdentityProvider!.getId(node.element as T);
}
},
getGroupId: options.diffIdentityProvider!.getGroupId ? (node: IAsyncDataTreeNode<TInput, T>): number | NotSelectableGroupIdType => {
return options.diffIdentityProvider!.getGroupId!(node.element as T);
} : undefined
}
};

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IIdentityProvider } from '../list/list.js';
import { IIdentityProvider, NotSelectableGroupIdType } from '../list/list.js';
import { getVisibleState, IIndexTreeModelSpliceOptions, isFilterResult } from './indexTreeModel.js';
import { IObjectTreeModel, IObjectTreeModelOptions, IObjectTreeModelSetChildrenOptions, ObjectTreeModel } from './objectTreeModel.js';
import { ICollapseStateChangeEvent, IObjectTreeElement, ITreeListSpliceData, ITreeModel, ITreeModelSpliceEvent, ITreeNode, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from './tree.js';
@@ -113,7 +113,10 @@ interface ICompressedObjectTreeModelOptions<T, TFilterData> extends IObjectTreeM
const wrapIdentityProvider = <T>(base: IIdentityProvider<T>): IIdentityProvider<ICompressedTreeNode<T>> => ({
getId(node) {
return node.elements.map(e => base.getId(e).toString()).join('\0');
}
},
getGroupId: base.getGroupId ? (node: ICompressedTreeNode<T>): number | NotSelectableGroupIdType => {
return base.getGroupId!(node.elements[node.elements.length - 1]);
} : undefined
});
// Exported only for test reasons, do not use directly
@@ -380,7 +383,10 @@ function mapOptions<T, TFilterData>(compressedNodeUnwrapper: CompressedNodeUnwra
identityProvider: options.identityProvider && {
getId(node: ICompressedTreeNode<T>): { toString(): string } {
return options.identityProvider!.getId(compressedNodeUnwrapper(node));
}
},
getGroupId: options.identityProvider!.getGroupId ? (node: ICompressedTreeNode<T>): number | NotSelectableGroupIdType => {
return options.identityProvider!.getGroupId!(compressedNodeUnwrapper(node));
} : undefined
},
sorter: options.sorter && {
compare(node: ICompressedTreeNode<T>, otherNode: ICompressedTreeNode<T>): number {

View File

@@ -8,6 +8,7 @@ import { IIdentityProvider, IListVirtualDelegate } from '../../../../browser/ui/
import { ICompressedTreeNode } from '../../../../browser/ui/tree/compressedObjectTreeModel.js';
import { CompressibleObjectTree, ICompressibleTreeRenderer, ObjectTree } from '../../../../browser/ui/tree/objectTree.js';
import { ITreeNode, ITreeRenderer } from '../../../../browser/ui/tree/tree.js';
import { runWithFakedTimers } from '../../../common/timeTravelScheduler.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../common/utils.js';
function getRowsTextContent(container: HTMLElement): string[] {
@@ -16,6 +17,17 @@ function getRowsTextContent(container: HTMLElement): string[] {
return rows.map(row => row.querySelector('.monaco-tl-contents')!.textContent!);
}
function clickElement(element: HTMLElement, ctrlKey = false): void {
element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, ctrlKey, button: 0 }));
element.dispatchEvent(new MouseEvent('click', { bubbles: true, ctrlKey, button: 0 }));
}
function dispatchKeydown(element: HTMLElement, key: string, code: string, keyCode: number): void {
const keyboardEvent = new KeyboardEvent('keydown', { bubbles: true, key, code });
Object.defineProperty(keyboardEvent, 'keyCode', { get: () => keyCode });
element.dispatchEvent(keyboardEvent);
}
suite('ObjectTree', function () {
suite('TreeNavigator', function () {
@@ -231,6 +243,84 @@ suite('ObjectTree', function () {
tree.setChildren(null, [{ element: 100 }, { element: 101 }, { element: 102 }, { element: 103 }]);
assert.deepStrictEqual(tree.getFocus(), [101]);
});
test('updateOptions preserves wrapped identity provider in view options', function () {
const container = document.createElement('div');
container.style.width = '200px';
container.style.height = '200px';
const delegate = new Delegate();
const renderer = new Renderer();
const identityProvider = {
getId(element: number): { toString(): string } {
return `${element}`;
},
getGroupId(element: number): number {
return element % 2;
}
};
const tree = new ObjectTree<number>('test', container, delegate, [renderer], { identityProvider });
try {
tree.layout(200);
tree.setChildren(null, [{ element: 0 }, { element: 1 }, { element: 2 }, { element: 3 }]);
const firstRow = container.querySelector('.monaco-list-row[data-index="0"]') as HTMLElement;
const secondRow = container.querySelector('.monaco-list-row[data-index="1"]') as HTMLElement;
clickElement(firstRow);
assert.deepStrictEqual(tree.getSelection(), [0]);
tree.updateOptions({ indent: 12 });
clickElement(secondRow, true);
assert.deepStrictEqual(tree.getSelection(), [1]);
} finally {
tree.dispose();
}
});
test('updateOptions preserves wrapped accessibility provider for type navigation re-announce', async function () {
const container = document.createElement('div');
container.style.width = '200px';
container.style.height = '200px';
const delegate = new Delegate();
const renderer = new Renderer();
const accessibilityProvider = {
getAriaLabel(element: number): string {
assert.strictEqual(typeof element, 'number');
return `aria ${element}`;
},
getWidgetAriaLabel(): string {
return 'tree';
}
};
const tree = new ObjectTree<number>('test', container, delegate, [renderer], {
accessibilityProvider,
keyboardNavigationLabelProvider: {
getKeyboardNavigationLabel: () => 'a'
}
});
try {
await runWithFakedTimers({ useFakeTimers: true }, async () => {
tree.layout(200);
tree.setChildren(null, [{ element: 0 }]);
tree.setFocus([0]);
tree.domFocus();
tree.updateOptions({ indent: 12 });
dispatchKeydown(tree.getHTMLElement(), 'a', 'KeyA', 65);
await Promise.resolve();
});
} finally {
tree.dispose();
}
});
});
suite('CompressibleObjectTree', function () {