Skip to content

Commit 37d08e3

Browse files
committed
Add before/after slots to Button
1 parent 12b6c47 commit 37d08e3

File tree

6 files changed

+134
-6
lines changed

6 files changed

+134
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
99
### Added
1010

1111
- Added `base` css part to **Button**.
12+
- Added `content-before` and `content-after` slot to **Button**.
1213

1314
## [2.1.0] - 2025-08-08
1415

dev/vscode-button/slots.html

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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>VSCode Elements</title>
7+
<link
8+
rel="stylesheet"
9+
href="/node_modules/@vscode/codicons/dist/codicon.css"
10+
id="vscode-codicon-stylesheet"
11+
>
12+
<script
13+
type="module"
14+
src="/node_modules/@vscode-elements/webview-playground/dist/index.js"
15+
></script>
16+
<script type="module" src="/dist/main.js"></script>
17+
<script>
18+
const logEvents = (selector, eventType) => {
19+
document.querySelector(selector).addEventListener(eventType, (ev) => {
20+
console.log(ev);
21+
});
22+
};
23+
</script>
24+
</head>
25+
26+
<body>
27+
<main>
28+
<vscode-demo>
29+
<vscode-button
30+
><svg
31+
width="16"
32+
height="16"
33+
fill="none"
34+
viewBox="0 0 24 24"
35+
xmlns="http://www.w3.org/2000/svg"
36+
slot="content-after"
37+
>
38+
<path d="M8 6H6v2h2V6Z" fill="currentColor" />
39+
<path
40+
d="M3 5.25A2.25 2.25 0 0 1 5.25 3h3.5A2.25 2.25 0 0 1 11 5.25v3.5A2.25 2.25 0 0 1 8.75 11h-3.5A2.25 2.25 0 0 1 3 8.75v-3.5Zm2.25-.75a.75.75 0 0 0-.75.75v3.5c0 .414.336.75.75.75h3.5a.75.75 0 0 0 .75-.75v-3.5a.75.75 0 0 0-.75-.75h-3.5ZM6 16h2v2H6v-2Z"
41+
fill="currentColor"
42+
/>
43+
<path
44+
d="M3 15.25A2.25 2.25 0 0 1 5.25 13h3.5A2.25 2.25 0 0 1 11 15.25v3.5A2.25 2.25 0 0 1 8.75 21h-3.5A2.25 2.25 0 0 1 3 18.75v-3.5Zm2.25-.75a.75.75 0 0 0-.75.75v3.5c0 .414.336.75.75.75h3.5a.75.75 0 0 0 .75-.75v-3.5a.75.75 0 0 0-.75-.75h-3.5ZM18 6h-2v2h2V6Z"
45+
fill="currentColor"
46+
/>
47+
<path
48+
d="M15.25 3A2.25 2.25 0 0 0 13 5.25v3.5A2.25 2.25 0 0 0 15.25 11h3.5A2.25 2.25 0 0 0 21 8.75v-3.5A2.25 2.25 0 0 0 18.75 3h-3.5Zm-.75 2.25a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-.75.75h-3.5a.75.75 0 0 1-.75-.75v-3.5ZM13 13h2.75v2.75H13V13ZM18.25 15.75h-2.5v2.5H13V21h2.75v-2.75h2.5V21H21v-2.75h-2.75v-2.5ZM18.25 15.75V13H21v2.75h-2.75Z"
49+
fill="currentColor"
50+
/>
51+
</svg>
52+
Button label</vscode-button
53+
>
54+
</vscode-demo>
55+
</main>
56+
</body>
57+
</html>

src/includes/test-helpers.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,23 +120,26 @@ type TagNameToElement<K extends AllTagNames> =
120120

121121
export function $<K extends AllTagNames>(selector: K): TagNameToElement<K>;
122122
export function $<K extends AllTagNames>(
123-
root: Element,
123+
root: Element | ShadowRoot,
124124
selector: K
125125
): TagNameToElement<K>;
126126
export function $<T extends Element = Element>(selector: string): T;
127127
export function $<T extends Element = Element>(
128-
root: Element,
128+
root: Element | ShadowRoot,
129129
selector: string
130130
): T;
131131
export function $<T extends Element = Element>(
132-
arg1: string | Element,
132+
arg1: string | Element | ShadowRoot,
133133
arg2?: string
134134
): T {
135135
let result: Element | null;
136136

137137
if (typeof arg1 === 'string') {
138138
result = document.querySelector(arg1);
139-
} else if (arg1 instanceof Element && typeof arg2 === 'string') {
139+
} else if (
140+
(arg1 instanceof Element || arg1 instanceof ShadowRoot) &&
141+
typeof arg2 === 'string'
142+
) {
140143
result = arg1.querySelector(arg2);
141144
} else {
142145
throw new Error('Invalid arguments passed to $()');

src/vscode-button/vscode-button.styles.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@ const styles: CSSResultGroup = [
157157
height: 100%;
158158
}
159159
160+
.has-content-before slot[name='content-before'] {
161+
margin-right: 4px;
162+
}
163+
164+
.has-content-after slot[name='content-after'] {
165+
margin-left: 4px;
166+
}
167+
160168
.icon,
161169
.icon-after {
162170
color: inherit;

src/vscode-button/vscode-button.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sinon from 'sinon';
22
import {VscodeButton} from './index.js';
33
import {expect, fixture, html} from '@open-wc/testing';
4+
import {$} from '../includes/test-helpers.js';
45

56
describe('vscode-button', () => {
67
it('is defined', () => {
@@ -78,7 +79,9 @@ describe('vscode-button', () => {
7879
`
7980
<div class="base" part="base">
8081
<vscode-icon name="account" class="icon"></vscode-icon>
82+
<slot name="content-before"></slot>
8183
<slot></slot>
84+
<slot name="content-after"></slot>
8285
</div>
8386
`
8487
);
@@ -93,7 +96,9 @@ describe('vscode-button', () => {
9396
`
9497
<div class="base" part="base">
9598
<vscode-icon name="account" spin class="icon"></vscode-icon>
99+
<slot name="content-before"></slot>
96100
<slot></slot>
101+
<slot name="content-after"></slot>
97102
</div>
98103
`
99104
);
@@ -111,7 +116,9 @@ describe('vscode-button', () => {
111116
`
112117
<div class="base" part="base">
113118
<vscode-icon name="account" class="icon" spin-duration="5"></vscode-icon>
119+
<slot name="content-before"></slot>
114120
<slot></slot>
121+
<slot name="content-after"></slot>
115122
</div>
116123
`
117124
);
@@ -125,7 +132,9 @@ describe('vscode-button', () => {
125132
expect(el).shadowDom.to.eq(
126133
`
127134
<div class="base" part="base">
135+
<slot name="content-before"></slot>
128136
<slot></slot>
137+
<slot name="content-after"></slot>
129138
<vscode-icon name="account" class="icon-after"></vscode-icon>
130139
</div>
131140
`
@@ -140,7 +149,9 @@ describe('vscode-button', () => {
140149
expect(el).shadowDom.to.eq(
141150
`
142151
<div class="base" part="base">
152+
<slot name="content-before"></slot>
143153
<slot></slot>
154+
<slot name="content-after"></slot>
144155
<vscode-icon name="account" class="icon-after" spin></vscode-icon>
145156
</div>
146157
`
@@ -158,7 +169,9 @@ describe('vscode-button', () => {
158169
expect(el).shadowDom.to.eq(
159170
`
160171
<div class="base" part="base">
172+
<slot name="content-before"></slot>
161173
<slot></slot>
174+
<slot name="content-after"></slot>
162175
<vscode-icon name="account" class="icon-after" spin-duration="5"></vscode-icon>
163176
</div>
164177
`
@@ -199,5 +212,25 @@ describe('vscode-button', () => {
199212
expect(submitSpy.called).to.be.true;
200213
});
201214

215+
it('adds custom class to wrapper if content-before slot is not empty', async () => {
216+
const el = await fixture<VscodeButton>(
217+
html`<vscode-button><b slot="content-before">b</b></vscode-button>`
218+
);
219+
220+
const base = $(el.shadowRoot!, '.base');
221+
222+
expect(base.classList.contains('has-content-before')).to.be.true;
223+
});
224+
225+
it('adds custom class to wrapper if content-after slot is not empty', async () => {
226+
const el = await fixture<VscodeButton>(
227+
html`<vscode-button><b slot="content-after">b</b></vscode-button>`
228+
);
229+
230+
const base = $(el.shadowRoot!, '.base');
231+
232+
expect(base.classList.contains('has-content-after')).to.be.true;
233+
});
234+
202235
it('sets the form value');
203236
});

src/vscode-button/vscode-button.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {html, nothing, PropertyValueMap, TemplateResult} from 'lit';
2-
import {property} from 'lit/decorators.js';
2+
import {property, state} from 'lit/decorators.js';
33
import {classMap} from 'lit/directives/class-map.js';
44
import {customElement, VscElement} from '../includes/VscElement.js';
55
import '../vscode-icon/index.js';
@@ -108,6 +108,12 @@ export class VscodeButton extends VscElement {
108108
private _prevTabindex = 0;
109109
private _internals: ElementInternals;
110110

111+
@state()
112+
private _hasContentBefore = false;
113+
114+
@state()
115+
private _hasContentAfter = false;
116+
111117
get form(): HTMLFormElement | null {
112118
return this._internals.form;
113119
}
@@ -211,12 +217,26 @@ export class VscodeButton extends VscElement {
211217
this.focused = false;
212218
};
213219

220+
private _handleSlotChange(ev: Event) {
221+
const slot = ev.target as HTMLSlotElement;
222+
223+
if (slot.name === 'content-before') {
224+
this._hasContentBefore = slot.assignedElements().length > 0;
225+
}
226+
227+
if (slot.name === 'content-after') {
228+
this._hasContentAfter = slot.assignedElements().length > 0;
229+
}
230+
}
231+
214232
override render(): TemplateResult {
215233
const hasIcon = this.icon !== '';
216234
const hasIconAfter = this.iconAfter !== '';
217235
const baseClasses = {
218236
base: true,
219237
'icon-only': this.iconOnly,
238+
'has-content-before': this._hasContentBefore,
239+
'has-content-after': this._hasContentAfter,
220240
};
221241

222242
const iconElem = hasIcon
@@ -238,10 +258,16 @@ export class VscodeButton extends VscElement {
238258
: nothing;
239259

240260
return html`
241-
<div class=${classMap(baseClasses)} part="base">
261+
<div
262+
class=${classMap(baseClasses)}
263+
part="base"
264+
@slotchange=${this._handleSlotChange}
265+
>
266+
<slot name="content-before"></slot>
242267
${iconElem}
243268
<slot></slot>
244269
${iconAfterElem}
270+
<slot name="content-after"></slot>
245271
</div>
246272
`;
247273
}

0 commit comments

Comments
 (0)