@vaadin/card 25.3.0-alpha1 → 25.3.0-alpha3
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.
- package/package.json +12 -12
- package/src/title-controller.d.ts +23 -0
- package/src/title-controller.js +139 -0
- package/src/vaadin-card.js +18 -76
- package/web-types.json +1 -1
- package/web-types.lit.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/card",
|
|
3
|
-
"version": "25.3.0-
|
|
3
|
+
"version": "25.3.0-alpha3",
|
|
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.3.0-
|
|
38
|
-
"@vaadin/vaadin-themable-mixin": "25.3.0-
|
|
37
|
+
"@vaadin/component-base": "25.3.0-alpha3",
|
|
38
|
+
"@vaadin/vaadin-themable-mixin": "25.3.0-alpha3",
|
|
39
39
|
"lit": "^3.0.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@vaadin/aura": "25.3.0-
|
|
43
|
-
"@vaadin/avatar": "25.3.0-
|
|
44
|
-
"@vaadin/button": "25.3.0-
|
|
45
|
-
"@vaadin/chai-plugins": "25.3.0-
|
|
46
|
-
"@vaadin/icon": "25.3.0-
|
|
47
|
-
"@vaadin/icons": "25.3.0-
|
|
48
|
-
"@vaadin/test-runner-commands": "25.3.0-
|
|
42
|
+
"@vaadin/aura": "25.3.0-alpha3",
|
|
43
|
+
"@vaadin/avatar": "25.3.0-alpha3",
|
|
44
|
+
"@vaadin/button": "25.3.0-alpha3",
|
|
45
|
+
"@vaadin/chai-plugins": "25.3.0-alpha3",
|
|
46
|
+
"@vaadin/icon": "25.3.0-alpha3",
|
|
47
|
+
"@vaadin/icons": "25.3.0-alpha3",
|
|
48
|
+
"@vaadin/test-runner-commands": "25.3.0-alpha3",
|
|
49
49
|
"@vaadin/testing-helpers": "^2.0.0",
|
|
50
|
-
"@vaadin/vaadin-lumo-styles": "25.3.0-
|
|
50
|
+
"@vaadin/vaadin-lumo-styles": "25.3.0-alpha3",
|
|
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": "
|
|
58
|
+
"gitHead": "ba0c4c55ea219eadd9aefe244f53e87803a066c8"
|
|
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
|
+
}
|
package/src/vaadin-card.js
CHANGED
|
@@ -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.
|
|
@@ -86,7 +86,6 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
|
|
|
86
86
|
*/
|
|
87
87
|
cardTitle: {
|
|
88
88
|
type: String,
|
|
89
|
-
observer: '__cardTitleChanged',
|
|
90
89
|
},
|
|
91
90
|
|
|
92
91
|
/**
|
|
@@ -97,7 +96,6 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
|
|
|
97
96
|
titleHeadingLevel: {
|
|
98
97
|
type: Number,
|
|
99
98
|
reflectToAttribute: true,
|
|
100
|
-
observer: '__titleHeadingLevelChanged',
|
|
101
99
|
},
|
|
102
100
|
};
|
|
103
101
|
}
|
|
@@ -139,9 +137,26 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
|
|
|
139
137
|
/** @protected */
|
|
140
138
|
firstUpdated() {
|
|
141
139
|
super.firstUpdated();
|
|
140
|
+
|
|
141
|
+
this.__titleController = new TitleController(this);
|
|
142
|
+
this.addController(this.__titleController);
|
|
143
|
+
|
|
142
144
|
this._onSlotChange();
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
/** @protected */
|
|
148
|
+
updated(props) {
|
|
149
|
+
super.updated(props);
|
|
150
|
+
|
|
151
|
+
if (props.has('cardTitle')) {
|
|
152
|
+
this.__titleController.setTitle(this.cardTitle);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (props.has('titleHeadingLevel')) {
|
|
156
|
+
this.__titleController.setLevel(this.titleHeadingLevel);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
145
160
|
/** @private */
|
|
146
161
|
_onSlotChange() {
|
|
147
162
|
this.toggleAttribute('_m', this.querySelector(':scope > [slot="media"]'));
|
|
@@ -158,9 +173,6 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
|
|
|
158
173
|
this.toggleAttribute('_hs', this.querySelector(':scope > [slot="header-suffix"]'));
|
|
159
174
|
this.toggleAttribute('_c', this.__hasContent());
|
|
160
175
|
this.toggleAttribute('_f', this.querySelector(':scope > [slot="footer"]'));
|
|
161
|
-
if (this.__getCustomTitleElement()) {
|
|
162
|
-
this.__clearStringTitle();
|
|
163
|
-
}
|
|
164
176
|
}
|
|
165
177
|
|
|
166
178
|
/** @private */
|
|
@@ -169,76 +181,6 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
|
|
|
169
181
|
return slot.assignedNodes({ flatten: true }).filter((node) => !isEmptyTextNode(node)).length > 0;
|
|
170
182
|
}
|
|
171
183
|
|
|
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
184
|
/**
|
|
243
185
|
* @protected
|
|
244
186
|
* @override
|
package/web-types.json
CHANGED