@vaadin/component-base 25.3.0-alpha7 → 25.3.0-alpha9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/component-base",
3
- "version": "25.3.0-alpha7",
3
+ "version": "25.3.0-alpha9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -38,11 +38,11 @@
38
38
  "lit": "^3.0.0"
39
39
  },
40
40
  "devDependencies": {
41
- "@vaadin/chai-plugins": "25.3.0-alpha7",
42
- "@vaadin/test-runner-commands": "25.3.0-alpha7",
41
+ "@vaadin/chai-plugins": "25.3.0-alpha9",
42
+ "@vaadin/test-runner-commands": "25.3.0-alpha9",
43
43
  "@vaadin/testing-helpers": "^2.0.0",
44
44
  "sinon": "^22.0.0"
45
45
  },
46
46
  "customElements": "custom-elements.json",
47
- "gitHead": "ae7b9823df5598faebd7a482993029ee489c35ae"
47
+ "gitHead": "cb915ebde095ec5b94a87af93dd4530f51984c52"
48
48
  }
package/src/define.js CHANGED
@@ -13,7 +13,7 @@ function dashToCamelCase(dash) {
13
13
 
14
14
  const experimentalMap = {};
15
15
 
16
- export function defineCustomElement(CustomElement, version = '25.3.0-alpha7') {
16
+ export function defineCustomElement(CustomElement, version = '25.3.0-alpha9') {
17
17
  Object.defineProperty(CustomElement, 'version', {
18
18
  get() {
19
19
  return version;
@@ -4,6 +4,7 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { dedupeMixin } from '@open-wc/dedupe-mixin';
7
+ import { setOrRemoveAttribute } from './dom-utils.js';
7
8
 
8
9
  /**
9
10
  * A mixin to delegate properties and attributes to a target element.
@@ -103,10 +104,8 @@ const DelegateStateMixinImplementation = (superclass) => {
103
104
 
104
105
  if (typeof value === 'boolean') {
105
106
  this.stateTarget.toggleAttribute(name, value);
106
- } else if (value) {
107
- this.stateTarget.setAttribute(name, value);
108
107
  } else {
109
- this.stateTarget.removeAttribute(name);
108
+ setOrRemoveAttribute(this.stateTarget, name, value);
110
109
  }
111
110
  }
112
111
 
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2026 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import type { DirectiveResult } from 'lit/directive.js';
7
+
8
+ /**
9
+ * A key-value set of part names to truthy values.
10
+ */
11
+ export interface PartNameInfo {
12
+ readonly [name: string]: string | boolean | number | null | undefined;
13
+ }
14
+
15
+ /**
16
+ * A directive that applies dynamic shadow DOM part names.
17
+ *
18
+ * This must be used in the `part` attribute and must be the only binding in it.
19
+ * Each property name in `partNameInfo` is added to the element's `part` list
20
+ * if the property value is truthy, and removed if the value is falsy.
21
+ */
22
+ export declare function partMap(partNameInfo: PartNameInfo): DirectiveResult;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2026 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { noChange } from 'lit';
7
+ import { Directive, directive, PartType } from 'lit/directive.js';
8
+
9
+ class PartMapDirective extends Directive {
10
+ // Part names applied by the directive on the previous render,
11
+ // used to remove names that no longer apply.
12
+ #previousParts;
13
+
14
+ // Part names declared statically in the attribute, never removed.
15
+ #staticParts;
16
+
17
+ constructor(partInfo) {
18
+ super(partInfo);
19
+ if (partInfo.type !== PartType.ATTRIBUTE || partInfo.name !== 'part' || partInfo.strings?.length > 2) {
20
+ throw new Error('`partMap()` can only be used in the `part` attribute and must be the only binding in it.');
21
+ }
22
+ }
23
+
24
+ render(partNameInfo) {
25
+ // Add spaces to ensure separation from static parts
26
+ return ` ${Object.keys(partNameInfo)
27
+ .filter((key) => partNameInfo[key])
28
+ .join(' ')} `;
29
+ }
30
+
31
+ update(part, [partNameInfo]) {
32
+ // Remember dynamic parts on the first render
33
+ if (this.#previousParts === undefined) {
34
+ this.#previousParts = new Set();
35
+ if (part.strings !== undefined) {
36
+ this.#staticParts = new Set(
37
+ part.strings
38
+ .join(' ')
39
+ .split(/\s/u)
40
+ .filter((s) => s !== ''),
41
+ );
42
+ }
43
+ Object.keys(partNameInfo).forEach((name) => {
44
+ if (partNameInfo[name] && !this.#staticParts?.has(name)) {
45
+ this.#previousParts.add(name);
46
+ }
47
+ });
48
+ return this.render(partNameInfo);
49
+ }
50
+
51
+ const partList = part.element.part;
52
+
53
+ // Remove old parts that no longer apply
54
+ this.#previousParts.forEach((name) => {
55
+ if (!(name in partNameInfo)) {
56
+ partList.remove(name);
57
+ this.#previousParts.delete(name);
58
+ }
59
+ });
60
+
61
+ // Add or remove parts based on their partMap value
62
+ Object.keys(partNameInfo).forEach((name) => {
63
+ const value = !!partNameInfo[name];
64
+ if (value !== this.#previousParts.has(name) && !this.#staticParts?.has(name)) {
65
+ if (value) {
66
+ partList.add(name);
67
+ this.#previousParts.add(name);
68
+ } else {
69
+ partList.remove(name);
70
+ this.#previousParts.delete(name);
71
+ }
72
+ }
73
+ });
74
+
75
+ return noChange;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * A directive that applies dynamic shadow DOM part names.
81
+ *
82
+ * This must be used in the `part` attribute and must be the only binding in it.
83
+ * Each property name in `partNameInfo` is added to the element's `part` list
84
+ * if the property value is truthy, and removed if the value is falsy.
85
+ */
86
+ export const partMap = directive(PartMapDirective);
@@ -37,15 +37,26 @@ export function deserializeAttributeValue(value: string): Set<string>;
37
37
  export function serializeAttributeValue(values: Set<string>): string;
38
38
 
39
39
  /**
40
- * Adds a value to an attribute containing space-delimited values.
40
+ * Sets the attribute to the given value, or removes the attribute when the
41
+ * value is falsy (e.g. `null`, `undefined`, `false` or an empty string).
41
42
  */
42
- export function addValueToAttribute(element: HTMLElement, attr: string, value: string): void;
43
+ export function setOrRemoveAttribute(
44
+ element: HTMLElement,
45
+ attr: string,
46
+ value: string | boolean | null | undefined,
47
+ ): void;
43
48
 
44
49
  /**
45
- * Removes a value from an attribute containing space-delimited values.
46
- * If the value is the last one, the whole attribute is removed.
50
+ * Adds one or more values to an attribute containing space-delimited values.
51
+ * If no values remain, the whole attribute is removed.
47
52
  */
48
- export function removeValueFromAttribute(element: HTMLElement, attr: string, value: string): void;
53
+ export function addValuesToAttribute(element: HTMLElement, attr: string, valuesToAdd: string | string[]): void;
54
+
55
+ /**
56
+ * Removes one or more values from an attribute containing space-delimited values.
57
+ * If no values remain, the whole attribute is removed.
58
+ */
59
+ export function removeValuesFromAttribute(element: HTMLElement, attr: string, valuesToRemove: string | string[]): void;
49
60
 
50
61
  /**
51
62
  * Returns true if the given node is an empty text node, false otherwise.
package/src/dom-utils.js CHANGED
@@ -84,11 +84,7 @@ export function getClosestElement(selector, node) {
84
84
  * @return {Set<string>}
85
85
  */
86
86
  export function deserializeAttributeValue(value) {
87
- if (!value) {
88
- return new Set();
89
- }
90
-
91
- return new Set(value.split(' '));
87
+ return new Set(value ? value.split(' ').filter(Boolean) : []);
92
88
  }
93
89
 
94
90
  /**
@@ -102,34 +98,65 @@ export function serializeAttributeValue(values) {
102
98
  }
103
99
 
104
100
  /**
105
- * Adds a value to an attribute containing space-delimited values.
101
+ * Sets the attribute to the given value, or removes the attribute when the
102
+ * value is falsy (e.g. `null`, `undefined`, `false` or an empty string).
106
103
  *
107
104
  * @param {HTMLElement} element
108
105
  * @param {string} attr
109
- * @param {string} value
106
+ * @param {string | boolean | null | undefined} value
107
+ */
108
+ export function setOrRemoveAttribute(element, attr, value) {
109
+ if (value) {
110
+ element.setAttribute(attr, value);
111
+ } else {
112
+ element.removeAttribute(attr);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Normalizes values passed to `addValuesToAttribute` and `removeValuesFromAttribute`
118
+ * into a set of values. Both a single string and every array entry may contain
119
+ * multiple values separated by space.
120
+ *
121
+ * @param {string | string[] | null | undefined} values
122
+ * @return {Set<string>}
123
+ */
124
+ function normalizeAttributeValues(values) {
125
+ return deserializeAttributeValue(Array.isArray(values) ? values.join(' ') : values);
126
+ }
127
+
128
+ /**
129
+ * Adds one or more values to an attribute containing space-delimited values.
130
+ * If no values remain, the whole attribute is removed.
131
+ *
132
+ * @param {HTMLElement} element
133
+ * @param {string} attr
134
+ * @param {string | string[]} valuesToAdd a string or an array of strings with values separated by space
110
135
  */
111
- export function addValueToAttribute(element, attr, value) {
136
+ export function addValuesToAttribute(element, attr, valuesToAdd) {
137
+ valuesToAdd = normalizeAttributeValues(valuesToAdd);
138
+
112
139
  const values = deserializeAttributeValue(element.getAttribute(attr));
113
- values.add(value);
114
- element.setAttribute(attr, serializeAttributeValue(values));
140
+ valuesToAdd.forEach((value) => values.add(value));
141
+
142
+ setOrRemoveAttribute(element, attr, serializeAttributeValue(values));
115
143
  }
116
144
 
117
145
  /**
118
- * Removes a value from an attribute containing space-delimited values.
119
- * If the value is the last one, the whole attribute is removed.
146
+ * Removes one or more values from an attribute containing space-delimited values.
147
+ * If no values remain, the whole attribute is removed.
120
148
  *
121
149
  * @param {HTMLElement} element
122
150
  * @param {string} attr
123
- * @param {string} value
151
+ * @param {string | string[]} valuesToRemove a string or an array of strings with values separated by space
124
152
  */
125
- export function removeValueFromAttribute(element, attr, value) {
153
+ export function removeValuesFromAttribute(element, attr, valuesToRemove) {
154
+ valuesToRemove = normalizeAttributeValues(valuesToRemove);
155
+
126
156
  const values = deserializeAttributeValue(element.getAttribute(attr));
127
- values.delete(value);
128
- if (values.size === 0) {
129
- element.removeAttribute(attr);
130
- return;
131
- }
132
- element.setAttribute(attr, serializeAttributeValue(values));
157
+ valuesToRemove.forEach((value) => values.delete(value));
158
+
159
+ setOrRemoveAttribute(element, attr, serializeAttributeValue(values));
133
160
  }
134
161
 
135
162
  /**
@@ -3,6 +3,7 @@
3
3
  * Copyright (c) 2021 - 2026 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
+ import { setOrRemoveAttribute } from './dom-utils.js';
6
7
 
7
8
  /**
8
9
  * A controller that detects if content inside the element overflows its scrolling viewport,
@@ -131,10 +132,6 @@ export class OverflowController {
131
132
  }
132
133
 
133
134
  #writeState({ overflow }) {
134
- if (overflow) {
135
- this.host.setAttribute('overflow', overflow);
136
- } else {
137
- this.host.removeAttribute('overflow');
138
- }
135
+ setOrRemoveAttribute(this.host, 'overflow', overflow);
139
136
  }
140
137
  }
@@ -22,7 +22,6 @@ export class IronListAdapter {
22
22
  reorderElements,
23
23
  elementsContainer,
24
24
  __disableHeightPlaceholder,
25
- __alwaysUpdateScrollerSize,
26
25
  }) {
27
26
  this.isAttached = true;
28
27
  this._vidxOffset = 0;
@@ -38,12 +37,6 @@ export class IronListAdapter {
38
37
  // elements with a non-zero height. Not for public use.
39
38
  this.__disableHeightPlaceholder = __disableHeightPlaceholder ?? false;
40
39
 
41
- // Internal option: a predicate that, when it returns true, makes the scroller
42
- // height always be applied instead of amortized (see `_updateScrollerSize`).
43
- // Used by components whose height tracks the content exactly (e.g. the grid's
44
- // `allRowsVisible` mode). Not for public use.
45
- this.__alwaysUpdateScrollerSize = __alwaysUpdateScrollerSize;
46
-
47
40
  // Iron-list uses this value to determine how many pages of elements to render
48
41
  this._maxPages = 1.3;
49
42
 
@@ -223,11 +216,6 @@ export class IronListAdapter {
223
216
  this.__afterElementsUpdated(updatedElements);
224
217
  }
225
218
 
226
- /** @override */
227
- _updateScrollerSize(forceUpdate) {
228
- super._updateScrollerSize(forceUpdate || !!this.__alwaysUpdateScrollerSize?.());
229
- }
230
-
231
219
  /**
232
220
  * Updates the height for a given set of items.
233
221
  *
@@ -426,6 +414,8 @@ export class IronListAdapter {
426
414
  requestAnimationFrame(() => this._resizeHandler());
427
415
  }
428
416
 
417
+ this._updateScrollerSize(true);
418
+
429
419
  // Re-render items once the scroll position has been restored.
430
420
  // This call also updates the cached scrollTarget height and
431
421
  // rechecks whether more virtual elements are needed, since the