Skip to content

Commit e397fef

Browse files
jnsquirebendera
authored andcommitted
Add vscode-breadcrumbs and vscode-breadcrumb-item components with styles and tests
1 parent fff13ad commit e397fef

File tree

9 files changed

+488
-0
lines changed

9 files changed

+488
-0
lines changed

dev/vscode-breadcrumbs.html

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>&lt;vscode-breadcrumbs&gt; Demo</title>
7+
<link rel="stylesheet" href="../dev-assets/default-webview-styles.css">
8+
<link
9+
rel="stylesheet"
10+
href="../node_modules/@vscode/codicons/dist/codicon.css"
11+
id="vscode-codicon-stylesheet"
12+
>
13+
<script type="module" src="../dev-assets/component-preview.js"></script>
14+
<script type="module" src="../dist/vscode-breadcrumbs/index.js"></script>
15+
<script type="module" src="../dist/vscode-breadcrumb-item/index.js"></script>
16+
<script type="module" src="../dist/vscode-icon/index.js"></script>
17+
</head>
18+
19+
<body class="vscode-light">
20+
<main>
21+
<div class="story">
22+
<h2 class="story-title">Basic example</h2>
23+
<div class="story-content">
24+
<component-preview>
25+
<vscode-breadcrumbs id="bc-1">
26+
<vscode-breadcrumb-item>
27+
<vscode-icon slot="icon" name="folder"></vscode-icon>
28+
workspace
29+
</vscode-breadcrumb-item>
30+
<vscode-breadcrumb-item>
31+
<vscode-icon slot="icon" name="folder"></vscode-icon>
32+
src
33+
</vscode-breadcrumb-item>
34+
<vscode-breadcrumb-item>
35+
<vscode-icon slot="icon" name="folder"></vscode-icon>
36+
components
37+
</vscode-breadcrumb-item>
38+
<vscode-breadcrumb-item>
39+
<vscode-icon slot="icon" name="file-code"></vscode-icon>
40+
vscode-breadcrumbs.ts
41+
</vscode-breadcrumb-item>
42+
</vscode-breadcrumbs>
43+
</component-preview>
44+
</div>
45+
</div>
46+
47+
<div class="story">
48+
<h2 class="story-title">Programmatic selection</h2>
49+
<div class="story-content">
50+
<component-preview>
51+
<vscode-breadcrumbs id="bc-2">
52+
<vscode-breadcrumb-item>Root</vscode-breadcrumb-item>
53+
<vscode-breadcrumb-item>src</vscode-breadcrumb-item>
54+
<vscode-breadcrumb-item>index.ts</vscode-breadcrumb-item>
55+
</vscode-breadcrumbs>
56+
</component-preview>
57+
<script type="module">
58+
const bc = document.getElementById('bc-2');
59+
bc.addEventListener('vsc-select', (e) =>
60+
console.log('select', e.detail)
61+
);
62+
bc.addEventListener('vsc-focus', (e) =>
63+
console.log('focus', e.detail)
64+
);
65+
</script>
66+
</div>
67+
</div>
68+
</main>
69+
</body>
70+
</html>

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ export {VscodeToolbarButton} from './vscode-toolbar-button/index.js';
3636
export {VscodeToolbarContainer} from './vscode-toolbar-container/index.js';
3737
export {VscodeTree} from './vscode-tree/index.js';
3838
export {VscodeTreeItem} from './vscode-tree-item/index.js';
39+
export {VscodeBreadcrumbs} from './vscode-breadcrumbs/index.js';
40+
export {VscodeBreadcrumbItem} from './vscode-breadcrumb-item/index.js';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {VscodeBreadcrumbItem} from './vscode-breadcrumb-item.js';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {css, CSSResultGroup} from 'lit';
2+
import defaultStyles from '../includes/default.styles.js';
3+
4+
const styles: CSSResultGroup = [
5+
defaultStyles,
6+
css`
7+
:host {
8+
display: inline-flex;
9+
align-items: center;
10+
gap: 6px;
11+
color: var(--vscode-breadcrumb-foreground, inherit);
12+
outline: none;
13+
cursor: default;
14+
}
15+
16+
:host(:focus) {
17+
color: var(
18+
--vscode-breadcrumb-focusForeground,
19+
var(--vscode-breadcrumb-foreground, inherit)
20+
);
21+
}
22+
23+
:host(.selected) {
24+
color: var(
25+
--vscode-breadcrumb-activeSelectionForeground,
26+
var(--vscode-breadcrumb-focusForeground, inherit)
27+
);
28+
}
29+
30+
.separator {
31+
user-select: none;
32+
color: var(--vscode-breadcrumb-foreground, inherit);
33+
opacity: 0.7;
34+
}
35+
36+
:host(:first-child) .separator {
37+
display: none;
38+
}
39+
40+
.icon::slotted(*) {
41+
display: inline-flex;
42+
align-items: center;
43+
font-size: 12px;
44+
line-height: 1;
45+
}
46+
`,
47+
];
48+
49+
export default styles;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {TemplateResult, html} from 'lit';
2+
import {customElement, VscElement} from '../includes/VscElement.js';
3+
import styles from './vscode-breadcrumb-item.styles.js';
4+
5+
/**
6+
* @tag vscode-breadcrumb-item
7+
*
8+
* Slot the label/icon as content.
9+
*
10+
* @cssprop [--vscode-breadcrumb-foreground=inherit] - Breadcrumb text and icon color
11+
* @cssprop [--vscode-breadcrumb-focusForeground=var(--vscode-breadcrumb-foreground, inherit)] - Text color when an item has focus
12+
* @cssprop [--vscode-breadcrumb-activeSelectionForeground=var(--vscode-breadcrumb-focusForeground, inherit)] - Text color for the selected item
13+
*/
14+
@customElement('vscode-breadcrumb-item')
15+
export class VscodeBreadcrumbItem extends VscElement {
16+
static override styles = styles;
17+
18+
override connectedCallback(): void {
19+
super.connectedCallback();
20+
if (!this.hasAttribute('tabindex')) {
21+
this.tabIndex = -1;
22+
}
23+
this.setAttribute('role', 'listitem');
24+
}
25+
26+
override render(): TemplateResult {
27+
return html`<span class="separator" aria-hidden="true" part="separator"
28+
></span
29+
>
30+
<span class="icon" part="icon"><slot name="icon"></slot></span>
31+
<span class="content" part="content"><slot></slot></span>`;
32+
}
33+
}
34+
35+
declare global {
36+
interface HTMLElementTagNameMap {
37+
'vscode-breadcrumb-item': VscodeBreadcrumbItem;
38+
}
39+
}

src/vscode-breadcrumbs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {VscodeBreadcrumbs} from './vscode-breadcrumbs.js';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {css, CSSResultGroup} from 'lit';
2+
import defaultStyles from '../includes/default.styles.js';
3+
4+
const styles: CSSResultGroup = [
5+
defaultStyles,
6+
css`
7+
:host {
8+
display: block;
9+
width: 100%;
10+
outline: none;
11+
}
12+
13+
.container {
14+
position: relative;
15+
display: flex;
16+
align-items: center;
17+
gap: 6px;
18+
overflow-x: auto;
19+
overflow-y: hidden;
20+
scrollbar-width: none; /* Firefox */
21+
-ms-overflow-style: none; /* IE 10+ */
22+
background: var(--vscode-breadcrumb-background, transparent);
23+
}
24+
25+
.container::-webkit-scrollbar {
26+
display: none; /* Chrome/Safari */
27+
}
28+
29+
::slotted(vscode-breadcrumb-item) {
30+
flex: 0 0 auto;
31+
}
32+
`,
33+
];
34+
35+
export default styles;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {expect, fixture, html} from '@open-wc/testing';
2+
import './vscode-breadcrumbs.js';
3+
import '../vscode-breadcrumb-item/vscode-breadcrumb-item.js';
4+
import {VscodeBreadcrumbs} from './vscode-breadcrumbs.js';
5+
6+
describe('vscode-breadcrumbs', () => {
7+
it('is defined', () => {
8+
const el = document.createElement('vscode-breadcrumbs');
9+
expect(el).to.be.instanceOf(VscodeBreadcrumbs);
10+
});
11+
12+
it('focuses and selects items on click', async () => {
13+
const el = (await fixture(html`
14+
<vscode-breadcrumbs>
15+
<vscode-breadcrumb-item>Root</vscode-breadcrumb-item>
16+
<vscode-breadcrumb-item>src</vscode-breadcrumb-item>
17+
<vscode-breadcrumb-item>index.ts</vscode-breadcrumb-item>
18+
</vscode-breadcrumbs>
19+
`)) as VscodeBreadcrumbs;
20+
21+
const items = el.querySelectorAll('vscode-breadcrumb-item');
22+
23+
const selectPromise = new Promise<CustomEvent>((resolve) => {
24+
const handler = (e: Event) => {
25+
el.removeEventListener('vsc-select', handler);
26+
resolve(e as CustomEvent);
27+
};
28+
el.addEventListener('vsc-select', handler, {once: true});
29+
});
30+
(items[1] as HTMLElement).click();
31+
const ev = await selectPromise;
32+
33+
expect((items[1] as HTMLElement).classList.contains('selected')).to.be.true;
34+
expect(ev.detail.index).to.equal(1);
35+
});
36+
37+
it('supports keyboard navigation and selection', async () => {
38+
const el = (await fixture(html`
39+
<vscode-breadcrumbs>
40+
<vscode-breadcrumb-item>Root</vscode-breadcrumb-item>
41+
<vscode-breadcrumb-item>src</vscode-breadcrumb-item>
42+
<vscode-breadcrumb-item>index.ts</vscode-breadcrumb-item>
43+
</vscode-breadcrumbs>
44+
`)) as VscodeBreadcrumbs;
45+
46+
// Focus the last item by default
47+
const items = el.querySelectorAll('vscode-breadcrumb-item');
48+
await el.updateComplete;
49+
50+
// Move left, then select
51+
el.dispatchEvent(
52+
new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})
53+
);
54+
el.dispatchEvent(
55+
new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})
56+
);
57+
58+
await el.updateComplete;
59+
60+
expect((items[1] as HTMLElement).classList.contains('selected')).to.be.true;
61+
});
62+
63+
it('sets aria-current on the last item and aria-label on the host by default', async () => {
64+
const el = (await fixture(html`
65+
<vscode-breadcrumbs>
66+
<vscode-breadcrumb-item>Root</vscode-breadcrumb-item>
67+
<vscode-breadcrumb-item>src</vscode-breadcrumb-item>
68+
<vscode-breadcrumb-item>index.ts</vscode-breadcrumb-item>
69+
</vscode-breadcrumbs>
70+
`)) as VscodeBreadcrumbs;
71+
72+
const items = el.querySelectorAll('vscode-breadcrumb-item');
73+
await el.updateComplete;
74+
75+
// Host should have an aria-label
76+
expect(el.getAttribute('aria-label')).to.equal('Breadcrumb');
77+
78+
// Last item should have aria-current="page"
79+
expect((items[2] as HTMLElement).getAttribute('aria-current')).to.equal('page');
80+
// Other items should not have aria-current
81+
expect((items[0] as HTMLElement).hasAttribute('aria-current')).to.be.false;
82+
});
83+
84+
it('does not override an existing aria-label on the host', async () => {
85+
const el = (await fixture(html`
86+
<vscode-breadcrumbs aria-label="Custom label">
87+
<vscode-breadcrumb-item>Root</vscode-breadcrumb-item>
88+
<vscode-breadcrumb-item>src</vscode-breadcrumb-item>
89+
</vscode-breadcrumbs>
90+
`)) as VscodeBreadcrumbs;
91+
92+
await el.updateComplete;
93+
expect(el.getAttribute('aria-label')).to.equal('Custom label');
94+
});
95+
});

0 commit comments

Comments
 (0)