From 1f03ee89f282556aa1678464741721473a705ac1 Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 16 Jul 2025 01:15:51 +0200 Subject: [PATCH 01/11] Rewrite findPrevItem() --- src/vscode-tree/helpers.test.ts | 129 ++++++++++++++++++++++++++++++++ src/vscode-tree/helpers.ts | 35 ++++----- 2 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 src/vscode-tree/helpers.test.ts diff --git a/src/vscode-tree/helpers.test.ts b/src/vscode-tree/helpers.test.ts new file mode 100644 index 000000000..9a92873a9 --- /dev/null +++ b/src/vscode-tree/helpers.test.ts @@ -0,0 +1,129 @@ +import {expect, fixture, html} from '@open-wc/testing'; +import {findLastChildItem, findPrevItem} from './helpers'; +import {VscodeTreeItem} from '../vscode-tree-item'; +import '../vscode-tree-item/vscode-tree-item.js'; +import {VscodeTree} from './vscode-tree'; +import './vscode-tree.js'; + +describe('vscode-tree helpers', () => { + describe('findPrevItem()', () => { + it('returns null if does not have parent element', () => { + const el = document.createElement('vscode-tree-item'); + + expect(findPrevItem(el)).to.be.null; + }); + + it('returns null if item is not tree item', () => { + const el = document.createElement('span'); + + expect(findPrevItem(el as VscodeTreeItem)).to.be.null; + }); + + it('returns previous sibling', async () => { + const el = await fixture(html` + + 1 + 2 + + `); + + const item1 = el.querySelector('#item1')!; + const item2 = el.querySelector('#item2')!; + + expect(findPrevItem(item2)).to.eq(item1); + }); + + it('returns previous tree item', async () => { + const el = await fixture(html` + + 1 +
aaa
+ 2 +
+ `); + + const item1 = el.querySelector('#item1')!; + const item2 = el.querySelector('#item2')!; + + expect(findPrevItem(item2)).to.eq(item1); + }); + + it('returns the previous closed branch', async () => { + const el = await fixture(html` + + + 1 + 1.1 + 1.2 + + 2 + + `); + + const item1 = el.querySelector('#item1')!; + const item2 = el.querySelector('#item2')!; + + expect(findPrevItem(item2)).to.eq(item1); + }); + + it('returns last children of the opened branch', async () => { + const el = await fixture(html` + + + 1 + 1.1 + 1.2 + + 2 + + `); + + const item2 = el.querySelector('#item2')!; + const item1_2 = el.querySelector('#item1_2')!; + + expect(findPrevItem(item2)).to.eq(item1_2); + }); + + it('returns last children of the nested opened branch', async () => { + const el = await fixture(html` + + + 1 + 1.1 + + 1.2 + 1.2.1 + 1.2.2 + + + 2 + + `); + + const item2 = el.querySelector('#item2')!; + const item1_2_2 = el.querySelector('#item1_2_2')!; + + expect(findPrevItem(item2)).to.eq(item1_2_2); + }); + }); + + describe('findLastChildItem()', () => { + it('returns the last child item', async () => { + const el = await fixture(html` + + + 1 + 1.1 + 1.2 + + 2 + + `); + + const item1 = el.querySelector('#item1')!; + const item1_2 = el.querySelector('#item1_2')!; + + expect(findLastChildItem(item1)).to.eq(item1_2); + }); + }); +}); diff --git a/src/vscode-tree/helpers.ts b/src/vscode-tree/helpers.ts index 0f4981a48..e618e41c5 100644 --- a/src/vscode-tree/helpers.ts +++ b/src/vscode-tree/helpers.ts @@ -1,11 +1,11 @@ -import {VscodeTreeItem} from '../vscode-tree-item'; -import type {VscodeTree} from './vscode-tree'; +import type {VscodeTreeItem} from '../vscode-tree-item/vscode-tree-item.js'; +import type {VscodeTree} from './vscode-tree.js'; -const isTreeItem = (item: HTMLElement): item is VscodeTreeItem => - item.tagName.toLowerCase() === 'vscode-tree-item'; +const isTreeItem = (item: Element | null | undefined): item is VscodeTreeItem => + item instanceof Element && item.matches('vscode-tree-item'); -const isTreeRoot = (item: HTMLElement): item is VscodeTree => - item.tagName.toLowerCase() === 'vscode-tree'; +const isTreeRoot = (item: Element | null | undefined): item is VscodeTree => + item instanceof Element && item.matches('vscode-tree'); export const initPathTrackerProps = ( parentElement: VscodeTree | VscodeTreeItem, @@ -42,16 +42,12 @@ export const initPathTrackerProps = ( }; export const findLastChildItem = (item: VscodeTreeItem): VscodeTreeItem => { - const children = item.querySelectorAll( - ':scope > vscode-tree-item' - ); + const lastItem = item.lastElementChild; - if (children.length < 1) { + if (!lastItem || !isTreeItem(lastItem)) { return item; } - const lastItem = children[children.length - 1]; - if (lastItem.branch && lastItem.open) { return findLastChildItem(lastItem); } else { @@ -125,19 +121,20 @@ export const findNextItem = (item: VscodeTreeItem): VscodeTreeItem | null => { export const findPrevItem = (item: VscodeTreeItem): VscodeTreeItem | null => { const {parentElement} = item; - const index = parseInt(item.dataset.index ?? '-1', 10); - if (!parentElement) { + if (!parentElement || !isTreeItem(item)) { return null; } - const prevSibling = parentElement.querySelector( - `:scope vscode-tree-item[data-index="${index - 1}"]` - ); + let prevSibling: Element | null = item.previousElementSibling; + + while (prevSibling && !isTreeItem(prevSibling)) { + prevSibling = prevSibling.previousElementSibling; + } if (!prevSibling) { - if (parentElement.tagName.toLowerCase() === 'vscode-tree-item') { - return parentElement as VscodeTreeItem; + if (isTreeItem(parentElement)) { + return parentElement; } } From 27f48edd584742020d47f9cf222db6d345f03a77 Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 16 Jul 2025 01:16:12 +0200 Subject: [PATCH 02/11] Shorter timeout --- web-test-runner.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web-test-runner.config.js b/web-test-runner.config.js index e4f7d0896..d07e7d771 100644 --- a/web-test-runner.config.js +++ b/web-test-runner.config.js @@ -5,6 +5,7 @@ export default { nodeResolve: true, preserveSymlinks: true, browsers: [playwrightLauncher({product: 'chromium'})], + testsFinishTimeout: 3000, filterBrowserLogs: ({args}) => !( typeof args[0] === 'string' && From 22e7d0755f7ef8a745f8cc148060db273e80c11e Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 16 Jul 2025 01:17:31 +0200 Subject: [PATCH 03/11] Add edge case to demo page --- dev/vscode-tree/basic-example.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/vscode-tree/basic-example.html b/dev/vscode-tree/basic-example.html index b153e8515..ba3dc4f65 100644 --- a/dev/vscode-tree/basic-example.html +++ b/dev/vscode-tree/basic-example.html @@ -53,7 +53,7 @@

Basic example

Tuition Undergraduate Graduate - Professional Schools + Professional Schools Sign Up Visit From 8e8010ecab3060905bf7f504286f2e7a2bddd252 Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 16 Jul 2025 23:30:07 +0200 Subject: [PATCH 04/11] Optimize the next item selection logic --- src/vscode-tree/helpers.ts | 113 ++++++++++++------------------------- 1 file changed, 37 insertions(+), 76 deletions(-) diff --git a/src/vscode-tree/helpers.ts b/src/vscode-tree/helpers.ts index e618e41c5..d474c5ba3 100644 --- a/src/vscode-tree/helpers.ts +++ b/src/vscode-tree/helpers.ts @@ -55,7 +55,7 @@ export const findLastChildItem = (item: VscodeTreeItem): VscodeTreeItem => { } }; -export const findClosestParentHasNextSibling = ( +export const findClosestAncestorHasNextSibling = ( item: VscodeTreeItem ): VscodeTreeItem | null => { if (!item.parentElement) { @@ -66,57 +66,61 @@ export const findClosestParentHasNextSibling = ( return null; } - const isLast = item.parentElement.dataset.last === 'true' ? true : false; + const nextSiblingOfParent = findNextTreeItemElementSibling( + item.parentElement + ); - if (!isLast) { - return item.parentElement; + if (nextSiblingOfParent) { + return nextSiblingOfParent; } else { - return findClosestParentHasNextSibling(item.parentElement); + return findClosestAncestorHasNextSibling(item.parentElement); } }; -export const findNextItem = (item: VscodeTreeItem): VscodeTreeItem | null => { - if (item.branch && item.open) { - return item.querySelector('vscode-tree-item'); +const findNextTreeItemElementSibling = (item: VscodeTreeItem) => { + let nextSibling: Element | null = item.nextElementSibling; + + while (nextSibling && !isTreeItem(nextSibling)) { + nextSibling = nextSibling.nextElementSibling; } + return nextSibling; +}; + +export const findNextItem = (item: VscodeTreeItem): VscodeTreeItem | null => { const {parentElement} = item; - if (!parentElement) { + if (!parentElement || !isTreeItem(item)) { return null; } - const numSiblings = parseInt(parentElement.dataset.numChildren ?? '0', 10); - const index = parseInt(item.dataset.index ?? '-1', 10); - - let level = 0; - - if ('level' in item) { - level = item.level; - } + let nextItem: VscodeTreeItem | null; - if (index === numSiblings - 1) { - const closestParent = findClosestParentHasNextSibling(item); + if (item.branch && item.open) { + const firstChildItem = item.querySelector('vscode-tree-item'); - if (closestParent) { - const cpIndex = parseInt(closestParent.dataset.index ?? '', 10) + 1; - const cpLevel = closestParent.level; + if (!firstChildItem) { + nextItem = findNextTreeItemElementSibling(item); - return ( - closestParent.parentElement?.querySelector( - `vscode-tree-item[level="${cpLevel}"][data-index="${cpIndex}"]` - ) ?? null - ); + if (!nextItem) { + nextItem = findClosestAncestorHasNextSibling(item); + } } else { - return null; + nextItem = firstChildItem; } - } + } else { + nextItem = findNextTreeItemElementSibling(item); - const nextElementIndex = Math.min(numSiblings - 1, index + 1); + if (!nextItem) { + nextItem = findClosestAncestorHasNextSibling(item); + } + } - return parentElement.querySelector( - `vscode-tree-item[level="${level}"][data-index="${nextElementIndex}"]` - ); + if (!nextItem) { + return item; + } else { + return nextItem; + } }; export const findPrevItem = (item: VscodeTreeItem): VscodeTreeItem | null => { @@ -147,49 +151,6 @@ export const findPrevItem = (item: VscodeTreeItem): VscodeTreeItem | null => { return prevSibling; }; -export const findAncestorOnSpecificLevel = ( - item: VscodeTreeItem, - level: number -): VscodeTreeItem | null => { - if ( - !item.parentElement || - item.parentElement.tagName.toLowerCase() !== 'vscode-tree-item' - ) { - return null; - } - - const parent = item.parentElement as VscodeTreeItem; - const itemLevel = +(item.dataset.level ?? ''); - - if (itemLevel > level) { - return findAncestorOnSpecificLevel(parent, level); - } - - if (itemLevel === level) { - return item; - } - - return null; -}; - -export const selectItemAndAllVisibleDescendants = (item: VscodeTreeItem) => { - if (!item) { - return; - } - - item.selected = true; - - if (item.branch && item.open) { - const children = item.querySelectorAll( - ':scope > vscode-tree-item' - ); - - children.forEach((c) => { - selectItemAndAllVisibleDescendants(c); - }); - } -}; - export function getParentItem(childItem: VscodeTreeItem) { if (!childItem.parentElement) { return null; From b0917b36f0f6a58d3bdaab69d1313e43f6d560cf Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 16 Jul 2025 23:42:33 +0200 Subject: [PATCH 05/11] Polish the docs --- src/vscode-tree/vscode-tree.ts | 43 ++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/vscode-tree/vscode-tree.ts b/src/vscode-tree/vscode-tree.ts index 0140d33a0..5121738fd 100644 --- a/src/vscode-tree/vscode-tree.ts +++ b/src/vscode-tree/vscode-tree.ts @@ -67,8 +67,6 @@ const DEFAULT_INDENT_GUIDE_DISPLAY = IndentGuides.onHover; /** * @tag vscode-tree - * - * @prop {'singleClick' | 'doubleClick'} expandMode */ @customElement('vscode-tree') export class VscodeTree extends VscElement { @@ -80,27 +78,55 @@ export class VscodeTree extends VscElement { * Although arrows are always visible in the Tree component by default in VSCode, some icon sets * (e.g., Material Icon Theme) allow disabling them in the file explorer view. This flag makes it * possible to mimic that behavior. + * + * @type {boolean} + * @default false */ @property({type: Boolean, reflect: true, attribute: 'hide-arrows'}) - hideArrows = false; + hideArrows: boolean = DEFAULT_HIDE_ARROWS; /** * Controls how tree folders are expanded when clicked. This property is designed to use * the `workbench.tree.expandMode` setting. + * + * Valid options are available as constants. + * + * ```javascript + * import {ExpandMode} from '@vscode-elements/elements/dist/vscode-tree/vscode-tree.js'; + * + * document.querySelector('vscode-tree').expandMode = ExpandMode.singleClick; + * ``` + * + * @type {'singleClick' | 'doubleClick'} + * @default 'singleClick' */ @property({type: String, attribute: 'expand-mode'}) - expandMode: ExpandMode = 'singleClick'; + expandMode: ExpandMode = DEFAULT_EXPAND_MODE; /** * Controls the indentation in pixels. This property is designed to use the * `workbench.tree.indent` setting. + * + * @type {number} + * @default 8 */ @property({type: Number, reflect: true}) - indent = 8; + indent: number = DEFAULT_INDENT; /** * Controls whether the tree should render indent guides. This property is * designed to use the `workbench.tree.renderIndentGuides` setting. + * + * Valid options are available as constants. + * + * ```javascript + * import {IndentGuides} from '@vscode-elements/elements/dist/vscode-tree/vscode-tree.js'; + * + * document.querySelector('vscode-tree').expandMode = IndentGuides.onHover; + * ``` + * + * @type {'none' | 'onHover' | 'always'} + * @default 'onHover' */ @property({ type: String, @@ -108,13 +134,16 @@ export class VscodeTree extends VscElement { useDefault: true, reflect: true, }) - indentGuides: IndentGuideDisplay = 'onHover'; + indentGuides: IndentGuideDisplay = DEFAULT_INDENT_GUIDE_DISPLAY; /** * Allows selecting multiple items. + * + * @type {boolean} + * @default false */ @property({type: Boolean, reflect: true, attribute: 'multi-select'}) - multiSelect = false; + multiSelect: boolean = DEFAULT_MULTI_SELECT; //#endregion From d299e9ac12d27289296f290759c93ea6182bdbdd Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 17 Jul 2025 00:23:23 +0200 Subject: [PATCH 06/11] Optimize the initial render --- src/vscode-tree/helpers.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/vscode-tree/helpers.ts b/src/vscode-tree/helpers.ts index d474c5ba3..91b08732b 100644 --- a/src/vscode-tree/helpers.ts +++ b/src/vscode-tree/helpers.ts @@ -20,12 +20,7 @@ export const initPathTrackerProps = ( parentElement.branch = numChildren > 0; } - parentElement.dataset.numChildren = numChildren.toString(); - items.forEach((item, i) => { - const level = parentElementLevel + 1; - const index = i.toString(); - if ('path' in parentElement) { item.path = [...parentElement.path, i]; } else { @@ -33,10 +28,6 @@ export const initPathTrackerProps = ( } item.level = parentElementLevel + 1; - item.dataset.level = (parentElementLevel + 1).toString(); - item.dataset.index = i.toString(); - item.dataset.last = i === numChildren - 1 ? 'true' : 'false'; - item.dataset.id = `${level}_${index}`; item.dataset.path = item.path.join('.'); }); }; From 0bba2f928123e3808cc9ad0884b15b16bc8caa09 Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 17 Jul 2025 00:23:34 +0200 Subject: [PATCH 07/11] Add custom styles demo --- dev/vscode-tree/custom-styles.html | 103 +++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 dev/vscode-tree/custom-styles.html diff --git a/dev/vscode-tree/custom-styles.html b/dev/vscode-tree/custom-styles.html new file mode 100644 index 000000000..e2d8008bc --- /dev/null +++ b/dev/vscode-tree/custom-styles.html @@ -0,0 +1,103 @@ + + + + + + VSCode Elements + + + + + + + + +

Custom styles

+
+ + + Home + + About + Overview + Administration + + Facts + History + Current Statistics + Awards + + + Campus Tours + For Prospective Students + For Alumni + For Visitors + + + + Admissions + Apply + + Tuition + Undergraduate + Graduate + Professional Schools + + Sign Up + Visit + Photo Tour + Connect + + + Academics + Colleges & Schools + Programs of Study + Honors Programs + Online Courses + Course Explorer + Register for Classes + Academic Calendar + Transcripts + + + +
+ + From 13471a0f31e07094817814fe6dd02d49afa1e613 Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 17 Jul 2025 00:23:48 +0200 Subject: [PATCH 08/11] Add custom state --- src/vscode-tree-item/vscode-tree-item.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vscode-tree-item/vscode-tree-item.ts b/src/vscode-tree-item/vscode-tree-item.ts index 5dae5b773..9b20439ca 100644 --- a/src/vscode-tree-item/vscode-tree-item.ts +++ b/src/vscode-tree-item/vscode-tree-item.ts @@ -98,6 +98,7 @@ export class VscodeTreeItem extends VscElement { //#region private variables private _path: number[] = []; + private _internals: ElementInternals; @state() private _hasBranchIcon = false; @@ -138,6 +139,7 @@ export class VscodeTreeItem extends VscElement { constructor() { super(); + this._internals = this.attachInternals(); this.addEventListener('focus', this._handleComponentFocus); } @@ -194,6 +196,7 @@ export class VscodeTreeItem extends VscElement { this._treeContextState.activeItem = this; this._setHasActiveItemFlagOnParent(this, true); this.tabIndex = 0; + this._internals.states.add('active'); } else { if (this._treeContextState.activeItem === this) { this._treeContextState.activeItem = null; @@ -201,6 +204,7 @@ export class VscodeTreeItem extends VscElement { } this.tabIndex = -1; + this._internals.states.delete('active'); } } From 52b83e5c60f0cbf329766cf8bfcba05ce056c18c Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 17 Jul 2025 21:52:44 +0200 Subject: [PATCH 09/11] Add missing #endregion --- src/vscode-tree/vscode-tree.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vscode-tree/vscode-tree.ts b/src/vscode-tree/vscode-tree.ts index 5121738fd..7e99b692b 100644 --- a/src/vscode-tree/vscode-tree.ts +++ b/src/vscode-tree/vscode-tree.ts @@ -181,6 +181,8 @@ export class VscodeTree extends VscElement { @queryAssignedElements({selector: 'vscode-tree-item'}) private _assignedTreeItems!: VscodeTreeItem[]; + //#endregion + //#region lifecycle methods constructor() { From fc102bb017426fc364f8468881ebdf487e5e3d37 Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 19 Jul 2025 12:37:50 +0200 Subject: [PATCH 10/11] Rename --- src/vscode-tree/helpers.ts | 2 +- src/vscode-tree/vscode-tree.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vscode-tree/helpers.ts b/src/vscode-tree/helpers.ts index 91b08732b..59282eb60 100644 --- a/src/vscode-tree/helpers.ts +++ b/src/vscode-tree/helpers.ts @@ -142,7 +142,7 @@ export const findPrevItem = (item: VscodeTreeItem): VscodeTreeItem | null => { return prevSibling; }; -export function getParentItem(childItem: VscodeTreeItem) { +export function findParentItem(childItem: VscodeTreeItem) { if (!childItem.parentElement) { return null; } diff --git a/src/vscode-tree/vscode-tree.ts b/src/vscode-tree/vscode-tree.ts index 7e99b692b..a26efb741 100644 --- a/src/vscode-tree/vscode-tree.ts +++ b/src/vscode-tree/vscode-tree.ts @@ -17,7 +17,7 @@ import { import { findNextItem, findPrevItem, - getParentItem, + findParentItem, initPathTrackerProps, } from './helpers.js'; @@ -271,7 +271,7 @@ export class VscodeTree extends VscElement { activeItem.highlightedGuides = true; this._treeContextState.highlightedItems.push(activeItem); } else { - const parent = getParentItem(activeItem); + const parent = findParentItem(activeItem); if (parent && parent.branch) { parent.highlightedGuides = true; @@ -286,7 +286,7 @@ export class VscodeTree extends VscElement { item.highlightedGuides = true; this._treeContextState.highlightedItems.push(item); } else { - const parent = getParentItem(item); + const parent = findParentItem(item); if (parent && parent.branch) { parent.highlightedGuides = true; @@ -390,7 +390,7 @@ export class VscodeTree extends VscElement { } const {focusedItem} = this._treeContextState; - const parent = getParentItem(focusedItem); + const parent = findParentItem(focusedItem); if (!focusedItem.branch) { if (parent && parent.branch) { From d48dfcbd9635c4bafb7ab1d32e8d023d87d2167c Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 19 Jul 2025 17:27:59 +0200 Subject: [PATCH 11/11] Prettier fix --- dev/vscode-tree/basic-example.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev/vscode-tree/basic-example.html b/dev/vscode-tree/basic-example.html index ba3dc4f65..18ad830ec 100644 --- a/dev/vscode-tree/basic-example.html +++ b/dev/vscode-tree/basic-example.html @@ -53,7 +53,9 @@

Basic example

Tuition Undergraduate Graduate - Professional Schools + Professional Schools Sign Up Visit