@vaadin/component-base 25.3.0-alpha1 → 25.3.0-alpha11
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/css-utils.js +40 -0
- package/src/data-provider-controller/cache.js +31 -7
- package/src/data-provider-controller/data-provider-controller.js +14 -17
- 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/directives/part-map.d.ts +22 -0
- package/src/directives/part-map.js +86 -0
- package/src/dom-utils.d.ts +16 -5
- package/src/dom-utils.js +47 -20
- package/src/media-query-controller.js +17 -19
- package/src/overflow-controller.js +32 -34
- package/src/slot-child-observe-controller.d.ts +5 -0
- package/src/slot-child-observe-controller.js +19 -18
- package/src/slot-controller.js +2 -1
- package/src/slot-observer.d.ts +7 -1
- package/src/slot-observer.js +24 -4
- package/src/styles/style-props.js +4 -2
- package/src/styles/user-colors.js +1 -1
- package/src/tooltip-controller.js +8 -11
- package/src/virtualizer-iron-list-adapter.js +16 -35
- package/src/styles/add-global-styles.js +0 -19
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-alpha11",
|
|
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-alpha11",
|
|
43
|
+
"@vaadin/test-runner-commands": "25.3.0-alpha11",
|
|
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": "7e0c61a37e68d8971def9cdf28ad0548a0f530a9"
|
|
48
49
|
}
|
package/src/css-utils.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2025 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registers a CSS custom property, tolerating the case where the property has
|
|
9
|
+
* already been registered. This can happen when a module is evaluated more than
|
|
10
|
+
* once (e.g. duplicate bundle chunks or two copies of the library on the page),
|
|
11
|
+
* in which case `CSS.registerProperty` throws `InvalidModificationError`. That
|
|
12
|
+
* error is caught and logged as a warning rather than allowed to break loading.
|
|
13
|
+
*
|
|
14
|
+
* @param {PropertyDefinition} definition
|
|
15
|
+
*/
|
|
16
|
+
export function registerCSSProperty(definition) {
|
|
17
|
+
try {
|
|
18
|
+
CSS.registerProperty(definition);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
if (e instanceof DOMException && e.name === 'InvalidModificationError') {
|
|
21
|
+
console.warn(`The CSS property ${definition.name} has already been registered.`);
|
|
22
|
+
} else {
|
|
23
|
+
throw e;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Add a `<style>` block with given styles to the document.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} id the id to set on the created element, only for informational purposes
|
|
32
|
+
* @param {CSSResultGroup[]} styles the styles to add
|
|
33
|
+
*/
|
|
34
|
+
export const addGlobalStyles = (id, ...styles) => {
|
|
35
|
+
const styleTag = document.createElement('style');
|
|
36
|
+
styleTag.id = id;
|
|
37
|
+
styleTag.textContent = styles.map((style) => style.toString()).join('\n');
|
|
38
|
+
|
|
39
|
+
document.head.insertAdjacentElement('afterbegin', styleTag);
|
|
40
|
+
};
|
|
@@ -15,13 +15,6 @@ export class Cache {
|
|
|
15
15
|
*/
|
|
16
16
|
context;
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
* The number of items to display per page.
|
|
20
|
-
*
|
|
21
|
-
* @type {number}
|
|
22
|
-
*/
|
|
23
|
-
pageSize;
|
|
24
|
-
|
|
25
18
|
/**
|
|
26
19
|
* An array of cached items.
|
|
27
20
|
*
|
|
@@ -50,6 +43,14 @@ export class Cache {
|
|
|
50
43
|
*/
|
|
51
44
|
#subCacheByIndex = {};
|
|
52
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The number of items per page.
|
|
48
|
+
*
|
|
49
|
+
* @type {number}
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
#pageSize;
|
|
53
|
+
|
|
53
54
|
/**
|
|
54
55
|
* The number of items.
|
|
55
56
|
*
|
|
@@ -123,6 +124,29 @@ export class Cache {
|
|
|
123
124
|
return this.#flatSize;
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The number of items per page.
|
|
129
|
+
*
|
|
130
|
+
* @return {number}
|
|
131
|
+
*/
|
|
132
|
+
get pageSize() {
|
|
133
|
+
return this.#pageSize;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Sets the number of items per page for this cache and its descendants.
|
|
138
|
+
* Changing the page size discards all pending page requests.
|
|
139
|
+
*
|
|
140
|
+
* @param {number} pageSize
|
|
141
|
+
*/
|
|
142
|
+
set pageSize(pageSize) {
|
|
143
|
+
this.#pageSize = pageSize;
|
|
144
|
+
this.pendingRequests = {};
|
|
145
|
+
this.subCaches.forEach((subCache) => {
|
|
146
|
+
subCache.pageSize = pageSize;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
126
150
|
/**
|
|
127
151
|
* The number of items.
|
|
128
152
|
*
|
|
@@ -29,13 +29,6 @@ export class DataProviderController extends EventTarget {
|
|
|
29
29
|
*/
|
|
30
30
|
dataProviderParams;
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
* A number of items to display per page.
|
|
34
|
-
*
|
|
35
|
-
* @type {number}
|
|
36
|
-
*/
|
|
37
|
-
pageSize;
|
|
38
|
-
|
|
39
32
|
/**
|
|
40
33
|
* A callback that returns whether the given item is expanded.
|
|
41
34
|
*
|
|
@@ -78,14 +71,13 @@ export class DataProviderController extends EventTarget {
|
|
|
78
71
|
) {
|
|
79
72
|
super();
|
|
80
73
|
this.host = host;
|
|
81
|
-
this.pageSize = pageSize;
|
|
82
74
|
this.getItemId = getItemId;
|
|
83
75
|
this.isExpanded = isExpanded;
|
|
84
76
|
this.placeholder = placeholder;
|
|
85
77
|
this.isPlaceholder = isPlaceholder;
|
|
86
78
|
this.dataProvider = dataProvider;
|
|
87
79
|
this.dataProviderParams = dataProviderParams;
|
|
88
|
-
this.rootCache = this.#createRootCache(size);
|
|
80
|
+
this.rootCache = this.#createRootCache(pageSize, size);
|
|
89
81
|
}
|
|
90
82
|
|
|
91
83
|
/**
|
|
@@ -95,6 +87,13 @@ export class DataProviderController extends EventTarget {
|
|
|
95
87
|
return this.rootCache.flatSize;
|
|
96
88
|
}
|
|
97
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The number of items per page in the root cache.
|
|
92
|
+
*/
|
|
93
|
+
get pageSize() {
|
|
94
|
+
return this.rootCache.pageSize;
|
|
95
|
+
}
|
|
96
|
+
|
|
98
97
|
/** @private */
|
|
99
98
|
get #cacheContext() {
|
|
100
99
|
return {
|
|
@@ -113,13 +112,12 @@ export class DataProviderController extends EventTarget {
|
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
/**
|
|
116
|
-
* Sets the page
|
|
115
|
+
* Sets the number of items per page in the root cache and any of its descendants.
|
|
117
116
|
*
|
|
118
117
|
* @param {number} pageSize
|
|
119
118
|
*/
|
|
120
119
|
setPageSize(pageSize) {
|
|
121
|
-
this.pageSize = pageSize;
|
|
122
|
-
this.clearCache();
|
|
120
|
+
this.rootCache.pageSize = pageSize;
|
|
123
121
|
}
|
|
124
122
|
|
|
125
123
|
/**
|
|
@@ -129,7 +127,6 @@ export class DataProviderController extends EventTarget {
|
|
|
129
127
|
*/
|
|
130
128
|
setDataProvider(dataProvider) {
|
|
131
129
|
this.dataProvider = dataProvider;
|
|
132
|
-
this.clearCache();
|
|
133
130
|
}
|
|
134
131
|
|
|
135
132
|
/**
|
|
@@ -143,7 +140,7 @@ export class DataProviderController extends EventTarget {
|
|
|
143
140
|
* Clears the cache.
|
|
144
141
|
*/
|
|
145
142
|
clearCache() {
|
|
146
|
-
this.rootCache = this.#createRootCache(this.rootCache.size);
|
|
143
|
+
this.rootCache = this.#createRootCache(this.rootCache.pageSize, this.rootCache.size);
|
|
147
144
|
}
|
|
148
145
|
|
|
149
146
|
/**
|
|
@@ -238,8 +235,8 @@ export class DataProviderController extends EventTarget {
|
|
|
238
235
|
}
|
|
239
236
|
|
|
240
237
|
/** @private */
|
|
241
|
-
#createRootCache(size) {
|
|
242
|
-
return new Cache(this.#cacheContext,
|
|
238
|
+
#createRootCache(pageSize, size) {
|
|
239
|
+
return new Cache(this.#cacheContext, pageSize, size);
|
|
243
240
|
}
|
|
244
241
|
|
|
245
242
|
/** @private */
|
|
@@ -250,7 +247,7 @@ export class DataProviderController extends EventTarget {
|
|
|
250
247
|
|
|
251
248
|
let params = {
|
|
252
249
|
page,
|
|
253
|
-
pageSize:
|
|
250
|
+
pageSize: cache.pageSize,
|
|
254
251
|
parentItem: cache.parentItem,
|
|
255
252
|
};
|
|
256
253
|
|
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-alpha11') {
|
|
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
|
|
@@ -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);
|
package/src/dom-utils.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
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>}
|
|
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
|
|
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
|
/**
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* A controller for listening on media query changes.
|
|
9
9
|
*/
|
|
10
10
|
export class MediaQueryController {
|
|
11
|
+
/** @type {MediaQueryList | null} */
|
|
12
|
+
#mediaQuery = null;
|
|
13
|
+
|
|
11
14
|
constructor(query, callback) {
|
|
12
15
|
/**
|
|
13
16
|
* The CSS media query to evaluate.
|
|
@@ -24,44 +27,39 @@ export class MediaQueryController {
|
|
|
24
27
|
* @protected
|
|
25
28
|
*/
|
|
26
29
|
this.callback = callback;
|
|
27
|
-
|
|
28
|
-
this._boundQueryHandler = this._queryHandler.bind(this);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
hostConnected() {
|
|
32
|
-
this
|
|
33
|
+
this.#removeListener();
|
|
33
34
|
|
|
34
|
-
this
|
|
35
|
+
this.#mediaQuery = window.matchMedia(this.query);
|
|
35
36
|
|
|
36
|
-
this
|
|
37
|
+
this.#addListener();
|
|
37
38
|
|
|
38
|
-
this
|
|
39
|
+
this.#queryHandler(this.#mediaQuery);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
hostDisconnected() {
|
|
42
|
-
this
|
|
43
|
+
this.#removeListener();
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this._mediaQuery.addListener(this._boundQueryHandler);
|
|
46
|
+
#addListener() {
|
|
47
|
+
if (this.#mediaQuery) {
|
|
48
|
+
this.#mediaQuery.addListener(this.#queryHandler);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this._mediaQuery.removeListener(this._boundQueryHandler);
|
|
52
|
+
#removeListener() {
|
|
53
|
+
if (this.#mediaQuery) {
|
|
54
|
+
this.#mediaQuery.removeListener(this.#queryHandler);
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
this
|
|
57
|
+
this.#mediaQuery = null;
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
_queryHandler(mediaQuery) {
|
|
60
|
+
#queryHandler = (mediaQuery) => {
|
|
63
61
|
if (typeof this.callback === 'function') {
|
|
64
62
|
this.callback(mediaQuery.matches);
|
|
65
63
|
}
|
|
66
|
-
}
|
|
64
|
+
};
|
|
67
65
|
}
|
|
@@ -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,
|
|
@@ -10,6 +11,15 @@
|
|
|
10
11
|
* where content is overflowing. Supported values are: `top`, `bottom`, `start`, `end`.
|
|
11
12
|
*/
|
|
12
13
|
export class OverflowController {
|
|
14
|
+
/** @type {ResizeObserver} */
|
|
15
|
+
#resizeObserver;
|
|
16
|
+
|
|
17
|
+
/** @type {MutationObserver} */
|
|
18
|
+
#childObserver;
|
|
19
|
+
|
|
20
|
+
/** @type {number} */
|
|
21
|
+
#resizeRaf;
|
|
22
|
+
|
|
13
23
|
constructor(host, scrollTarget) {
|
|
14
24
|
/**
|
|
15
25
|
* The controller host element.
|
|
@@ -25,9 +35,6 @@ export class OverflowController {
|
|
|
25
35
|
* @type {HTMLElement}
|
|
26
36
|
*/
|
|
27
37
|
this.scrollTarget = scrollTarget || host;
|
|
28
|
-
|
|
29
|
-
/** @private */
|
|
30
|
-
this.__boundOnScroll = this.__onScroll.bind(this);
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
hostConnected() {
|
|
@@ -46,64 +53,60 @@ export class OverflowController {
|
|
|
46
53
|
observe() {
|
|
47
54
|
const { host } = this;
|
|
48
55
|
|
|
49
|
-
this
|
|
50
|
-
this.
|
|
56
|
+
this.#resizeObserver = new ResizeObserver(() => this.#onResize());
|
|
57
|
+
this.#resizeObserver.observe(host);
|
|
51
58
|
|
|
52
59
|
// Observe initial children
|
|
53
60
|
[...host.children].forEach((child) => {
|
|
54
|
-
this.
|
|
61
|
+
this.#resizeObserver.observe(child);
|
|
55
62
|
});
|
|
56
63
|
|
|
57
|
-
this
|
|
64
|
+
this.#childObserver = new MutationObserver((mutations) => {
|
|
58
65
|
mutations.forEach(({ addedNodes, removedNodes }) => {
|
|
59
66
|
addedNodes.forEach((node) => {
|
|
60
67
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
61
|
-
this.
|
|
68
|
+
this.#resizeObserver.observe(node);
|
|
62
69
|
}
|
|
63
70
|
});
|
|
64
71
|
|
|
65
72
|
removedNodes.forEach((node) => {
|
|
66
73
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
67
|
-
this.
|
|
74
|
+
this.#resizeObserver.unobserve(node);
|
|
68
75
|
}
|
|
69
76
|
});
|
|
70
77
|
|
|
71
78
|
if (addedNodes.length === 0 && removedNodes.length > 0) {
|
|
72
|
-
this
|
|
79
|
+
this.#updateState({ sync: true });
|
|
73
80
|
}
|
|
74
81
|
});
|
|
75
82
|
});
|
|
76
83
|
|
|
77
|
-
this.
|
|
84
|
+
this.#childObserver.observe(host, { childList: true });
|
|
78
85
|
|
|
79
86
|
// Update overflow attribute on scroll
|
|
80
|
-
this.scrollTarget.addEventListener('scroll', this
|
|
87
|
+
this.scrollTarget.addEventListener('scroll', this.#onScroll);
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.__updateState({ sync: false });
|
|
90
|
+
#onResize() {
|
|
91
|
+
this.#updateState({ sync: false });
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
94
|
+
#onScroll = () => {
|
|
95
|
+
this.#updateState({ sync: true });
|
|
96
|
+
};
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
cancelAnimationFrame(this.__resizeRaf);
|
|
98
|
+
#updateState({ sync }) {
|
|
99
|
+
cancelAnimationFrame(this.#resizeRaf);
|
|
96
100
|
|
|
97
|
-
const state = this
|
|
101
|
+
const state = this.#readState();
|
|
98
102
|
if (sync) {
|
|
99
|
-
this
|
|
103
|
+
this.#writeState(state);
|
|
100
104
|
} else {
|
|
101
|
-
this
|
|
105
|
+
this.#resizeRaf = requestAnimationFrame(() => this.#writeState(state));
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
__readState() {
|
|
109
|
+
#readState() {
|
|
107
110
|
const target = this.scrollTarget;
|
|
108
111
|
|
|
109
112
|
let overflow = '';
|
|
@@ -128,12 +131,7 @@ export class OverflowController {
|
|
|
128
131
|
return { overflow: overflow.trim() };
|
|
129
132
|
}
|
|
130
133
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (overflow) {
|
|
134
|
-
this.host.setAttribute('overflow', overflow);
|
|
135
|
-
} else {
|
|
136
|
-
this.host.removeAttribute('overflow');
|
|
137
|
-
}
|
|
134
|
+
#writeState({ overflow }) {
|
|
135
|
+
setOrRemoveAttribute(this.host, 'overflow', overflow);
|
|
138
136
|
}
|
|
139
137
|
}
|
|
@@ -25,4 +25,9 @@ export class SlotChildObserveController extends SlotController {
|
|
|
25
25
|
* Override to update default node text on property change.
|
|
26
26
|
*/
|
|
27
27
|
protected updateDefaultNode(node: Node): void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fire an event to notify the controller host about node changes.
|
|
31
|
+
*/
|
|
32
|
+
protected _notifyChange(node: Node): void;
|
|
28
33
|
}
|
|
@@ -10,6 +10,9 @@ import { SlotController } from './slot-controller.js';
|
|
|
10
10
|
* and the text content, and fires an event to notify host element about those.
|
|
11
11
|
*/
|
|
12
12
|
export class SlotChildObserveController extends SlotController {
|
|
13
|
+
/** @type {MutationObserver} */
|
|
14
|
+
#nodeObserver;
|
|
15
|
+
|
|
13
16
|
constructor(host, slot, tagName, config = {}) {
|
|
14
17
|
super(host, slot, tagName, { ...config, useUniqueId: true });
|
|
15
18
|
}
|
|
@@ -22,8 +25,8 @@ export class SlotChildObserveController extends SlotController {
|
|
|
22
25
|
* @override
|
|
23
26
|
*/
|
|
24
27
|
initCustomNode(node) {
|
|
25
|
-
this
|
|
26
|
-
this.
|
|
28
|
+
this.#updateNodeId(node);
|
|
29
|
+
this._notifyChange(node);
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -39,7 +42,7 @@ export class SlotChildObserveController extends SlotController {
|
|
|
39
42
|
|
|
40
43
|
// Custom node is added to the slot
|
|
41
44
|
if (node && node !== this.defaultNode) {
|
|
42
|
-
this.
|
|
45
|
+
this._notifyChange(node);
|
|
43
46
|
} else {
|
|
44
47
|
this.restoreDefaultNode();
|
|
45
48
|
this.updateDefaultNode(this.node);
|
|
@@ -58,7 +61,7 @@ export class SlotChildObserveController extends SlotController {
|
|
|
58
61
|
const node = super.attachDefaultNode();
|
|
59
62
|
|
|
60
63
|
if (node) {
|
|
61
|
-
this
|
|
64
|
+
this.#updateNodeId(node);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
return node;
|
|
@@ -80,7 +83,7 @@ export class SlotChildObserveController extends SlotController {
|
|
|
80
83
|
* @protected
|
|
81
84
|
*/
|
|
82
85
|
updateDefaultNode(node) {
|
|
83
|
-
this.
|
|
86
|
+
this._notifyChange(node);
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
/**
|
|
@@ -92,11 +95,11 @@ export class SlotChildObserveController extends SlotController {
|
|
|
92
95
|
*/
|
|
93
96
|
observeNode(node) {
|
|
94
97
|
// Stop observing the previous node, if any.
|
|
95
|
-
if (this
|
|
96
|
-
this.
|
|
98
|
+
if (this.#nodeObserver) {
|
|
99
|
+
this.#nodeObserver.disconnect();
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
this
|
|
102
|
+
this.#nodeObserver = new MutationObserver((mutations) => {
|
|
100
103
|
mutations.forEach((mutation) => {
|
|
101
104
|
const target = mutation.target;
|
|
102
105
|
|
|
@@ -108,17 +111,17 @@ export class SlotChildObserveController extends SlotController {
|
|
|
108
111
|
// We use attributeFilter to only observe ID mutation,
|
|
109
112
|
// no need to check for attribute name separately.
|
|
110
113
|
if (isCurrentNodeMutation) {
|
|
111
|
-
this
|
|
114
|
+
this.#updateNodeId(target);
|
|
112
115
|
}
|
|
113
116
|
} else if (isCurrentNodeMutation || target.parentElement === this.node) {
|
|
114
117
|
// Node text content has changed.
|
|
115
|
-
this.
|
|
118
|
+
this._notifyChange(this.node);
|
|
116
119
|
}
|
|
117
120
|
});
|
|
118
121
|
});
|
|
119
122
|
|
|
120
123
|
// Observe changes to node ID attribute, text content and children.
|
|
121
|
-
this.
|
|
124
|
+
this.#nodeObserver.observe(node, {
|
|
122
125
|
attributes: true,
|
|
123
126
|
attributeFilter: ['id'],
|
|
124
127
|
childList: true,
|
|
@@ -133,9 +136,8 @@ export class SlotChildObserveController extends SlotController {
|
|
|
133
136
|
*
|
|
134
137
|
* @param {Node} node
|
|
135
138
|
* @return {boolean}
|
|
136
|
-
* @private
|
|
137
139
|
*/
|
|
138
|
-
|
|
140
|
+
#hasContent(node) {
|
|
139
141
|
if (!node) {
|
|
140
142
|
return false;
|
|
141
143
|
}
|
|
@@ -150,12 +152,12 @@ export class SlotChildObserveController extends SlotController {
|
|
|
150
152
|
* Fire an event to notify the controller host about node changes.
|
|
151
153
|
*
|
|
152
154
|
* @param {Node} node
|
|
153
|
-
* @
|
|
155
|
+
* @protected
|
|
154
156
|
*/
|
|
155
|
-
|
|
157
|
+
_notifyChange(node) {
|
|
156
158
|
this.dispatchEvent(
|
|
157
159
|
new CustomEvent('slot-content-changed', {
|
|
158
|
-
detail: { hasContent: this
|
|
160
|
+
detail: { hasContent: this.#hasContent(node), node },
|
|
159
161
|
}),
|
|
160
162
|
);
|
|
161
163
|
}
|
|
@@ -164,9 +166,8 @@ export class SlotChildObserveController extends SlotController {
|
|
|
164
166
|
* Set default ID on the node in case it is an HTML element.
|
|
165
167
|
*
|
|
166
168
|
* @param {Node} node
|
|
167
|
-
* @private
|
|
168
169
|
*/
|
|
169
|
-
|
|
170
|
+
#updateNodeId(node) {
|
|
170
171
|
// When in multiple mode, only set ID attribute on the element in default slot.
|
|
171
172
|
const isFirstNode = !this.nodes || node === this.nodes[0];
|
|
172
173
|
if (node.nodeType === Node.ELEMENT_NODE && (!this.multiple || isFirstNode) && !node.id) {
|
package/src/slot-controller.js
CHANGED
|
@@ -202,7 +202,8 @@ export class SlotController extends EventTarget {
|
|
|
202
202
|
const selector = slotName === '' ? 'slot:not([name])' : `slot[name=${slotName}]`;
|
|
203
203
|
const slot = this.host.shadowRoot.querySelector(selector);
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
// eslint-disable-next-line no-new
|
|
206
|
+
new SlotObserver(slot, ({ addedNodes, removedNodes }) => {
|
|
206
207
|
const current = this.multiple ? this.nodes : [this.node];
|
|
207
208
|
|
|
208
209
|
// Calling `slot.assignedNodes()` includes whitespace text nodes in case of default slot:
|
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
|
}
|
|
@@ -4,7 +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 { css } from 'lit';
|
|
7
|
-
import { addGlobalStyles } from '
|
|
7
|
+
import { addGlobalStyles, registerCSSProperty } from '../css-utils.js';
|
|
8
8
|
|
|
9
9
|
// NOTE: Base color CSS custom properties are explicitly registered as `<color>`
|
|
10
10
|
// here to avoid performance issues in Aura. Aura overrides these properties with
|
|
@@ -18,7 +18,7 @@ import { addGlobalStyles } from './add-global-styles.js';
|
|
|
18
18
|
'--vaadin-border-color-secondary',
|
|
19
19
|
'--vaadin-background-color',
|
|
20
20
|
].forEach((propertyName) => {
|
|
21
|
-
|
|
21
|
+
registerCSSProperty({
|
|
22
22
|
name: propertyName,
|
|
23
23
|
syntax: '<color>',
|
|
24
24
|
inherits: true,
|
|
@@ -83,10 +83,12 @@ addGlobalStyles(
|
|
|
83
83
|
--_vaadin-icon-arrow-up: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>');
|
|
84
84
|
--_vaadin-icon-calendar: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/></svg>');
|
|
85
85
|
--_vaadin-icon-checkmark: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>');
|
|
86
|
+
--_vaadin-icon-checkmark-small: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>');
|
|
86
87
|
--_vaadin-icon-chevron-down: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
|
|
87
88
|
--_vaadin-icon-chevron-right: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>');
|
|
88
89
|
--_vaadin-icon-clock: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 6v6l4 2"/><circle cx="12" cy="12" r="10"/></svg>');
|
|
89
90
|
--_vaadin-icon-cross: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>');
|
|
91
|
+
--_vaadin-icon-cross-small: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>');
|
|
90
92
|
--_vaadin-icon-drag: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M11 7c0 .82843-.6716 1.5-1.5 1.5C8.67157 8.5 8 7.82843 8 7s.67157-1.5 1.5-1.5c.8284 0 1.5.67157 1.5 1.5Zm0 5c0 .8284-.6716 1.5-1.5 1.5-.82843 0-1.5-.6716-1.5-1.5s.67157-1.5 1.5-1.5c.8284 0 1.5.6716 1.5 1.5Zm0 5c0 .8284-.6716 1.5-1.5 1.5-.82843 0-1.5-.6716-1.5-1.5s.67157-1.5 1.5-1.5c.8284 0 1.5.6716 1.5 1.5Zm5-10c0 .82843-.6716 1.5-1.5 1.5S13 7.82843 13 7s.6716-1.5 1.5-1.5S16 6.17157 16 7Zm0 5c0 .8284-.6716 1.5-1.5 1.5S13 12.8284 13 12s.6716-1.5 1.5-1.5 1.5.6716 1.5 1.5Zm0 5c0 .8284-.6716 1.5-1.5 1.5S13 17.8284 13 17s.6716-1.5 1.5-1.5 1.5.6716 1.5 1.5Z" fill="currentColor"/></svg>');
|
|
91
93
|
--_vaadin-icon-ellipsis: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>');
|
|
92
94
|
--_vaadin-icon-eye: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>');
|
|
@@ -4,7 +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 { css } from 'lit';
|
|
7
|
-
import { addGlobalStyles } from '
|
|
7
|
+
import { addGlobalStyles } from '../css-utils.js';
|
|
8
8
|
|
|
9
9
|
addGlobalStyles(
|
|
10
10
|
'vaadin-base-user-colors',
|
|
@@ -14,7 +14,6 @@ export class TooltipController extends SlotController {
|
|
|
14
14
|
super(host, 'tooltip');
|
|
15
15
|
|
|
16
16
|
this.setTarget(host);
|
|
17
|
-
this.__onContentChange = this.__onContentChange.bind(this);
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
/**
|
|
@@ -50,8 +49,8 @@ export class TooltipController extends SlotController {
|
|
|
50
49
|
if (!this.manual) {
|
|
51
50
|
this.host.setAttribute('has-tooltip', '');
|
|
52
51
|
}
|
|
53
|
-
this
|
|
54
|
-
tooltipNode.addEventListener('content-changed', this
|
|
52
|
+
this.#notifyChange(tooltipNode);
|
|
53
|
+
tooltipNode.addEventListener('content-changed', this.#onContentChange);
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
/**
|
|
@@ -65,8 +64,8 @@ export class TooltipController extends SlotController {
|
|
|
65
64
|
if (!this.manual) {
|
|
66
65
|
this.host.removeAttribute('has-tooltip');
|
|
67
66
|
}
|
|
68
|
-
tooltipNode.removeEventListener('content-changed', this
|
|
69
|
-
this
|
|
67
|
+
tooltipNode.removeEventListener('content-changed', this.#onContentChange);
|
|
68
|
+
this.#notifyChange(null);
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
/**
|
|
@@ -179,13 +178,11 @@ export class TooltipController extends SlotController {
|
|
|
179
178
|
}
|
|
180
179
|
}
|
|
181
180
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
181
|
+
#onContentChange = (event) => {
|
|
182
|
+
this.#notifyChange(event.target);
|
|
183
|
+
};
|
|
186
184
|
|
|
187
|
-
|
|
188
|
-
__notifyChange(node) {
|
|
185
|
+
#notifyChange(node) {
|
|
189
186
|
this.dispatchEvent(new CustomEvent('tooltip-changed', { detail: { node } }));
|
|
190
187
|
}
|
|
191
188
|
}
|
|
@@ -78,17 +78,17 @@ export class IronListAdapter {
|
|
|
78
78
|
});
|
|
79
79
|
attachObserver.observe(this.scrollTarget);
|
|
80
80
|
|
|
81
|
-
this.
|
|
82
|
-
this.elementsContainer.addEventListener('focusin', () => {
|
|
83
|
-
this.scrollTarget.dispatchEvent(
|
|
84
|
-
new CustomEvent('virtualizer-element-focused', { detail: { element: this.__getFocusedElement() } }),
|
|
85
|
-
);
|
|
86
|
-
});
|
|
81
|
+
this.elementsContainer.addEventListener('focusin', () => this.__onElementFocused());
|
|
87
82
|
|
|
88
83
|
if (this.reorderElements) {
|
|
89
84
|
// Reordering the physical elements cancels the user's grab of the scroll bar handle on Safari.
|
|
90
85
|
// Need to defer reordering until the user lets go of the scroll bar handle.
|
|
91
|
-
this.scrollTarget.addEventListener('mousedown', () => {
|
|
86
|
+
this.scrollTarget.addEventListener('mousedown', (event) => {
|
|
87
|
+
// Only handle clicks on the scroll target itself
|
|
88
|
+
if (event.target !== this.scrollTarget) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
92
|
this.__mouseDown = true;
|
|
93
93
|
});
|
|
94
94
|
this.scrollTarget.addEventListener('mouseup', () => {
|
|
@@ -156,7 +156,7 @@ export class IronListAdapter {
|
|
|
156
156
|
this.__skipNextVirtualIndexAdjust = true;
|
|
157
157
|
super.scrollToIndex(targetVirtualIndex);
|
|
158
158
|
|
|
159
|
-
if (this.adjustedFirstVisibleIndex !== index && this._scrollTop < this._maxScrollTop
|
|
159
|
+
if (this.adjustedFirstVisibleIndex !== index && this._scrollTop < this._maxScrollTop) {
|
|
160
160
|
// Workaround an iron-list issue by manually adjusting the scroll position
|
|
161
161
|
this._scrollTop -= this.__getIndexScrollOffset(index) || 0;
|
|
162
162
|
}
|
|
@@ -184,9 +184,6 @@ export class IronListAdapter {
|
|
|
184
184
|
if (this.__scrollReorderDebouncer) {
|
|
185
185
|
this.__scrollReorderDebouncer.flush();
|
|
186
186
|
}
|
|
187
|
-
if (this.__debouncerWheelAnimationFrame) {
|
|
188
|
-
this.__debouncerWheelAnimationFrame.flush();
|
|
189
|
-
}
|
|
190
187
|
}
|
|
191
188
|
|
|
192
189
|
hostConnected() {
|
|
@@ -409,6 +406,8 @@ export class IronListAdapter {
|
|
|
409
406
|
requestAnimationFrame(() => this._resizeHandler());
|
|
410
407
|
}
|
|
411
408
|
|
|
409
|
+
this._updateScrollerSize(true);
|
|
410
|
+
|
|
412
411
|
// Re-render items once the scroll position has been restored.
|
|
413
412
|
// This call also updates the cached scrollTarget height and
|
|
414
413
|
// rechecks whether more virtual elements are needed, since the
|
|
@@ -455,16 +454,10 @@ export class IronListAdapter {
|
|
|
455
454
|
|
|
456
455
|
/** @private */
|
|
457
456
|
updateViewportBoundaries() {
|
|
458
|
-
|
|
459
|
-
this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(styles['padding-top'], 10);
|
|
460
|
-
this._isRTL = Boolean(styles.direction === 'rtl');
|
|
461
|
-
this._viewportWidth = this.elementsContainer.offsetWidth;
|
|
457
|
+
this._scrollerPaddingTop = parseInt(window.getComputedStyle(this.scrollTarget)['padding-top'], 10);
|
|
462
458
|
this._viewportHeight = this.scrollTarget.offsetHeight;
|
|
463
459
|
}
|
|
464
460
|
|
|
465
|
-
/** @private */
|
|
466
|
-
setAttribute() {}
|
|
467
|
-
|
|
468
461
|
/** @private */
|
|
469
462
|
_createPool(size) {
|
|
470
463
|
const physicalItems = this.createElements(size);
|
|
@@ -514,20 +507,8 @@ export class IronListAdapter {
|
|
|
514
507
|
toggleScrollListener() {}
|
|
515
508
|
|
|
516
509
|
/** @private */
|
|
517
|
-
__getFocusedElement(
|
|
518
|
-
|
|
519
|
-
// focus lives in a nested shadow tree. Descend through nested shadow
|
|
520
|
-
// roots' `activeElement`s to reach the real focused node, then walk up
|
|
521
|
-
// the flattened tree (via `assignedSlot`/`parentNode`/`host`) until a
|
|
522
|
-
// visible row is reached.
|
|
523
|
-
let node = document.activeElement;
|
|
524
|
-
while (node?.shadowRoot?.activeElement) {
|
|
525
|
-
node = node.shadowRoot.activeElement;
|
|
526
|
-
}
|
|
527
|
-
while (node && !visibleElements.includes(node)) {
|
|
528
|
-
node = node.assignedSlot || node.parentNode || node.host;
|
|
529
|
-
}
|
|
530
|
-
return node;
|
|
510
|
+
__getFocusedElement() {
|
|
511
|
+
return this.__getVisibleElements().find((element) => element.matches(':focus-within'));
|
|
531
512
|
}
|
|
532
513
|
|
|
533
514
|
/** @private */
|
|
@@ -551,12 +532,12 @@ export class IronListAdapter {
|
|
|
551
532
|
}
|
|
552
533
|
|
|
553
534
|
/** @private */
|
|
554
|
-
__onElementFocused(
|
|
535
|
+
__onElementFocused() {
|
|
555
536
|
if (!this.reorderElements) {
|
|
556
537
|
return;
|
|
557
538
|
}
|
|
558
539
|
|
|
559
|
-
const focusedElement =
|
|
540
|
+
const focusedElement = this.__getFocusedElement();
|
|
560
541
|
if (!focusedElement) {
|
|
561
542
|
return;
|
|
562
543
|
}
|
|
@@ -798,7 +779,7 @@ export class IronListAdapter {
|
|
|
798
779
|
|
|
799
780
|
// Which row to use as a target?
|
|
800
781
|
const visibleElements = this.__getVisibleElements();
|
|
801
|
-
const targetElement = this.__getFocusedElement(
|
|
782
|
+
const targetElement = this.__getFocusedElement() || visibleElements[0];
|
|
802
783
|
if (!targetElement) {
|
|
803
784
|
// All elements are hidden, don't reorder
|
|
804
785
|
return;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright (c) 2025 - 2026 Vaadin Ltd.
|
|
4
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Add a `<style>` block with given styles to the document.
|
|
9
|
-
*
|
|
10
|
-
* @param {string} id the id to set on the created element, only for informational purposes
|
|
11
|
-
* @param {CSSResultGroup[]} styles the styles to add
|
|
12
|
-
*/
|
|
13
|
-
export const addGlobalStyles = (id, ...styles) => {
|
|
14
|
-
const styleTag = document.createElement('style');
|
|
15
|
-
styleTag.id = id;
|
|
16
|
-
styleTag.textContent = styles.map((style) => style.toString()).join('\n');
|
|
17
|
-
|
|
18
|
-
document.head.insertAdjacentElement('afterbegin', styleTag);
|
|
19
|
-
};
|