@vaadin/card 25.2.7 → 25.3.0-alpha10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,6 +56,13 @@
56
56
  "description": "The title of the card. When set, any custom slotted title is removed and this string-based title is used instead. If this title is used, an `aria-labelledby` attribute that points to the generated title element is set.",
57
57
  "fieldName": "cardTitle"
58
58
  },
59
+ {
60
+ "type": {
61
+ "text": "string"
62
+ },
63
+ "description": "The theme variants to apply to the component.",
64
+ "name": "theme"
65
+ },
59
66
  {
60
67
  "name": "title-heading-level",
61
68
  "type": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/card",
3
- "version": "25.2.7",
3
+ "version": "25.3.0-alpha10",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -34,20 +34,20 @@
34
34
  "web-component"
35
35
  ],
36
36
  "dependencies": {
37
- "@vaadin/component-base": "~25.2.7",
38
- "@vaadin/vaadin-themable-mixin": "~25.2.7",
37
+ "@vaadin/component-base": "25.3.0-alpha10",
38
+ "@vaadin/vaadin-themable-mixin": "25.3.0-alpha10",
39
39
  "lit": "^3.0.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@vaadin/aura": "~25.2.7",
43
- "@vaadin/avatar": "~25.2.7",
44
- "@vaadin/button": "~25.2.7",
45
- "@vaadin/chai-plugins": "~25.2.7",
46
- "@vaadin/icon": "~25.2.7",
47
- "@vaadin/icons": "~25.2.7",
48
- "@vaadin/test-runner-commands": "~25.2.7",
42
+ "@vaadin/aura": "25.3.0-alpha10",
43
+ "@vaadin/avatar": "25.3.0-alpha10",
44
+ "@vaadin/button": "25.3.0-alpha10",
45
+ "@vaadin/chai-plugins": "25.3.0-alpha10",
46
+ "@vaadin/icon": "25.3.0-alpha10",
47
+ "@vaadin/icons": "25.3.0-alpha10",
48
+ "@vaadin/test-runner-commands": "25.3.0-alpha10",
49
49
  "@vaadin/testing-helpers": "^2.0.0",
50
- "@vaadin/vaadin-lumo-styles": "~25.2.7",
50
+ "@vaadin/vaadin-lumo-styles": "25.3.0-alpha10",
51
51
  "sinon": "^22.0.0"
52
52
  },
53
53
  "customElements": "custom-elements.json",
@@ -55,5 +55,5 @@
55
55
  "web-types.json",
56
56
  "web-types.lit.json"
57
57
  ],
58
- "gitHead": "14be1af674f3968cce68c6eb4742340694c4a949"
58
+ "gitHead": "f2833abdf9b613fa0d0ed216830e3f4de87b7dac"
59
59
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2024 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { SlotChildObserveController } from '@vaadin/component-base/src/slot-child-observe-controller.js';
7
+
8
+ /**
9
+ * A controller to manage the card title element.
10
+ */
11
+ export class TitleController extends SlotChildObserveController {
12
+ constructor(host: HTMLElement);
13
+
14
+ /**
15
+ * Set title based on the corresponding host property.
16
+ */
17
+ setTitle(title: string): void;
18
+
19
+ /**
20
+ * Set heading level based on the corresponding host property.
21
+ */
22
+ setLevel(level: number): void;
23
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2024 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { SlotChildObserveController } from '@vaadin/component-base/src/slot-child-observe-controller.js';
7
+
8
+ /**
9
+ * A controller to manage the card title element.
10
+ */
11
+ export class TitleController extends SlotChildObserveController {
12
+ constructor(host) {
13
+ // Do not provide tag name, as we create the title lazily.
14
+ super(host, 'title', null, { uniqueIdPrefix: 'card-title' });
15
+ }
16
+
17
+ /**
18
+ * Set title based on the corresponding host property.
19
+ *
20
+ * @param {string} title
21
+ */
22
+ setTitle(title) {
23
+ this.title = title;
24
+
25
+ if (title && title.trim() !== '') {
26
+ // The string title replaces a custom slotted title, if any.
27
+ const child = this.getSlotChild();
28
+ if (child && child !== this.defaultNode) {
29
+ child.remove();
30
+ }
31
+
32
+ // Create or restore the default title node.
33
+ this.restoreDefaultNode();
34
+ this.updateDefaultNode(this.node);
35
+ } else if (this.node && this.node === this.defaultNode) {
36
+ // Clearing the string title removes the generated node.
37
+ this.node.remove();
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Set heading level based on the corresponding host property.
43
+ *
44
+ * @param {number} level
45
+ */
46
+ setLevel(level) {
47
+ this.level = level;
48
+
49
+ // When the default title is used, update it.
50
+ if (this.node && this.node === this.defaultNode) {
51
+ this.updateDefaultNode(this.node);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Override method inherited from `SlotController` to set up the default
57
+ * title node as a heading and label the card by it. The heading role and
58
+ * `aria-labelledby` are only applied to the generated string title, never
59
+ * to a custom slotted title.
60
+ *
61
+ * @param {Node} node
62
+ * @protected
63
+ * @override
64
+ */
65
+ initNode(node) {
66
+ if (node && node === this.defaultNode) {
67
+ node.setAttribute('role', 'heading');
68
+ this.host.setAttribute('aria-labelledby', node.id);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Override method inherited from `SlotChildObserveController` to handle a
74
+ * custom title element. Skips the default `super` call so the custom node
75
+ * keeps no generated ID. Clears the string title property, as a custom
76
+ * title takes over.
77
+ *
78
+ * @param {Node} _node
79
+ * @protected
80
+ * @override
81
+ */
82
+ initCustomNode(_node) {
83
+ this.host.cardTitle = '';
84
+ }
85
+
86
+ /**
87
+ * Override method inherited from `SlotChildObserveController` to drop the
88
+ * labelling once the generated title node leaves the slot and is not
89
+ * replaced by a custom title.
90
+ *
91
+ * @param {Node} node
92
+ * @protected
93
+ * @override
94
+ */
95
+ teardownNode(node) {
96
+ if (this.getSlotChild() !== this.defaultNode) {
97
+ this.host.removeAttribute('aria-labelledby');
98
+ }
99
+
100
+ super.teardownNode(node);
101
+ }
102
+
103
+ /**
104
+ * Override method inherited from `SlotChildObserveController`
105
+ * to create the default title element lazily as needed.
106
+ *
107
+ * @protected
108
+ * @override
109
+ */
110
+ restoreDefaultNode() {
111
+ const { title } = this;
112
+
113
+ // No title yet, create one.
114
+ if (title && title.trim() !== '') {
115
+ this.tagName = 'div';
116
+
117
+ const node = this.attachDefaultNode();
118
+ this.initNode(node);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Override method inherited from `SlotChildObserveController`
124
+ * to update the default title element.
125
+ *
126
+ * @param {Node | undefined} node
127
+ * @protected
128
+ * @override
129
+ */
130
+ updateDefaultNode(node) {
131
+ if (node) {
132
+ node.textContent = this.title;
133
+ node.setAttribute('aria-level', this.level || 2);
134
+ }
135
+
136
+ // Notify the host after update.
137
+ super.updateDefaultNode(node);
138
+ }
139
+ }
@@ -8,10 +8,10 @@ import { defineCustomElement } from '@vaadin/component-base/src/define.js';
8
8
  import { isEmptyTextNode } from '@vaadin/component-base/src/dom-utils.js';
9
9
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
10
10
  import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
11
- import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
12
11
  import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
13
12
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
14
13
  import { cardStyles } from './styles/vaadin-card-base-styles.js';
14
+ import { TitleController } from './title-controller.js';
15
15
 
16
16
  /**
17
17
  * `<vaadin-card>` is a versatile container for grouping related content and actions.
@@ -61,6 +61,7 @@ import { cardStyles } from './styles/vaadin-card-base-styles.js';
61
61
  *
62
62
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
63
63
  *
64
+ * @attr {string} theme - The theme variants to apply to the component.
64
65
  * @customElement vaadin-card
65
66
  * @extends HTMLElement
66
67
  */
@@ -86,7 +87,6 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
86
87
  */
87
88
  cardTitle: {
88
89
  type: String,
89
- observer: '__cardTitleChanged',
90
90
  },
91
91
 
92
92
  /**
@@ -97,7 +97,6 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
97
97
  titleHeadingLevel: {
98
98
  type: Number,
99
99
  reflectToAttribute: true,
100
- observer: '__titleHeadingLevelChanged',
101
100
  },
102
101
  };
103
102
  }
@@ -139,9 +138,26 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
139
138
  /** @protected */
140
139
  firstUpdated() {
141
140
  super.firstUpdated();
141
+
142
+ this.__titleController = new TitleController(this);
143
+ this.addController(this.__titleController);
144
+
142
145
  this._onSlotChange();
143
146
  }
144
147
 
148
+ /** @protected */
149
+ updated(props) {
150
+ super.updated(props);
151
+
152
+ if (props.has('cardTitle')) {
153
+ this.__titleController.setTitle(this.cardTitle);
154
+ }
155
+
156
+ if (props.has('titleHeadingLevel')) {
157
+ this.__titleController.setLevel(this.titleHeadingLevel);
158
+ }
159
+ }
160
+
145
161
  /** @private */
146
162
  _onSlotChange() {
147
163
  this.toggleAttribute('_m', this.querySelector(':scope > [slot="media"]'));
@@ -158,9 +174,6 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
158
174
  this.toggleAttribute('_hs', this.querySelector(':scope > [slot="header-suffix"]'));
159
175
  this.toggleAttribute('_c', this.__hasContent());
160
176
  this.toggleAttribute('_f', this.querySelector(':scope > [slot="footer"]'));
161
- if (this.__getCustomTitleElement()) {
162
- this.__clearStringTitle();
163
- }
164
177
  }
165
178
 
166
179
  /** @private */
@@ -169,76 +182,6 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
169
182
  return slot.assignedNodes({ flatten: true }).filter((node) => !isEmptyTextNode(node)).length > 0;
170
183
  }
171
184
 
172
- /** @private */
173
- __clearStringTitle() {
174
- const stringTitleElement = this.__getStringTitleElement();
175
- if (stringTitleElement) {
176
- this.removeChild(stringTitleElement);
177
- }
178
- const ariaLabelledby = this.getAttribute('aria-labelledby');
179
- if (ariaLabelledby?.startsWith('card-title-')) {
180
- this.removeAttribute('aria-labelledby');
181
- }
182
- if (this.cardTitle) {
183
- this.cardTitle = '';
184
- }
185
- }
186
-
187
- /** @private */
188
- __getCustomTitleElement() {
189
- return Array.from(this.querySelectorAll('[slot="title"]')).find((el) => {
190
- return !el.hasAttribute('card-string-title');
191
- });
192
- }
193
-
194
- /** @private */
195
- __cardTitleChanged(title) {
196
- if (!title) {
197
- this.__clearStringTitle();
198
- return;
199
- }
200
- const customTitleElement = this.__getCustomTitleElement();
201
- if (customTitleElement) {
202
- this.removeChild(customTitleElement);
203
- }
204
- let stringTitleElement = this.__getStringTitleElement();
205
- if (!stringTitleElement) {
206
- stringTitleElement = this.__createStringTitleElement();
207
- this.appendChild(stringTitleElement);
208
- this.setAttribute('aria-labelledby', stringTitleElement.id);
209
- }
210
- stringTitleElement.textContent = title;
211
- }
212
-
213
- /** @private */
214
- __createStringTitleElement() {
215
- const stringTitleElement = document.createElement('div');
216
- stringTitleElement.setAttribute('slot', 'title');
217
- stringTitleElement.setAttribute('role', 'heading');
218
- this.__setTitleHeadingLevel(stringTitleElement, this.titleHeadingLevel);
219
- stringTitleElement.setAttribute('card-string-title', '');
220
- stringTitleElement.id = `card-title-${generateUniqueId()}`;
221
- return stringTitleElement;
222
- }
223
-
224
- /** @private */
225
- __titleHeadingLevelChanged(titleHeadingLevel) {
226
- const stringTitleElement = this.__getStringTitleElement();
227
- if (stringTitleElement) {
228
- this.__setTitleHeadingLevel(stringTitleElement, titleHeadingLevel);
229
- }
230
- }
231
-
232
- /** @private */
233
- __setTitleHeadingLevel(stringTitleElement, titleHeadingLevel) {
234
- stringTitleElement.setAttribute('aria-level', titleHeadingLevel || 2);
235
- }
236
-
237
- /** @private */
238
- __getStringTitleElement() {
239
- return this.querySelector('[slot="title"][card-string-title]');
240
- }
241
-
242
185
  /**
243
186
  * @protected
244
187
  * @override
package/web-types.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/card",
4
- "version": "25.2.7",
4
+ "version": "25.3.0-alpha10",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
@@ -24,9 +24,7 @@
24
24
  "description": "The theme variants to apply to the component.",
25
25
  "value": {
26
26
  "type": [
27
- "string",
28
- "null",
29
- "undefined"
27
+ "string"
30
28
  ]
31
29
  }
32
30
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/card",
4
- "version": "25.2.7",
4
+ "version": "25.3.0-alpha10",
5
5
  "description-markup": "markdown",
6
6
  "framework": "lit",
7
7
  "framework-config": {