diff --git a/dev/vscode-tree/basic-example.html b/dev/vscode-tree/basic-example.html
index b153e8515..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
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
+
+
+
+
+
+
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');
}
}
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..59282eb60 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,
@@ -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,25 +28,17 @@ 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('.');
});
};
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 {
@@ -59,7 +46,7 @@ export const findLastChildItem = (item: VscodeTreeItem): VscodeTreeItem => {
}
};
-export const findClosestParentHasNextSibling = (
+export const findClosestAncestorHasNextSibling = (
item: VscodeTreeItem
): VscodeTreeItem | null => {
if (!item.parentElement) {
@@ -70,74 +57,79 @@ 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;
+ let nextItem: VscodeTreeItem | null;
- if ('level' in item) {
- level = item.level;
- }
-
- 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 => {
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;
}
}
@@ -150,50 +142,7 @@ 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) {
+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 0140d33a0..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';
@@ -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
@@ -152,6 +181,8 @@ export class VscodeTree extends VscElement {
@queryAssignedElements({selector: 'vscode-tree-item'})
private _assignedTreeItems!: VscodeTreeItem[];
+ //#endregion
+
//#region lifecycle methods
constructor() {
@@ -240,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;
@@ -255,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;
@@ -359,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) {
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' &&