Skip to content

Commit e351211

Browse files
web-padawanclaude
andauthored
feat: add badge number property and shadow parts wrappers (#11151)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bc7010e commit e351211

File tree

15 files changed

+185
-21
lines changed

15 files changed

+185
-21
lines changed

dev/badge.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,13 @@ <h2 class="heading">Icon</h2>
3434
<vaadin-icon slot="prefix" icon="vaadin:check"></vaadin-icon>
3535
</vaadin-badge>
3636
</section>
37+
38+
<section class="section">
39+
<h2 class="heading">Number</h2>
40+
<vaadin-badge number="3">
41+
<span>new emails</span>
42+
</vaadin-badge>
43+
<vaadin-badge number="3"></vaadin-badge>
44+
</section>
3745
</body>
3846
</html>

packages/badge/src/styles/vaadin-badge-base-styles.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,23 @@ export const badgeStyles = css`
2525
color: var(--vaadin-badge-text-color, var(--vaadin-text-color));
2626
background: var(--vaadin-badge-background, var(--vaadin-background-container));
2727
border-radius: var(--vaadin-badge-border-radius, var(--vaadin-radius-l));
28+
min-width: var(--vaadin-badge-min-width, calc(1lh + var(--vaadin-badge-padding, var(--vaadin-padding-xs)) * 2));
2829
flex-shrink: 0;
2930
}
3031
3132
:host([hidden]) {
3233
display: none !important;
3334
}
3435
35-
:host([has-prefix]:not([has-content])) {
36-
padding: var(--vaadin-padding-xs);
36+
:host(:not([has-prefix])) [part='prefix'],
37+
:host(:not([has-content])) [part='content'],
38+
:host(:not([has-number])) [part='number'] {
39+
display: none;
40+
}
41+
42+
:host([has-prefix]:not([has-content], [has-number])),
43+
:host([has-number]:not([has-content], [has-prefix])) {
44+
padding: var(--vaadin-badge-padding, var(--vaadin-padding-xs));
3745
border-radius: 50%;
3846
}
3947

packages/badge/src/vaadin-badge.d.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,21 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
2222
*
2323
* ### Styling
2424
*
25+
* The following shadow DOM parts are available for styling:
26+
*
27+
* Part name | Description
28+
* -----------|-------------
29+
* `prefix` | The container for the prefix slot
30+
* `number` | The container for the number value
31+
* `content` | The container for the default slot
32+
*
2533
* The following state attributes are available for styling:
2634
*
2735
* Attribute | Description
2836
* ---------------|-------------
2937
* `has-prefix` | Set when the badge has content in the prefix slot
3038
* `has-content` | Set when the badge has content in the default slot
39+
* `has-number` | Set when the badge has a number value
3140
*
3241
* The following custom CSS properties are available for styling:
3342
*
@@ -40,12 +49,18 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
4049
* `--vaadin-badge-font-family` |
4150
* `--vaadin-badge-gap` |
4251
* `--vaadin-badge-line-height` |
52+
* `--vaadin-badge-min-width` |
4353
* `--vaadin-badge-padding` |
4454
* `--vaadin-badge-text-color` |
4555
*
4656
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
4757
*/
48-
declare class Badge extends ElementMixin(ThemableMixin(HTMLElement)) {}
58+
declare class Badge extends ElementMixin(ThemableMixin(HTMLElement)) {
59+
/**
60+
* The number to display in the badge.
61+
*/
62+
number: number | null | undefined;
63+
}
4964

5065
declare global {
5166
interface HTMLElementTagNameMap {

packages/badge/src/vaadin-badge.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,21 @@ import { badgeStyles } from './styles/vaadin-badge-base-styles.js';
2929
*
3030
* ### Styling
3131
*
32+
* The following shadow DOM parts are available for styling:
33+
*
34+
* Part name | Description
35+
* -----------|-------------
36+
* `prefix` | The container for the prefix slot
37+
* `number` | The container for the number value
38+
* `content` | The container for the default slot
39+
*
3240
* The following state attributes are available for styling:
3341
*
3442
* Attribute | Description
3543
* ---------------|-------------
3644
* `has-prefix` | Set when the badge has content in the prefix slot
3745
* `has-content` | Set when the badge has content in the default slot
46+
* `has-number` | Set when the badge has a number value
3847
*
3948
* The following custom CSS properties are available for styling:
4049
*
@@ -47,6 +56,7 @@ import { badgeStyles } from './styles/vaadin-badge-base-styles.js';
4756
* `--vaadin-badge-font-family` |
4857
* `--vaadin-badge-gap` |
4958
* `--vaadin-badge-line-height` |
59+
* `--vaadin-badge-min-width` |
5060
* `--vaadin-badge-padding` |
5161
* `--vaadin-badge-text-color` |
5262
*
@@ -74,9 +84,33 @@ class Badge extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(L
7484
return true;
7585
}
7686

87+
static get properties() {
88+
return {
89+
/**
90+
* The number to display in the badge.
91+
*/
92+
number: {
93+
type: Number,
94+
},
95+
};
96+
}
97+
7798
/** @protected */
7899
render() {
79-
return html`<slot name="prefix"></slot><slot></slot>`;
100+
return html`
101+
<div part="prefix"><slot name="prefix"></slot></div>
102+
<div part="number">${this.number}</div>
103+
<div part="content"><slot></slot></div>
104+
`;
105+
}
106+
107+
/** @protected */
108+
willUpdate(props) {
109+
super.willUpdate(props);
110+
111+
if (props.has('number')) {
112+
this.toggleAttribute('has-number', this.number != null);
113+
}
80114
}
81115

82116
/** @protected */

packages/badge/test/badge.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,40 @@ describe('vaadin-badge', () => {
6565
});
6666
});
6767

68+
describe('has-number attribute', () => {
69+
it('should not set has-number attribute by default', () => {
70+
expect(badge.hasAttribute('has-number')).to.be.false;
71+
});
72+
73+
it('should set has-number attribute when number property is set', async () => {
74+
badge.number = 5;
75+
await nextUpdate(badge);
76+
expect(badge.hasAttribute('has-number')).to.be.true;
77+
});
78+
79+
it('should remove has-number attribute when number is set to null', async () => {
80+
badge.number = 5;
81+
await nextUpdate(badge);
82+
badge.number = null;
83+
await nextUpdate(badge);
84+
expect(badge.hasAttribute('has-number')).to.be.false;
85+
});
86+
87+
it('should remove has-number attribute when number is set to undefined', async () => {
88+
badge.number = 5;
89+
await nextUpdate(badge);
90+
badge.number = undefined;
91+
await nextUpdate(badge);
92+
expect(badge.hasAttribute('has-number')).to.be.false;
93+
});
94+
95+
it('should set has-number attribute when number is 0', async () => {
96+
badge.number = 0;
97+
await nextUpdate(badge);
98+
expect(badge.hasAttribute('has-number')).to.be.true;
99+
});
100+
});
101+
68102
describe('has-prefix attribute', () => {
69103
it('should not set has-prefix attribute by default', () => {
70104
expect(badge.hasAttribute('has-prefix')).to.be.false;

packages/badge/test/dom/__snapshots__/badge.test.snap.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,38 @@ snapshots["vaadin-badge host content"] =
1414
`;
1515
/* end snapshot vaadin-badge host content */
1616

17+
snapshots["vaadin-badge host number"] =
18+
`<vaadin-badge has-number="">
19+
</vaadin-badge>
20+
`;
21+
/* end snapshot vaadin-badge host number */
22+
1723
snapshots["vaadin-badge shadow default"] =
18-
`<slot name="prefix">
19-
</slot>
20-
<slot>
21-
</slot>
24+
`<div part="prefix">
25+
<slot name="prefix">
26+
</slot>
27+
</div>
28+
<div part="number">
29+
</div>
30+
<div part="content">
31+
<slot>
32+
</slot>
33+
</div>
2234
`;
2335
/* end snapshot vaadin-badge shadow default */
2436

37+
snapshots["vaadin-badge shadow number"] =
38+
`<div part="prefix">
39+
<slot name="prefix">
40+
</slot>
41+
</div>
42+
<div part="number">
43+
5
44+
</div>
45+
<div part="content">
46+
<slot>
47+
</slot>
48+
</div>
49+
`;
50+
/* end snapshot vaadin-badge shadow number */
51+

packages/badge/test/dom/badge.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,23 @@ describe('vaadin-badge', () => {
2525
await nextUpdate(badge);
2626
await expect(badge).dom.to.equalSnapshot();
2727
});
28+
29+
it('number', async () => {
30+
badge.number = 5;
31+
await nextUpdate(badge);
32+
await expect(badge).dom.to.equalSnapshot();
33+
});
2834
});
2935

3036
describe('shadow', () => {
3137
it('default', async () => {
3238
await expect(badge).shadowDom.to.equalSnapshot();
3339
});
40+
41+
it('number', async () => {
42+
badge.number = 5;
43+
await nextUpdate(badge);
44+
await expect(badge).shadowDom.to.equalSnapshot();
45+
});
3446
});
3547
});

packages/badge/test/typings/badge.types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ const badge = document.createElement('vaadin-badge');
99
// Mixins
1010
assertType<ElementMixinClass>(badge);
1111
assertType<ThemableMixinClass>(badge);
12+
13+
// Properties
14+
assertType<number | null | undefined>(badge.number);

packages/badge/test/visual/base/badge.test.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { visualDiff } from '@web/test-runner-visual-regression';
33
import '@vaadin/icon';
44
import '@vaadin/icons';
55
import '../../../src/vaadin-badge.js';
6+
import type { Icon } from '@vaadin/icon';
67
import type { Badge } from '../../../src/vaadin-badge.js';
78

89
window.Vaadin ??= {};
@@ -25,20 +26,42 @@ describe('badge', () => {
2526
await visualDiff(div, 'basic');
2627
});
2728

28-
it('icon', async () => {
29-
const icon = document.createElement('vaadin-icon');
30-
icon.setAttribute('slot', 'prefix');
31-
icon.icon = 'vaadin:check';
32-
element.appendChild(icon);
33-
element.append('Completed');
34-
await visualDiff(div, 'icon');
29+
it('number', async () => {
30+
element.number = 5;
31+
await visualDiff(div, 'number');
3532
});
3633

37-
it('icon-only', async () => {
38-
const icon = document.createElement('vaadin-icon');
39-
icon.setAttribute('slot', 'prefix');
40-
icon.icon = 'vaadin:check';
41-
element.appendChild(icon);
42-
await visualDiff(div, 'icon-only');
34+
it('number-content', async () => {
35+
element.number = 3;
36+
element.textContent = 'Messages';
37+
await visualDiff(div, 'number-content');
38+
});
39+
40+
describe('icon', () => {
41+
let icon: Icon;
42+
43+
beforeEach(() => {
44+
icon = document.createElement('vaadin-icon');
45+
icon.setAttribute('slot', 'prefix');
46+
icon.icon = 'vaadin:check';
47+
});
48+
49+
it('icon', async () => {
50+
element.appendChild(icon);
51+
await visualDiff(div, 'icon');
52+
});
53+
54+
it('icon-content', async () => {
55+
element.appendChild(icon);
56+
element.append('Completed');
57+
await visualDiff(div, 'icon-content');
58+
});
59+
60+
it('icon-number-content', async () => {
61+
element.number = 3;
62+
element.appendChild(icon);
63+
element.append('Completed');
64+
await visualDiff(div, 'icon-number-content');
65+
});
4366
});
4467
});
1.34 KB
Loading

0 commit comments

Comments
 (0)