@vaadin/component-base 25.3.0-alpha8 → 25.3.0-beta1
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/custom-elements.json +2 -1
- package/package.json +5 -4
- package/src/define.js +1 -1
- package/src/delegate-state-mixin.js +2 -3
- package/src/dir-mixin.d.ts +14 -0
- package/src/dir-mixin.js +10 -0
- package/src/dom-utils.d.ts +23 -5
- package/src/dom-utils.js +66 -20
- package/src/gestures.d.ts +12 -0
- package/src/i18n-mixin.js +3 -30
- package/src/object-utils.d.ts +39 -0
- package/src/object-utils.js +109 -0
- package/src/overflow-controller.js +2 -5
- package/src/slot-child-observe-controller.js +2 -19
- package/src/slot-observer.d.ts +7 -1
- package/src/slot-observer.js +24 -4
- package/src/virtualizer-iron-list-adapter.js +10 -46
package/custom-elements.json
CHANGED
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
{
|
|
51
51
|
"name": "superClass"
|
|
52
52
|
}
|
|
53
|
-
]
|
|
53
|
+
],
|
|
54
|
+
"deprecated": "This mixin is deprecated and will be removed in Vaadin 26,\nafter which components will no longer set the `dir` attribute on themselves.\nUse the `:dir(rtl)` CSS selector to style components in right-to-left mode,\nand `element.matches(':dir(rtl)')` to detect it in JavaScript."
|
|
54
55
|
}
|
|
55
56
|
],
|
|
56
57
|
"exports": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/component-base",
|
|
3
|
-
"version": "25.3.0-
|
|
3
|
+
"version": "25.3.0-beta1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -38,11 +38,12 @@
|
|
|
38
38
|
"lit": "^3.0.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@vaadin/
|
|
41
|
+
"@polymer/polymer": "^3.0.0",
|
|
42
|
+
"@vaadin/chai-plugins": "25.3.0-beta1",
|
|
43
|
+
"@vaadin/test-runner-commands": "25.3.0-beta1",
|
|
43
44
|
"@vaadin/testing-helpers": "^2.0.0",
|
|
44
45
|
"sinon": "^22.0.0"
|
|
45
46
|
},
|
|
46
47
|
"customElements": "custom-elements.json",
|
|
47
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "295432e44a6967e1aff36462b2e3e1d0e64eb8cc"
|
|
48
49
|
}
|
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-
|
|
16
|
+
export function defineCustomElement(CustomElement, version = '25.3.0-beta1') {
|
|
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
|
|
108
|
+
setOrRemoveAttribute(this.stateTarget, name, value);
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
111
|
|
package/src/dir-mixin.d.ts
CHANGED
|
@@ -7,9 +7,23 @@ import type { Constructor } from '@open-wc/dedupe-mixin';
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A mixin to handle `dir` attribute based on the one set on the `<html>` element.
|
|
10
|
+
*
|
|
11
|
+
* @deprecated This mixin is deprecated and will be removed in Vaadin 26,
|
|
12
|
+
* after which components will no longer set the `dir` attribute on themselves.
|
|
13
|
+
* Use the `:dir(rtl)` CSS selector to style components in right-to-left mode,
|
|
14
|
+
* and `element.matches(':dir(rtl)')` to detect it in JavaScript.
|
|
10
15
|
*/
|
|
11
16
|
export declare function DirMixin<T extends Constructor<HTMLElement>>(base: T): Constructor<DirMixinClass> & T;
|
|
12
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @deprecated This mixin is deprecated and will be removed in Vaadin 26,
|
|
20
|
+
* after which components will no longer set the `dir` attribute on themselves.
|
|
21
|
+
* Use the `:dir(rtl)` CSS selector to style components in right-to-left mode,
|
|
22
|
+
* and `element.matches(':dir(rtl)')` to detect it in JavaScript.
|
|
23
|
+
*/
|
|
13
24
|
export declare class DirMixinClass {
|
|
25
|
+
/**
|
|
26
|
+
* @deprecated Use `this.matches(':dir(rtl)')` instead.
|
|
27
|
+
*/
|
|
14
28
|
protected readonly __isRTL: boolean;
|
|
15
29
|
}
|
package/src/dir-mixin.js
CHANGED
|
@@ -33,6 +33,11 @@ directionObserver.observe(document.documentElement, { attributes: true, attribut
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* A mixin to handle `dir` attribute based on the one set on the `<html>` element.
|
|
36
|
+
*
|
|
37
|
+
* @deprecated This mixin is deprecated and will be removed in Vaadin 26,
|
|
38
|
+
* after which components will no longer set the `dir` attribute on themselves.
|
|
39
|
+
* Use the `:dir(rtl)` CSS selector to style components in right-to-left mode,
|
|
40
|
+
* and `element.matches(':dir(rtl)')` to detect it in JavaScript.
|
|
36
41
|
*/
|
|
37
42
|
export const DirMixin = (superClass) =>
|
|
38
43
|
class VaadinDirMixin extends superClass {
|
|
@@ -60,6 +65,7 @@ export const DirMixin = (superClass) =>
|
|
|
60
65
|
/**
|
|
61
66
|
* @return {boolean}
|
|
62
67
|
* @protected
|
|
68
|
+
* @deprecated Use `this.matches(':dir(rtl)')` instead.
|
|
63
69
|
*/
|
|
64
70
|
get __isRTL() {
|
|
65
71
|
return this.getAttribute('dir') === 'rtl';
|
|
@@ -106,6 +112,10 @@ export const DirMixin = (superClass) =>
|
|
|
106
112
|
this.__unsubscribe();
|
|
107
113
|
}
|
|
108
114
|
|
|
115
|
+
// The two overrides below are only invoked by Polymer's property reflection.
|
|
116
|
+
// Vaadin components no longer extend `PolymerElement`, but some add-ons still
|
|
117
|
+
// apply this mixin to `PolymerElement`, so the overrides are kept for them.
|
|
118
|
+
|
|
109
119
|
/** @protected */
|
|
110
120
|
_valueToNodeAttribute(node, value, attribute) {
|
|
111
121
|
// Override default Polymer attribute reflection to match native behavior of HTMLElement.dir property
|
package/src/dom-utils.d.ts
CHANGED
|
@@ -37,17 +37,35 @@ export function deserializeAttributeValue(value: string): Set<string>;
|
|
|
37
37
|
export function serializeAttributeValue(values: Set<string>): string;
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
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
|
|
43
|
+
export function setOrRemoveAttribute(
|
|
44
|
+
element: HTMLElement,
|
|
45
|
+
attr: string,
|
|
46
|
+
value: string | boolean | null | undefined,
|
|
47
|
+
): void;
|
|
43
48
|
|
|
44
49
|
/**
|
|
45
|
-
*
|
|
46
|
-
* If
|
|
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
|
|
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.
|
|
52
63
|
*/
|
|
53
64
|
export function isEmptyTextNode(node: Node): boolean;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns true if the given node has content of its own: an element with
|
|
68
|
+
* children, a defined custom element — which may render content in its
|
|
69
|
+
* shadow root — or a node with non-empty text.
|
|
70
|
+
*/
|
|
71
|
+
export function hasNodeContent(node: Node | null | undefined): boolean;
|
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
|
-
|
|
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
|
-
*
|
|
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>}
|
|
110
123
|
*/
|
|
111
|
-
|
|
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
|
|
135
|
+
*/
|
|
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
|
-
|
|
140
|
+
valuesToAdd.forEach((value) => values.add(value));
|
|
141
|
+
|
|
142
|
+
setOrRemoveAttribute(element, attr, serializeAttributeValue(values));
|
|
115
143
|
}
|
|
116
144
|
|
|
117
145
|
/**
|
|
118
|
-
* Removes
|
|
119
|
-
* If
|
|
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}
|
|
151
|
+
* @param {string | string[]} valuesToRemove a string or an array of strings with values separated by space
|
|
124
152
|
*/
|
|
125
|
-
export function
|
|
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
|
-
|
|
129
|
-
|
|
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
|
/**
|
|
@@ -141,3 +168,22 @@ export function removeValueFromAttribute(element, attr, value) {
|
|
|
141
168
|
export function isEmptyTextNode(node) {
|
|
142
169
|
return node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '';
|
|
143
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Returns true if the given node has content of its own: an element with
|
|
174
|
+
* children, a defined custom element — which may render content in its
|
|
175
|
+
* shadow root — or a node with non-empty text.
|
|
176
|
+
*
|
|
177
|
+
* @param {Node | null | undefined} node
|
|
178
|
+
* @return {boolean}
|
|
179
|
+
*/
|
|
180
|
+
export function hasNodeContent(node) {
|
|
181
|
+
if (!node) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return Boolean(
|
|
186
|
+
(node.nodeType === Node.ELEMENT_NODE && (customElements.get(node.localName) || node.children.length > 0)) ||
|
|
187
|
+
node.textContent?.trim(),
|
|
188
|
+
);
|
|
189
|
+
}
|
package/src/gestures.d.ts
CHANGED
|
@@ -43,6 +43,18 @@ export { removeListener };
|
|
|
43
43
|
declare function register(recog: GestureRecognizer): void;
|
|
44
44
|
export { register };
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The registered gesture event recognizers by their event type.
|
|
48
|
+
*/
|
|
49
|
+
declare const gestures: Record<string, GestureRecognizer | undefined>;
|
|
50
|
+
export { gestures };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The registered gesture event recognizers.
|
|
54
|
+
*/
|
|
55
|
+
declare const recognizers: GestureRecognizer[];
|
|
56
|
+
export { recognizers };
|
|
57
|
+
|
|
46
58
|
/**
|
|
47
59
|
* Sets scrolling direction on node.
|
|
48
60
|
*
|
package/src/i18n-mixin.js
CHANGED
|
@@ -3,34 +3,7 @@
|
|
|
3
3
|
* Copyright (c) 2025 - 2026 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
function deepMerge(target, ...sources) {
|
|
8
|
-
const isArray = (item) => Array.isArray(item);
|
|
9
|
-
const isObject = (item) => item && typeof item === 'object' && !isArray(item);
|
|
10
|
-
const merge = (target, source) => {
|
|
11
|
-
if (isObject(source) && isObject(target)) {
|
|
12
|
-
Object.keys(source).forEach((key) => {
|
|
13
|
-
const sourceValue = source[key];
|
|
14
|
-
if (isObject(sourceValue)) {
|
|
15
|
-
if (!target[key]) {
|
|
16
|
-
target[key] = {};
|
|
17
|
-
}
|
|
18
|
-
merge(target[key], sourceValue);
|
|
19
|
-
} else if (isArray(sourceValue)) {
|
|
20
|
-
target[key] = [...sourceValue];
|
|
21
|
-
} else if (sourceValue !== undefined && sourceValue !== null) {
|
|
22
|
-
target[key] = sourceValue;
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
sources.forEach((source) => {
|
|
29
|
-
merge(target, source);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
return target;
|
|
33
|
-
}
|
|
6
|
+
import { deepMergePartials } from './object-utils.js';
|
|
34
7
|
|
|
35
8
|
/**
|
|
36
9
|
* A mixin that allows to set partial I18N properties.
|
|
@@ -75,7 +48,7 @@ export const I18nMixin = (superClass) =>
|
|
|
75
48
|
constructor() {
|
|
76
49
|
super();
|
|
77
50
|
|
|
78
|
-
this.i18n =
|
|
51
|
+
this.i18n = deepMergePartials({}, this.constructor.defaultI18n);
|
|
79
52
|
}
|
|
80
53
|
|
|
81
54
|
/**
|
|
@@ -97,6 +70,6 @@ export const I18nMixin = (superClass) =>
|
|
|
97
70
|
return;
|
|
98
71
|
}
|
|
99
72
|
this.__customI18n = value;
|
|
100
|
-
this.__effectiveI18n =
|
|
73
|
+
this.__effectiveI18n = deepMergePartials({}, this.constructor.defaultI18n, this.__customI18n);
|
|
101
74
|
}
|
|
102
75
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Recursively copies own properties of `source` into `target` and returns
|
|
9
|
+
* `target`. Plain objects are merged, other values are assigned as they are.
|
|
10
|
+
* An object is plain when it inherits from `Object.prototype` or from nothing,
|
|
11
|
+
* so values such as a `Date` or a class instance are assigned, not merged.
|
|
12
|
+
*
|
|
13
|
+
* Merges a single source. Use `deepMergePartials()` to merge several objects,
|
|
14
|
+
* or to merge objects that only provide some of the properties.
|
|
15
|
+
*
|
|
16
|
+
* Both arguments are expected to be plain objects. When either of them is not,
|
|
17
|
+
* `target` is returned without changes. A property of the target that is not a
|
|
18
|
+
* plain object is replaced with the merged object, unlike the arguments.
|
|
19
|
+
*
|
|
20
|
+
* Keys that would modify `Object.prototype`, such as `__proto__`, are ignored.
|
|
21
|
+
*/
|
|
22
|
+
export function deepMerge<T extends object>(target: T, source: object): T;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Recursively merges partial objects into `target` in order and returns
|
|
26
|
+
* `target`, so that a later source overrides an earlier one.
|
|
27
|
+
*
|
|
28
|
+
* Values that are `null` or `undefined` are skipped, so a source that only
|
|
29
|
+
* provides some of the properties does not remove the others. For the same
|
|
30
|
+
* reason, a property that the target already has is not replaced with an
|
|
31
|
+
* object when the source has one for the same key. Arrays are copied one level
|
|
32
|
+
* deep, so that the result does not share an array with any of the sources.
|
|
33
|
+
*
|
|
34
|
+
* Sources that are not plain objects are ignored. When `target` is not a plain
|
|
35
|
+
* object, it is returned without changes.
|
|
36
|
+
*
|
|
37
|
+
* Keys that would modify `Object.prototype`, such as `__proto__`, are ignored.
|
|
38
|
+
*/
|
|
39
|
+
export function deepMergePartials<T extends object>(target: T, ...sources: object[]): T;
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Keys that are not copied while merging, as assigning them would modify
|
|
9
|
+
* `Object.prototype` instead of the merge target, and so affect every
|
|
10
|
+
* object in the application.
|
|
11
|
+
*/
|
|
12
|
+
const IGNORED_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
13
|
+
|
|
14
|
+
const isPlainObject = (value) => {
|
|
15
|
+
if (!value || typeof value !== 'object') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const prototype = Object.getPrototypeOf(value);
|
|
19
|
+
return prototype === Object.prototype || prototype === null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Merges `source` into `target`. With `partial`, the source is treated as an
|
|
24
|
+
* object that may provide only some of the properties: nullish values are
|
|
25
|
+
* skipped and arrays are copied instead of shared.
|
|
26
|
+
*/
|
|
27
|
+
function merge(target, source, partial) {
|
|
28
|
+
if (!isPlainObject(target) || !isPlainObject(source)) {
|
|
29
|
+
return target;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Object.keys(source).forEach((key) => {
|
|
33
|
+
if (IGNORED_KEYS.includes(key)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const value = source[key];
|
|
38
|
+
|
|
39
|
+
if (isPlainObject(value)) {
|
|
40
|
+
// Only merge into an own plain object, so that the merge can never
|
|
41
|
+
// continue into an object inherited from the prototype chain.
|
|
42
|
+
if (!Object.hasOwn(target, key) || !isPlainObject(target[key])) {
|
|
43
|
+
// With `partial`, a value that the target already has is kept, so that
|
|
44
|
+
// a source property of an unexpected type does not remove a default.
|
|
45
|
+
if (partial && Object.hasOwn(target, key) && target[key]) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
target[key] = {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
merge(target[key], value, partial);
|
|
53
|
+
} else if (partial && Array.isArray(value)) {
|
|
54
|
+
target[key] = [...value];
|
|
55
|
+
} else if (!partial || (value !== undefined && value !== null)) {
|
|
56
|
+
target[key] = value;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return target;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Recursively copies own properties of `source` into `target` and returns
|
|
65
|
+
* `target`. Plain objects are merged, other values are assigned as they are.
|
|
66
|
+
* An object is plain when it inherits from `Object.prototype` or from nothing,
|
|
67
|
+
* so values such as a `Date` or a class instance are assigned, not merged.
|
|
68
|
+
*
|
|
69
|
+
* Merges a single source. Use `deepMergePartials()` to merge several objects,
|
|
70
|
+
* or to merge objects that only provide some of the properties.
|
|
71
|
+
*
|
|
72
|
+
* Both arguments are expected to be plain objects. When either of them is not,
|
|
73
|
+
* `target` is returned without changes. A property of the target that is not a
|
|
74
|
+
* plain object is replaced with the merged object, unlike the arguments.
|
|
75
|
+
*
|
|
76
|
+
* Keys that would modify `Object.prototype`, such as `__proto__`, are ignored.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} target the object to merge into, modified in place
|
|
79
|
+
* @param {object} source the object to copy the properties from
|
|
80
|
+
* @return {object} the `target` object
|
|
81
|
+
*/
|
|
82
|
+
export function deepMerge(target, source) {
|
|
83
|
+
return merge(target, source, false);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Recursively merges partial objects into `target` in order and returns
|
|
88
|
+
* `target`, so that a later source overrides an earlier one.
|
|
89
|
+
*
|
|
90
|
+
* Values that are `null` or `undefined` are skipped, so a source that only
|
|
91
|
+
* provides some of the properties does not remove the others. For the same
|
|
92
|
+
* reason, a property that the target already has is not replaced with an
|
|
93
|
+
* object when the source has one for the same key. Arrays are copied one level
|
|
94
|
+
* deep, so that the result does not share an array with any of the sources.
|
|
95
|
+
*
|
|
96
|
+
* Sources that are not plain objects are ignored. When `target` is not a plain
|
|
97
|
+
* object, it is returned without changes.
|
|
98
|
+
*
|
|
99
|
+
* Keys that would modify `Object.prototype`, such as `__proto__`, are ignored.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} target the object to merge into, modified in place
|
|
102
|
+
* @param {...object} sources the objects to copy the properties from
|
|
103
|
+
* @return {object} the `target` object
|
|
104
|
+
*/
|
|
105
|
+
export function deepMergePartials(target, ...sources) {
|
|
106
|
+
sources.forEach((source) => merge(target, source, true));
|
|
107
|
+
|
|
108
|
+
return target;
|
|
109
|
+
}
|
|
@@ -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
|
-
|
|
135
|
-
this.host.setAttribute('overflow', overflow);
|
|
136
|
-
} else {
|
|
137
|
-
this.host.removeAttribute('overflow');
|
|
138
|
-
}
|
|
135
|
+
setOrRemoveAttribute(this.host, 'overflow', overflow);
|
|
139
136
|
}
|
|
140
137
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Copyright (c) 2022 - 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 { hasNodeContent } from './dom-utils.js';
|
|
6
7
|
import { SlotController } from './slot-controller.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -130,24 +131,6 @@ export class SlotChildObserveController extends SlotController {
|
|
|
130
131
|
});
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
/**
|
|
134
|
-
* Returns true if a node is an HTML element with children,
|
|
135
|
-
* or is a defined custom element, or has non-empty text.
|
|
136
|
-
*
|
|
137
|
-
* @param {Node} node
|
|
138
|
-
* @return {boolean}
|
|
139
|
-
*/
|
|
140
|
-
#hasContent(node) {
|
|
141
|
-
if (!node) {
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
(node.nodeType === Node.ELEMENT_NODE && (customElements.get(node.localName) || node.children.length > 0)) ||
|
|
147
|
-
(node.textContent && node.textContent.trim() !== '')
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
134
|
/**
|
|
152
135
|
* Fire an event to notify the controller host about node changes.
|
|
153
136
|
*
|
|
@@ -157,7 +140,7 @@ export class SlotChildObserveController extends SlotController {
|
|
|
157
140
|
_notifyChange(node) {
|
|
158
141
|
this.dispatchEvent(
|
|
159
142
|
new CustomEvent('slot-content-changed', {
|
|
160
|
-
detail: { hasContent:
|
|
143
|
+
detail: { hasContent: hasNodeContent(node), node },
|
|
161
144
|
}),
|
|
162
145
|
);
|
|
163
146
|
}
|
package/src/slot-observer.d.ts
CHANGED
|
@@ -14,12 +14,18 @@
|
|
|
14
14
|
* bubbling to it and diffs the **union** of `assignedNodes({ flatten: true })`
|
|
15
15
|
* every descendant `<slot>`. Cross-slot reassignment of the same node does
|
|
16
16
|
* not change the union and therefore fires no callback.
|
|
17
|
+
*
|
|
18
|
+
* The initial pass runs in a microtask by default. Use the `syncInitial` option
|
|
19
|
+
* when the callback sets state that affects the layout of the component, so that
|
|
20
|
+
* it has its final size once connected. Otherwise consumers that measure it
|
|
21
|
+
* synchronously, such as auto-width columns in `<vaadin-grid>`, would measure the
|
|
22
|
+
* component before that state is applied.
|
|
17
23
|
*/
|
|
18
24
|
export class SlotObserver {
|
|
19
25
|
constructor(
|
|
20
26
|
target: HTMLSlotElement | DocumentFragment,
|
|
21
27
|
callback: (info: { addedNodes: Node[]; currentNodes: Node[]; movedNodes: Node[]; removedNodes: Node[] }) => void,
|
|
22
|
-
forceInitial?: boolean,
|
|
28
|
+
options?: { forceInitial?: boolean; syncInitial?: boolean },
|
|
23
29
|
);
|
|
24
30
|
|
|
25
31
|
readonly target: HTMLSlotElement | DocumentFragment;
|
package/src/slot-observer.js
CHANGED
|
@@ -14,9 +14,20 @@
|
|
|
14
14
|
* bubbling to it and diffs the **union** of `assignedNodes({ flatten: true })`
|
|
15
15
|
* across every descendant `<slot>`. Cross-slot reassignment of the same node
|
|
16
16
|
* does not change the union and therefore fires no callback.
|
|
17
|
+
*
|
|
18
|
+
* The initial pass runs in a microtask by default. Use the `syncInitial` option
|
|
19
|
+
* when the callback sets state that affects the layout of the component, so that
|
|
20
|
+
* it has its final size once connected. Otherwise consumers that measure it
|
|
21
|
+
* synchronously, such as auto-width columns in `<vaadin-grid>`, would measure the
|
|
22
|
+
* component before that state is applied.
|
|
17
23
|
*/
|
|
18
24
|
export class SlotObserver {
|
|
19
|
-
|
|
25
|
+
/**
|
|
26
|
+
* @param {HTMLSlotElement | DocumentFragment} target
|
|
27
|
+
* @param {Function} callback
|
|
28
|
+
* @param {{ forceInitial?: boolean, syncInitial?: boolean }} options
|
|
29
|
+
*/
|
|
30
|
+
constructor(target, callback, options = {}) {
|
|
20
31
|
/** @type {HTMLSlotElement | DocumentFragment} */
|
|
21
32
|
this.target = target;
|
|
22
33
|
|
|
@@ -24,7 +35,7 @@ export class SlotObserver {
|
|
|
24
35
|
this.callback = callback;
|
|
25
36
|
|
|
26
37
|
/** @type {boolean} */
|
|
27
|
-
this.forceInitial = forceInitial;
|
|
38
|
+
this.forceInitial = options.forceInitial;
|
|
28
39
|
|
|
29
40
|
/** @type {Node[]} */
|
|
30
41
|
this._storedNodes = [];
|
|
@@ -40,7 +51,12 @@ export class SlotObserver {
|
|
|
40
51
|
};
|
|
41
52
|
|
|
42
53
|
this.connect();
|
|
43
|
-
|
|
54
|
+
|
|
55
|
+
if (options.syncInitial) {
|
|
56
|
+
this.flush();
|
|
57
|
+
} else {
|
|
58
|
+
this._schedule();
|
|
59
|
+
}
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
/**
|
|
@@ -69,7 +85,11 @@ export class SlotObserver {
|
|
|
69
85
|
this._scheduled = true;
|
|
70
86
|
|
|
71
87
|
queueMicrotask(() => {
|
|
72
|
-
|
|
88
|
+
// Skip if the nodes have already been processed by an explicit `flush()`
|
|
89
|
+
// in the meantime, to avoid running the diff a second time for nothing.
|
|
90
|
+
if (this._scheduled) {
|
|
91
|
+
this.flush();
|
|
92
|
+
}
|
|
73
93
|
});
|
|
74
94
|
}
|
|
75
95
|
}
|
|
@@ -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
|
|
|
@@ -85,12 +78,7 @@ export class IronListAdapter {
|
|
|
85
78
|
});
|
|
86
79
|
attachObserver.observe(this.scrollTarget);
|
|
87
80
|
|
|
88
|
-
this.
|
|
89
|
-
this.elementsContainer.addEventListener('focusin', () => {
|
|
90
|
-
this.scrollTarget.dispatchEvent(
|
|
91
|
-
new CustomEvent('virtualizer-element-focused', { detail: { element: this.__getFocusedElement() } }),
|
|
92
|
-
);
|
|
93
|
-
});
|
|
81
|
+
this.elementsContainer.addEventListener('focusin', () => this.__onElementFocused());
|
|
94
82
|
|
|
95
83
|
if (this.reorderElements) {
|
|
96
84
|
// Reordering the physical elements cancels the user's grab of the scroll bar handle on Safari.
|
|
@@ -168,7 +156,7 @@ export class IronListAdapter {
|
|
|
168
156
|
this.__skipNextVirtualIndexAdjust = true;
|
|
169
157
|
super.scrollToIndex(targetVirtualIndex);
|
|
170
158
|
|
|
171
|
-
if (this.adjustedFirstVisibleIndex !== index && this._scrollTop < this._maxScrollTop
|
|
159
|
+
if (this.adjustedFirstVisibleIndex !== index && this._scrollTop < this._maxScrollTop) {
|
|
172
160
|
// Workaround an iron-list issue by manually adjusting the scroll position
|
|
173
161
|
this._scrollTop -= this.__getIndexScrollOffset(index) || 0;
|
|
174
162
|
}
|
|
@@ -196,9 +184,6 @@ export class IronListAdapter {
|
|
|
196
184
|
if (this.__scrollReorderDebouncer) {
|
|
197
185
|
this.__scrollReorderDebouncer.flush();
|
|
198
186
|
}
|
|
199
|
-
if (this.__debouncerWheelAnimationFrame) {
|
|
200
|
-
this.__debouncerWheelAnimationFrame.flush();
|
|
201
|
-
}
|
|
202
187
|
}
|
|
203
188
|
|
|
204
189
|
hostConnected() {
|
|
@@ -223,11 +208,6 @@ export class IronListAdapter {
|
|
|
223
208
|
this.__afterElementsUpdated(updatedElements);
|
|
224
209
|
}
|
|
225
210
|
|
|
226
|
-
/** @override */
|
|
227
|
-
_updateScrollerSize(forceUpdate) {
|
|
228
|
-
super._updateScrollerSize(forceUpdate || !!this.__alwaysUpdateScrollerSize?.());
|
|
229
|
-
}
|
|
230
|
-
|
|
231
211
|
/**
|
|
232
212
|
* Updates the height for a given set of items.
|
|
233
213
|
*
|
|
@@ -426,6 +406,8 @@ export class IronListAdapter {
|
|
|
426
406
|
requestAnimationFrame(() => this._resizeHandler());
|
|
427
407
|
}
|
|
428
408
|
|
|
409
|
+
this._updateScrollerSize(true);
|
|
410
|
+
|
|
429
411
|
// Re-render items once the scroll position has been restored.
|
|
430
412
|
// This call also updates the cached scrollTarget height and
|
|
431
413
|
// rechecks whether more virtual elements are needed, since the
|
|
@@ -472,16 +454,10 @@ export class IronListAdapter {
|
|
|
472
454
|
|
|
473
455
|
/** @private */
|
|
474
456
|
updateViewportBoundaries() {
|
|
475
|
-
|
|
476
|
-
this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(styles['padding-top'], 10);
|
|
477
|
-
this._isRTL = Boolean(styles.direction === 'rtl');
|
|
478
|
-
this._viewportWidth = this.elementsContainer.offsetWidth;
|
|
457
|
+
this._scrollerPaddingTop = parseInt(window.getComputedStyle(this.scrollTarget)['padding-top'], 10);
|
|
479
458
|
this._viewportHeight = this.scrollTarget.offsetHeight;
|
|
480
459
|
}
|
|
481
460
|
|
|
482
|
-
/** @private */
|
|
483
|
-
setAttribute() {}
|
|
484
|
-
|
|
485
461
|
/** @private */
|
|
486
462
|
_createPool(size) {
|
|
487
463
|
const physicalItems = this.createElements(size);
|
|
@@ -531,20 +507,8 @@ export class IronListAdapter {
|
|
|
531
507
|
toggleScrollListener() {}
|
|
532
508
|
|
|
533
509
|
/** @private */
|
|
534
|
-
__getFocusedElement(
|
|
535
|
-
|
|
536
|
-
// focus lives in a nested shadow tree. Descend through nested shadow
|
|
537
|
-
// roots' `activeElement`s to reach the real focused node, then walk up
|
|
538
|
-
// the flattened tree (via `assignedSlot`/`parentNode`/`host`) until a
|
|
539
|
-
// visible row is reached.
|
|
540
|
-
let node = document.activeElement;
|
|
541
|
-
while (node?.shadowRoot?.activeElement) {
|
|
542
|
-
node = node.shadowRoot.activeElement;
|
|
543
|
-
}
|
|
544
|
-
while (node && !visibleElements.includes(node)) {
|
|
545
|
-
node = node.assignedSlot || node.parentNode || node.host;
|
|
546
|
-
}
|
|
547
|
-
return node;
|
|
510
|
+
__getFocusedElement() {
|
|
511
|
+
return this.__getVisibleElements().find((element) => element.matches(':focus-within'));
|
|
548
512
|
}
|
|
549
513
|
|
|
550
514
|
/** @private */
|
|
@@ -568,12 +532,12 @@ export class IronListAdapter {
|
|
|
568
532
|
}
|
|
569
533
|
|
|
570
534
|
/** @private */
|
|
571
|
-
__onElementFocused(
|
|
535
|
+
__onElementFocused() {
|
|
572
536
|
if (!this.reorderElements) {
|
|
573
537
|
return;
|
|
574
538
|
}
|
|
575
539
|
|
|
576
|
-
const focusedElement =
|
|
540
|
+
const focusedElement = this.__getFocusedElement();
|
|
577
541
|
if (!focusedElement) {
|
|
578
542
|
return;
|
|
579
543
|
}
|
|
@@ -815,7 +779,7 @@ export class IronListAdapter {
|
|
|
815
779
|
|
|
816
780
|
// Which row to use as a target?
|
|
817
781
|
const visibleElements = this.__getVisibleElements();
|
|
818
|
-
const targetElement = this.__getFocusedElement(
|
|
782
|
+
const targetElement = this.__getFocusedElement() || visibleElements[0];
|
|
819
783
|
if (!targetElement) {
|
|
820
784
|
// All elements are hidden, don't reorder
|
|
821
785
|
return;
|