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' &&