From a8a9758b77ce577cf7b967d87302f1eaf81735be Mon Sep 17 00:00:00 2001 From: Joel Squire Date: Tue, 19 Aug 2025 22:26:46 -0400 Subject: [PATCH 1/6] Add vscode-breadcrumbs and vscode-breadcrumb-item components with styles and tests --- dev/vscode-breadcrumbs.html | 70 +++++++ src/main.ts | 2 + src/vscode-breadcrumb-item/index.ts | 1 + .../vscode-breadcrumb-item.styles.ts | 49 +++++ .../vscode-breadcrumb-item.ts | 39 ++++ src/vscode-breadcrumbs/index.ts | 1 + .../vscode-breadcrumbs.styles.ts | 35 ++++ .../vscode-breadcrumbs.test.ts | 95 +++++++++ src/vscode-breadcrumbs/vscode-breadcrumbs.ts | 196 ++++++++++++++++++ 9 files changed, 488 insertions(+) create mode 100644 dev/vscode-breadcrumbs.html create mode 100644 src/vscode-breadcrumb-item/index.ts create mode 100644 src/vscode-breadcrumb-item/vscode-breadcrumb-item.styles.ts create mode 100644 src/vscode-breadcrumb-item/vscode-breadcrumb-item.ts create mode 100644 src/vscode-breadcrumbs/index.ts create mode 100644 src/vscode-breadcrumbs/vscode-breadcrumbs.styles.ts create mode 100644 src/vscode-breadcrumbs/vscode-breadcrumbs.test.ts create mode 100644 src/vscode-breadcrumbs/vscode-breadcrumbs.ts diff --git a/dev/vscode-breadcrumbs.html b/dev/vscode-breadcrumbs.html new file mode 100644 index 000000000..9a579fb8b --- /dev/null +++ b/dev/vscode-breadcrumbs.html @@ -0,0 +1,70 @@ + + + + + + <vscode-breadcrumbs> Demo + + + + + + + + + +
+
+

Basic example

+
+ + + + + workspace + + + + src + + + + components + + + + vscode-breadcrumbs.ts + + + +
+
+ +
+

Programmatic selection

+
+ + + Root + src + index.ts + + + +
+
+
+ + diff --git a/src/main.ts b/src/main.ts index bfd2955fc..62980cc31 100644 --- a/src/main.ts +++ b/src/main.ts @@ -36,3 +36,5 @@ export {VscodeToolbarButton} from './vscode-toolbar-button/index.js'; export {VscodeToolbarContainer} from './vscode-toolbar-container/index.js'; export {VscodeTree} from './vscode-tree/index.js'; export {VscodeTreeItem} from './vscode-tree-item/index.js'; +export {VscodeBreadcrumbs} from './vscode-breadcrumbs/index.js'; +export {VscodeBreadcrumbItem} from './vscode-breadcrumb-item/index.js'; diff --git a/src/vscode-breadcrumb-item/index.ts b/src/vscode-breadcrumb-item/index.ts new file mode 100644 index 000000000..0b5725e40 --- /dev/null +++ b/src/vscode-breadcrumb-item/index.ts @@ -0,0 +1 @@ +export {VscodeBreadcrumbItem} from './vscode-breadcrumb-item.js'; diff --git a/src/vscode-breadcrumb-item/vscode-breadcrumb-item.styles.ts b/src/vscode-breadcrumb-item/vscode-breadcrumb-item.styles.ts new file mode 100644 index 000000000..aca3dc5f8 --- /dev/null +++ b/src/vscode-breadcrumb-item/vscode-breadcrumb-item.styles.ts @@ -0,0 +1,49 @@ +import {css, CSSResultGroup} from 'lit'; +import defaultStyles from '../includes/default.styles.js'; + +const styles: CSSResultGroup = [ + defaultStyles, + css` + :host { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--vscode-breadcrumb-foreground, inherit); + outline: none; + cursor: default; + } + + :host(:focus) { + color: var( + --vscode-breadcrumb-focusForeground, + var(--vscode-breadcrumb-foreground, inherit) + ); + } + + :host(.selected) { + color: var( + --vscode-breadcrumb-activeSelectionForeground, + var(--vscode-breadcrumb-focusForeground, inherit) + ); + } + + .separator { + user-select: none; + color: var(--vscode-breadcrumb-foreground, inherit); + opacity: 0.7; + } + + :host(:first-child) .separator { + display: none; + } + + .icon::slotted(*) { + display: inline-flex; + align-items: center; + font-size: 12px; + line-height: 1; + } + `, +]; + +export default styles; diff --git a/src/vscode-breadcrumb-item/vscode-breadcrumb-item.ts b/src/vscode-breadcrumb-item/vscode-breadcrumb-item.ts new file mode 100644 index 000000000..9dfbd72f9 --- /dev/null +++ b/src/vscode-breadcrumb-item/vscode-breadcrumb-item.ts @@ -0,0 +1,39 @@ +import {TemplateResult, html} from 'lit'; +import {customElement, VscElement} from '../includes/VscElement.js'; +import styles from './vscode-breadcrumb-item.styles.js'; + +/** + * @tag vscode-breadcrumb-item + * + * Slot the label/icon as content. + * + * @cssprop [--vscode-breadcrumb-foreground=inherit] - Breadcrumb text and icon color + * @cssprop [--vscode-breadcrumb-focusForeground=var(--vscode-breadcrumb-foreground, inherit)] - Text color when an item has focus + * @cssprop [--vscode-breadcrumb-activeSelectionForeground=var(--vscode-breadcrumb-focusForeground, inherit)] - Text color for the selected item + */ +@customElement('vscode-breadcrumb-item') +export class VscodeBreadcrumbItem extends VscElement { + static override styles = styles; + + override connectedCallback(): void { + super.connectedCallback(); + if (!this.hasAttribute('tabindex')) { + this.tabIndex = -1; + } + this.setAttribute('role', 'listitem'); + } + + override render(): TemplateResult { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'vscode-breadcrumb-item': VscodeBreadcrumbItem; + } +} diff --git a/src/vscode-breadcrumbs/index.ts b/src/vscode-breadcrumbs/index.ts new file mode 100644 index 000000000..e7ae284f3 --- /dev/null +++ b/src/vscode-breadcrumbs/index.ts @@ -0,0 +1 @@ +export {VscodeBreadcrumbs} from './vscode-breadcrumbs.js'; diff --git a/src/vscode-breadcrumbs/vscode-breadcrumbs.styles.ts b/src/vscode-breadcrumbs/vscode-breadcrumbs.styles.ts new file mode 100644 index 000000000..6459f9682 --- /dev/null +++ b/src/vscode-breadcrumbs/vscode-breadcrumbs.styles.ts @@ -0,0 +1,35 @@ +import {css, CSSResultGroup} from 'lit'; +import defaultStyles from '../includes/default.styles.js'; + +const styles: CSSResultGroup = [ + defaultStyles, + css` + :host { + display: block; + width: 100%; + outline: none; + } + + .container { + position: relative; + display: flex; + align-items: center; + gap: 6px; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE 10+ */ + background: var(--vscode-breadcrumb-background, transparent); + } + + .container::-webkit-scrollbar { + display: none; /* Chrome/Safari */ + } + + ::slotted(vscode-breadcrumb-item) { + flex: 0 0 auto; + } + `, +]; + +export default styles; diff --git a/src/vscode-breadcrumbs/vscode-breadcrumbs.test.ts b/src/vscode-breadcrumbs/vscode-breadcrumbs.test.ts new file mode 100644 index 000000000..e596cb86b --- /dev/null +++ b/src/vscode-breadcrumbs/vscode-breadcrumbs.test.ts @@ -0,0 +1,95 @@ +import {expect, fixture, html} from '@open-wc/testing'; +import './vscode-breadcrumbs.js'; +import '../vscode-breadcrumb-item/vscode-breadcrumb-item.js'; +import {VscodeBreadcrumbs} from './vscode-breadcrumbs.js'; + +describe('vscode-breadcrumbs', () => { + it('is defined', () => { + const el = document.createElement('vscode-breadcrumbs'); + expect(el).to.be.instanceOf(VscodeBreadcrumbs); + }); + + it('focuses and selects items on click', async () => { + const el = (await fixture(html` + + Root + src + index.ts + + `)) as VscodeBreadcrumbs; + + const items = el.querySelectorAll('vscode-breadcrumb-item'); + + const selectPromise = new Promise((resolve) => { + const handler = (e: Event) => { + el.removeEventListener('vsc-select', handler); + resolve(e as CustomEvent); + }; + el.addEventListener('vsc-select', handler, {once: true}); + }); + (items[1] as HTMLElement).click(); + const ev = await selectPromise; + + expect((items[1] as HTMLElement).classList.contains('selected')).to.be.true; + expect(ev.detail.index).to.equal(1); + }); + + it('supports keyboard navigation and selection', async () => { + const el = (await fixture(html` + + Root + src + index.ts + + `)) as VscodeBreadcrumbs; + + // Focus the last item by default + const items = el.querySelectorAll('vscode-breadcrumb-item'); + await el.updateComplete; + + // Move left, then select + el.dispatchEvent( + new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true}) + ); + el.dispatchEvent( + new KeyboardEvent('keydown', {key: 'Enter', bubbles: true}) + ); + + await el.updateComplete; + + expect((items[1] as HTMLElement).classList.contains('selected')).to.be.true; + }); + + it('sets aria-current on the last item and aria-label on the host by default', async () => { + const el = (await fixture(html` + + Root + src + index.ts + + `)) as VscodeBreadcrumbs; + + const items = el.querySelectorAll('vscode-breadcrumb-item'); + await el.updateComplete; + + // Host should have an aria-label + expect(el.getAttribute('aria-label')).to.equal('Breadcrumb'); + + // Last item should have aria-current="page" + expect((items[2] as HTMLElement).getAttribute('aria-current')).to.equal('page'); + // Other items should not have aria-current + expect((items[0] as HTMLElement).hasAttribute('aria-current')).to.be.false; + }); + + it('does not override an existing aria-label on the host', async () => { + const el = (await fixture(html` + + Root + src + + `)) as VscodeBreadcrumbs; + + await el.updateComplete; + expect(el.getAttribute('aria-label')).to.equal('Custom label'); + }); +}); diff --git a/src/vscode-breadcrumbs/vscode-breadcrumbs.ts b/src/vscode-breadcrumbs/vscode-breadcrumbs.ts new file mode 100644 index 000000000..830826e9c --- /dev/null +++ b/src/vscode-breadcrumbs/vscode-breadcrumbs.ts @@ -0,0 +1,196 @@ +import {TemplateResult, html} from 'lit'; +import {customElement, VscElement} from '../includes/VscElement.js'; +import {property} from 'lit/decorators.js'; +import styles from './vscode-breadcrumbs.styles.js'; +import '../vscode-breadcrumb-item/vscode-breadcrumb-item.js'; + +export type VscodeBreadcrumbsEventDetail = { + index: number; + item: HTMLElement; + payload?: Event; +}; + +export type VscodeBreadcrumbsFocusEvent = + CustomEvent; +export type VscodeBreadcrumbsSelectEvent = + CustomEvent; + +/** + * @tag vscode-breadcrumbs + * + * Container for breadcrumb items. + * + * @fires vsc-focus Fired when a breadcrumb item receives focus. Detail: { index, item, payload } + * @fires vsc-select Fired when a breadcrumb item is selected. Detail: { index, item, payload } + * + * @cssprop [--vscode-breadcrumb-background=transparent] - Breadcrumbs container background + * + * @prop {number} selectedIndex - Index of the selected breadcrumb item (-1 for none) + * @attr {number} selected-index - Index of the selected breadcrumb item (-1 for none) + */ +@customElement('vscode-breadcrumbs') +export class VscodeBreadcrumbs extends VscElement { + static override styles = styles; + + @property({type: Number, reflect: true, attribute: 'selected-index'}) + selectedIndex = -1; + private _focusedIndex = -1; + + override connectedCallback(): void { + super.connectedCallback(); + this.setAttribute('role', 'list'); + if (!this.hasAttribute('aria-label')) { + this.setAttribute('aria-label', 'Breadcrumb'); + } + this.tabIndex = -1; // focus individual items + this.addEventListener('click', this.onClick as EventListener); + this.addEventListener('keydown', this.onKeydown as EventListener); + } + + override disconnectedCallback(): void { + this.removeEventListener('click', this.onClick as EventListener); + this.removeEventListener('keydown', this.onKeydown as EventListener); + super.disconnectedCallback(); + } + + override firstUpdated(): void { + this.refreshItems(); + } + + private getItems(): HTMLElement[] { + return Array.from( + this.querySelectorAll('vscode-breadcrumb-item') + ) as HTMLElement[]; + } + + private refreshItems(_event?: Event): void { + const items = this.getItems(); + // Make one item tabbable if none yet + if (this._focusedIndex < 0 && items.length) { + this._focusedIndex = items.length - 1; // default to last + } + this.applyItemStates(); + } + + private applyItemStates(): void { + const items = this.getItems(); + items.forEach((el, idx) => { + el.setAttribute('tabindex', idx === this._focusedIndex ? '0' : '-1'); + el.classList.toggle('selected', idx === this.selectedIndex); + // Mark the last breadcrumb as the current page for accessibility + if (idx === items.length - 1) { + el.setAttribute('aria-current', 'page'); + } else { + el.removeAttribute('aria-current'); + } + }); + // Ensure focused is scrolled into view + items[this._focusedIndex]?.scrollIntoView({block: 'nearest', inline: 'nearest'}); + } + + private onClick(ev: MouseEvent) { + const items = this.getItems(); + for ( + let el = ev.target as HTMLElement | null; + el && this.contains(el); + el = el.parentElement + ) { + const idx = items.indexOf(el); + if (idx >= 0) { + this.focusItem(idx, ev); + this.selectItem(idx, ev); + break; + } + } + } + + private onKeydown(ev: KeyboardEvent): void { + const items = this.getItems(); + if (!items.length) return; + if (ev.key === 'ArrowLeft') { + ev.preventDefault(); + const next = Math.max(0, this._focusedIndex - 1); + this.focusItem(next, ev); + } else if (ev.key === 'ArrowRight') { + ev.preventDefault(); + const next = Math.min(items.length - 1, this._focusedIndex + 1); + this.focusItem(next, ev); + } else if (ev.key === 'Enter' || ev.key === ' ') { + ev.preventDefault(); + this.selectItem(this._focusedIndex, ev); + } + } + + private focusItem(idx: number, payload?: Event): void { + const items = this.getItems(); + if (idx < 0 || idx >= items.length) return; + this._focusedIndex = idx; + this.applyItemStates(); + items[idx].focus(); + this.dispatchEvent( + this.createFocusEvent({index: idx, item: items[idx], payload}) + ); + } + + private selectItem(idx: number, payload?: Event): void { + const items = this.getItems(); + if (idx < 0 || idx >= items.length) return; + this.selectedIndex = idx; + this.applyItemStates(); + this.dispatchEvent( + this.createSelectEvent({index: idx, item: items[idx], payload}) + ); + } + + override render(): TemplateResult { + return html`
+ +
`; + } + + override attributeChangedCallback( + name: string, + old: string | null, + value: string | null + ): void { + super.attributeChangedCallback(name, old, value); + if (name === 'selected-index') { + // When selected-index changes via attribute/property, update item states + this.applyItemStates(); + } + } + + protected createFocusEvent( + detail: VscodeBreadcrumbsEventDetail + ): VscodeBreadcrumbsFocusEvent { + return new CustomEvent('vsc-focus', { + detail, + bubbles: true, + composed: true, + }); + } + + protected createSelectEvent( + detail: VscodeBreadcrumbsEventDetail + ): VscodeBreadcrumbsSelectEvent { + return new CustomEvent('vsc-select', { + detail, + bubbles: true, + composed: true, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'vscode-breadcrumbs': VscodeBreadcrumbs; + } + + interface GlobalEventHandlersEventMap { + 'vsc-focus': VscodeBreadcrumbsFocusEvent; + 'vsc-select': VscodeBreadcrumbsSelectEvent; + } +} From 12a56e8e79e709ddd5f83ff0fc761bacb6e1c3ac Mon Sep 17 00:00:00 2001 From: Joel Squire Date: Tue, 19 Aug 2025 22:42:53 -0400 Subject: [PATCH 2/6] Prettier --- dev/vscode-breadcrumbs.html | 5 ++++- src/vscode-breadcrumbs/vscode-breadcrumbs.test.ts | 4 +++- src/vscode-breadcrumbs/vscode-breadcrumbs.ts | 10 +++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/dev/vscode-breadcrumbs.html b/dev/vscode-breadcrumbs.html index 9a579fb8b..a3bbbf8d0 100644 --- a/dev/vscode-breadcrumbs.html +++ b/dev/vscode-breadcrumbs.html @@ -12,7 +12,10 @@ > - + diff --git a/src/vscode-breadcrumbs/vscode-breadcrumbs.test.ts b/src/vscode-breadcrumbs/vscode-breadcrumbs.test.ts index e596cb86b..a55f32aef 100644 --- a/src/vscode-breadcrumbs/vscode-breadcrumbs.test.ts +++ b/src/vscode-breadcrumbs/vscode-breadcrumbs.test.ts @@ -76,7 +76,9 @@ describe('vscode-breadcrumbs', () => { expect(el.getAttribute('aria-label')).to.equal('Breadcrumb'); // Last item should have aria-current="page" - expect((items[2] as HTMLElement).getAttribute('aria-current')).to.equal('page'); + expect((items[2] as HTMLElement).getAttribute('aria-current')).to.equal( + 'page' + ); // Other items should not have aria-current expect((items[0] as HTMLElement).hasAttribute('aria-current')).to.be.false; }); diff --git a/src/vscode-breadcrumbs/vscode-breadcrumbs.ts b/src/vscode-breadcrumbs/vscode-breadcrumbs.ts index 830826e9c..f7fa34e82 100644 --- a/src/vscode-breadcrumbs/vscode-breadcrumbs.ts +++ b/src/vscode-breadcrumbs/vscode-breadcrumbs.ts @@ -85,7 +85,10 @@ export class VscodeBreadcrumbs extends VscElement { } }); // Ensure focused is scrolled into view - items[this._focusedIndex]?.scrollIntoView({block: 'nearest', inline: 'nearest'}); + items[this._focusedIndex]?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }); } private onClick(ev: MouseEvent) { @@ -143,10 +146,7 @@ export class VscodeBreadcrumbs extends VscElement { } override render(): TemplateResult { - return html`
+ return html`
`; } From e75e8c7547b900173bdc77883546e6bb3af4fa3f Mon Sep 17 00:00:00 2001 From: bendera Date: Fri, 22 Aug 2025 14:55:15 +0200 Subject: [PATCH 3/6] Refactor usage of built-in icons --- .../template-elements.ts => icons.ts} | 18 ++++++++++++++++-- .../vscode-select/vscode-select-base.ts | 2 +- src/vscode-checkbox/vscode-checkbox.ts | 17 ++--------------- .../vscode-collapsible.styles.ts | 12 ++++++++++-- src/vscode-collapsible/vscode-collapsible.ts | 18 ++---------------- src/vscode-multi-select/vscode-multi-select.ts | 2 +- .../vscode-single-select.ts | 2 +- src/vscode-tree-item/vscode-tree-item.ts | 16 ++-------------- 8 files changed, 35 insertions(+), 52 deletions(-) rename src/includes/{vscode-select/template-elements.ts => icons.ts} (64%) diff --git a/src/includes/vscode-select/template-elements.ts b/src/includes/icons.ts similarity index 64% rename from src/includes/vscode-select/template-elements.ts rename to src/includes/icons.ts index bbe3525af..a1f6660f1 100644 --- a/src/includes/vscode-select/template-elements.ts +++ b/src/includes/icons.ts @@ -1,6 +1,6 @@ -import {html, svg} from 'lit'; +import {svg} from 'lit'; -export const chevronDownIcon = html` +export const chevronDownIcon = svg` `; +export const chevronRightIcon = svg` + +`; + export const checkIcon = svg` - - `; - const check = this.checked && !this.indeterminate ? icon : nothing; + const check = this.checked && !this.indeterminate ? checkIcon : nothing; const indeterminate = this.indeterminate ? html`` : nothing; diff --git a/src/vscode-collapsible/vscode-collapsible.styles.ts b/src/vscode-collapsible/vscode-collapsible.styles.ts index 82af0f747..8fe257001 100644 --- a/src/vscode-collapsible/vscode-collapsible.styles.ts +++ b/src/vscode-collapsible/vscode-collapsible.styles.ts @@ -47,13 +47,21 @@ const styles: CSSResultGroup = [ } .header-icon { + display: block; + height: 16px; + margin: 0 3px; + width: 16px; + } + + .header-icon svg { color: var(--vscode-icon-foreground, #cccccc); display: block; flex-shrink: 0; - margin: 0 3px; + height: 100%; + width: 100%; } - .collapsible.open .header-icon { + .collapsible.open .header-icon svg { transform: rotate(90deg); } diff --git a/src/vscode-collapsible/vscode-collapsible.ts b/src/vscode-collapsible/vscode-collapsible.ts index 3363f2ce3..5f5ec61ec 100644 --- a/src/vscode-collapsible/vscode-collapsible.ts +++ b/src/vscode-collapsible/vscode-collapsible.ts @@ -1,6 +1,7 @@ import {html, nothing, TemplateResult} from 'lit'; import {property} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; +import {chevronRightIcon} from '../includes/icons.js'; import {customElement, VscElement} from '../includes/VscElement.js'; import styles from './vscode-collapsible.styles.js'; @@ -92,21 +93,6 @@ export class VscodeCollapsible extends VscElement { }; const heading = this.heading ? this.heading : this.title; - const icon = html` - - `; - const descriptionMarkup = this.description ? html`${this.description}` : nothing; @@ -119,7 +105,7 @@ export class VscodeCollapsible extends VscElement { @click=${this._onHeaderClick} @keydown=${this._onHeaderKeyDown} > - ${icon} +
${chevronRightIcon}

${heading}${descriptionMarkup}

diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index 5ac841860..e20379277 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -2,7 +2,7 @@ import {html, LitElement, nothing, TemplateResult} from 'lit'; import {property, query} from 'lit/decorators.js'; import {ifDefined} from 'lit/directives/if-defined.js'; import {customElement} from '../includes/VscElement.js'; -import {chevronDownIcon} from '../includes/vscode-select/template-elements.js'; +import {chevronDownIcon} from '../includes/icons.js'; import {VscodeSelectBase} from '../includes/vscode-select/vscode-select-base.js'; import styles from './vscode-multi-select.styles.js'; import {AssociatedFormControl} from '../includes/AssociatedFormControl.js'; diff --git a/src/vscode-single-select/vscode-single-select.ts b/src/vscode-single-select/vscode-single-select.ts index 39a7f5dac..64b3511ae 100644 --- a/src/vscode-single-select/vscode-single-select.ts +++ b/src/vscode-single-select/vscode-single-select.ts @@ -2,7 +2,7 @@ import {html, LitElement, TemplateResult} from 'lit'; import {property, query} from 'lit/decorators.js'; import {ifDefined} from 'lit/directives/if-defined.js'; import {customElement} from '../includes/VscElement.js'; -import {chevronDownIcon} from '../includes/vscode-select/template-elements.js'; +import {chevronDownIcon} from '../includes/icons.js'; import {VscodeSelectBase} from '../includes/vscode-select/vscode-select-base.js'; import {AssociatedFormControl} from '../includes/AssociatedFormControl.js'; import styles from './vscode-single-select.styles.js'; diff --git a/src/vscode-tree-item/vscode-tree-item.ts b/src/vscode-tree-item/vscode-tree-item.ts index e13892f76..e32dc0cda 100644 --- a/src/vscode-tree-item/vscode-tree-item.ts +++ b/src/vscode-tree-item/vscode-tree-item.ts @@ -4,6 +4,7 @@ import {property, queryAssignedElements, state} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; import {customElement, VscElement} from '../includes/VscElement.js'; import {stylePropertyMap} from '../includes/style-property-map.js'; +import {chevronRightIcon} from '../includes/icons.js'; import { ConfigContext, configContext, @@ -17,19 +18,6 @@ import {ExpandMode, IndentGuides} from '../vscode-tree/vscode-tree.js'; const BASE_INDENT = 3; const ARROW_CONTAINER_WIDTH = 30; -const arrowIcon = html` - -`; - function getParentItem(childItem: VscodeTreeItem) { if (!childItem.parentElement) { return null; @@ -432,7 +420,7 @@ export class VscodeTreeItem extends VscElement { 'icon-rotated': this.open, })} > - ${arrowIcon} + ${chevronRightIcon}
` : nothing}
From 4a60c32c1b268721459b0d028ec161d7c572d3b5 Mon Sep 17 00:00:00 2001 From: bendera Date: Fri, 22 Aug 2025 14:57:29 +0200 Subject: [PATCH 4/6] Rewrite Breadcrumb demo page --- dev/vscode-breadcrumbs.html | 73 ----------------------- dev/vscode-breadcrumbs/basic-example.html | 51 ++++++++++++++++ 2 files changed, 51 insertions(+), 73 deletions(-) delete mode 100644 dev/vscode-breadcrumbs.html create mode 100644 dev/vscode-breadcrumbs/basic-example.html diff --git a/dev/vscode-breadcrumbs.html b/dev/vscode-breadcrumbs.html deleted file mode 100644 index a3bbbf8d0..000000000 --- a/dev/vscode-breadcrumbs.html +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - <vscode-breadcrumbs> Demo - - - - - - - - - -
-
-

Basic example

-
- - - - - workspace - - - - src - - - - components - - - - vscode-breadcrumbs.ts - - - -
-
- -
-

Programmatic selection

-
- - - Root - src - index.ts - - - -
-
-
- - diff --git a/dev/vscode-breadcrumbs/basic-example.html b/dev/vscode-breadcrumbs/basic-example.html new file mode 100644 index 000000000..9651f785e --- /dev/null +++ b/dev/vscode-breadcrumbs/basic-example.html @@ -0,0 +1,51 @@ + + + + + + VSCode Elements + + + + + + + +

Basic example

+
+ + + + + workspace + + + + src + + + + components + + + + vscode-breadcrumbs.ts + + + +
+ + From 1380db9f91443f7079540be653ba8798996cc359 Mon Sep 17 00:00:00 2001 From: bendera Date: Fri, 22 Aug 2025 22:53:41 +0200 Subject: [PATCH 5/6] Polish Breadcrumbs styles --- .../vscode-breadcrumb-item.styles.ts | 52 +++++++++++++------ .../vscode-breadcrumb-item.ts | 31 +++++++++-- .../vscode-breadcrumbs.styles.ts | 10 ++-- 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/vscode-breadcrumb-item/vscode-breadcrumb-item.styles.ts b/src/vscode-breadcrumb-item/vscode-breadcrumb-item.styles.ts index aca3dc5f8..023ba8520 100644 --- a/src/vscode-breadcrumb-item/vscode-breadcrumb-item.styles.ts +++ b/src/vscode-breadcrumb-item/vscode-breadcrumb-item.styles.ts @@ -5,19 +5,17 @@ const styles: CSSResultGroup = [ defaultStyles, css` :host { - display: inline-flex; - align-items: center; - gap: 6px; - color: var(--vscode-breadcrumb-foreground, inherit); - outline: none; - cursor: default; + display: inline-block; } :host(:focus) { - color: var( - --vscode-breadcrumb-focusForeground, - var(--vscode-breadcrumb-foreground, inherit) - ); + outline: none; + } + + :host(:focus) .root { + color: var(--vscode-breadcrumb-focusForeground, #e0e0e0); + outline: 1px solid var(--vscode-focusBorder, #0078d4); + text-decoration: underline; } :host(.selected) { @@ -27,21 +25,45 @@ const styles: CSSResultGroup = [ ); } + .root { + display: flex; + align-items: center; + color: var(--vscode-breadcrumb-foreground, inherit); + outline: none; + cursor: pointer; + } + + :host(:hover) .root { + color: var(--vscode-breadcrumb-focusForeground, #e0e0e0); + } + + .icon { + height: 16px; + width: 16px; + } + + .icon.has-icon { + margin-right: 6px; + } + .separator { user-select: none; color: var(--vscode-breadcrumb-foreground, inherit); + height: 16px; opacity: 0.7; + width: 16px; } :host(:first-child) .separator { display: none; } - .icon::slotted(*) { - display: inline-flex; - align-items: center; - font-size: 12px; - line-height: 1; + :host(:first-child) .root { + margin-left: 16px; + } + + :host(:last-child) .root { + margin-right: 8px; } `, ]; diff --git a/src/vscode-breadcrumb-item/vscode-breadcrumb-item.ts b/src/vscode-breadcrumb-item/vscode-breadcrumb-item.ts index 9dfbd72f9..195956ac7 100644 --- a/src/vscode-breadcrumb-item/vscode-breadcrumb-item.ts +++ b/src/vscode-breadcrumb-item/vscode-breadcrumb-item.ts @@ -1,6 +1,9 @@ import {TemplateResult, html} from 'lit'; import {customElement, VscElement} from '../includes/VscElement.js'; import styles from './vscode-breadcrumb-item.styles.js'; +import {chevronRightIcon} from '../includes/icons.js'; +import {state} from 'lit/decorators.js'; +import {classMap} from 'lit/directives/class-map.js'; /** * @tag vscode-breadcrumb-item @@ -23,12 +26,30 @@ export class VscodeBreadcrumbItem extends VscElement { this.setAttribute('role', 'listitem'); } + @state() + _hasIcon = false; + + private _handleSlotChange(ev: Event) { + const slot = ev.target as HTMLSlotElement; + + this._hasIcon = slot.assignedElements().length > 0; + } + override render(): TemplateResult { - return html` - - `; + return html` +
+ +
+ +
+
+
+ `; } } diff --git a/src/vscode-breadcrumbs/vscode-breadcrumbs.styles.ts b/src/vscode-breadcrumbs/vscode-breadcrumbs.styles.ts index 6459f9682..681eac2a5 100644 --- a/src/vscode-breadcrumbs/vscode-breadcrumbs.styles.ts +++ b/src/vscode-breadcrumbs/vscode-breadcrumbs.styles.ts @@ -11,15 +11,15 @@ const styles: CSSResultGroup = [ } .container { - position: relative; - display: flex; + -ms-overflow-style: none; /* IE 10+ */ align-items: center; - gap: 6px; + background: var(--vscode-breadcrumb-background, transparent); + display: flex; + height: 22px; overflow-x: auto; overflow-y: hidden; + position: relative; scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE 10+ */ - background: var(--vscode-breadcrumb-background, transparent); } .container::-webkit-scrollbar { From f0bb09433d87a246b8149f9ec03b14e247e5b0c8 Mon Sep 17 00:00:00 2001 From: bendera Date: Sun, 24 Aug 2025 23:32:56 +0200 Subject: [PATCH 6/6] Fix test --- src/vscode-checkbox/vscode-checkbox.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode-checkbox/vscode-checkbox.test.ts b/src/vscode-checkbox/vscode-checkbox.test.ts index 7653d5333..c44fb0f27 100644 --- a/src/vscode-checkbox/vscode-checkbox.test.ts +++ b/src/vscode-checkbox/vscode-checkbox.test.ts @@ -188,7 +188,7 @@ describe('vscode-checkbox', () => { const input = el.shadowRoot?.querySelector('input'); expect(input?.checked).to.be.true; - expect(el.shadowRoot?.querySelector('.check-icon')).to.be.ok; + expect(el.shadowRoot?.querySelector('.icon.checked')).to.be.ok; }); it('indeterminate state should be applied', async () => {