Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions dev/vscode-breadcrumbs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>&lt;vscode-breadcrumbs&gt; Demo</title>
<link rel="stylesheet" href="../dev-assets/default-webview-styles.css">
<link
rel="stylesheet"
href="../node_modules/@vscode/codicons/dist/codicon.css"
id="vscode-codicon-stylesheet"
>
<script type="module" src="../dev-assets/component-preview.js"></script>
<script type="module" src="../dist/vscode-breadcrumbs/index.js"></script>
<script
type="module"
src="../dist/vscode-breadcrumb-item/index.js"
></script>
<script type="module" src="../dist/vscode-icon/index.js"></script>
</head>

<body class="vscode-light">
<main>
<div class="story">
<h2 class="story-title">Basic example</h2>
<div class="story-content">
<component-preview>
<vscode-breadcrumbs id="bc-1">
<vscode-breadcrumb-item>
<vscode-icon slot="icon" name="folder"></vscode-icon>
workspace
</vscode-breadcrumb-item>
<vscode-breadcrumb-item>
<vscode-icon slot="icon" name="folder"></vscode-icon>
src
</vscode-breadcrumb-item>
<vscode-breadcrumb-item>
<vscode-icon slot="icon" name="folder"></vscode-icon>
components
</vscode-breadcrumb-item>
<vscode-breadcrumb-item>
<vscode-icon slot="icon" name="file-code"></vscode-icon>
vscode-breadcrumbs.ts
</vscode-breadcrumb-item>
</vscode-breadcrumbs>
</component-preview>
</div>
</div>

<div class="story">
<h2 class="story-title">Programmatic selection</h2>
<div class="story-content">
<component-preview>
<vscode-breadcrumbs id="bc-2">
<vscode-breadcrumb-item>Root</vscode-breadcrumb-item>
<vscode-breadcrumb-item>src</vscode-breadcrumb-item>
<vscode-breadcrumb-item>index.ts</vscode-breadcrumb-item>
</vscode-breadcrumbs>
</component-preview>
<script type="module">
const bc = document.getElementById('bc-2');
bc.addEventListener('vsc-select', (e) =>
console.log('select', e.detail)
);
bc.addEventListener('vsc-focus', (e) =>
console.log('focus', e.detail)
);
</script>
</div>
</div>
</main>
</body>
</html>
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,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';
1 change: 1 addition & 0 deletions src/vscode-breadcrumb-item/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {VscodeBreadcrumbItem} from './vscode-breadcrumb-item.js';
49 changes: 49 additions & 0 deletions src/vscode-breadcrumb-item/vscode-breadcrumb-item.styles.ts
Original file line number Diff line number Diff line change
@@ -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;
39 changes: 39 additions & 0 deletions src/vscode-breadcrumb-item/vscode-breadcrumb-item.ts
Original file line number Diff line number Diff line change
@@ -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`<span class="separator" aria-hidden="true" part="separator"
>›</span
>
<span class="icon" part="icon"><slot name="icon"></slot></span>
<span class="content" part="content"><slot></slot></span>`;
}
}

declare global {
interface HTMLElementTagNameMap {
'vscode-breadcrumb-item': VscodeBreadcrumbItem;
}
}
1 change: 1 addition & 0 deletions src/vscode-breadcrumbs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {VscodeBreadcrumbs} from './vscode-breadcrumbs.js';
35 changes: 35 additions & 0 deletions src/vscode-breadcrumbs/vscode-breadcrumbs.styles.ts
Original file line number Diff line number Diff line change
@@ -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;
97 changes: 97 additions & 0 deletions src/vscode-breadcrumbs/vscode-breadcrumbs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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`
<vscode-breadcrumbs>
<vscode-breadcrumb-item>Root</vscode-breadcrumb-item>
<vscode-breadcrumb-item>src</vscode-breadcrumb-item>
<vscode-breadcrumb-item>index.ts</vscode-breadcrumb-item>
</vscode-breadcrumbs>
`)) as VscodeBreadcrumbs;

const items = el.querySelectorAll('vscode-breadcrumb-item');

const selectPromise = new Promise<CustomEvent>((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`
<vscode-breadcrumbs>
<vscode-breadcrumb-item>Root</vscode-breadcrumb-item>
<vscode-breadcrumb-item>src</vscode-breadcrumb-item>
<vscode-breadcrumb-item>index.ts</vscode-breadcrumb-item>
</vscode-breadcrumbs>
`)) 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`
<vscode-breadcrumbs>
<vscode-breadcrumb-item>Root</vscode-breadcrumb-item>
<vscode-breadcrumb-item>src</vscode-breadcrumb-item>
<vscode-breadcrumb-item>index.ts</vscode-breadcrumb-item>
</vscode-breadcrumbs>
`)) 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`
<vscode-breadcrumbs aria-label="Custom label">
<vscode-breadcrumb-item>Root</vscode-breadcrumb-item>
<vscode-breadcrumb-item>src</vscode-breadcrumb-item>
</vscode-breadcrumbs>
`)) as VscodeBreadcrumbs;

await el.updateComplete;
expect(el.getAttribute('aria-label')).to.equal('Custom label');
});
});
Loading