@vaadin/component-base 24.0.0-alpha1 → 24.0.0-alpha10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +0 -1
- package/index.js +0 -1
- package/package.json +2 -2
- package/src/a11y-announcer.d.ts +1 -1
- package/src/a11y-announcer.js +1 -1
- package/src/active-mixin.d.ts +1 -1
- package/src/active-mixin.js +1 -1
- package/src/browser-utils.js +7 -7
- package/src/controller-mixin.d.ts +1 -1
- package/src/controller-mixin.js +1 -1
- package/src/delegate-focus-mixin.d.ts +48 -0
- package/src/delegate-focus-mixin.js +228 -0
- package/src/delegate-state-mixin.d.ts +20 -0
- package/src/delegate-state-mixin.js +125 -0
- package/src/dir-mixin.d.ts +2 -4
- package/src/dir-mixin.js +7 -29
- package/src/dir-utils.d.ts +19 -0
- package/src/dir-utils.js +36 -0
- package/src/disabled-mixin.d.ts +1 -1
- package/src/disabled-mixin.js +1 -1
- package/src/dom-utils.d.ts +1 -1
- package/src/dom-utils.js +1 -1
- package/src/element-mixin.d.ts +1 -1
- package/src/element-mixin.js +11 -5
- package/src/focus-mixin.d.ts +1 -1
- package/src/focus-mixin.js +1 -1
- package/src/focus-trap-controller.d.ts +1 -1
- package/src/focus-trap-controller.js +22 -22
- package/src/focus-utils.d.ts +1 -1
- package/src/focus-utils.js +1 -1
- package/src/gestures.js +1 -1
- package/src/iron-list-core.js +32 -12
- package/src/keyboard-direction-mixin.d.ts +1 -1
- package/src/keyboard-direction-mixin.js +12 -12
- package/src/keyboard-mixin.d.ts +1 -1
- package/src/keyboard-mixin.js +1 -1
- package/src/list-mixin.d.ts +50 -0
- package/src/list-mixin.js +343 -0
- package/src/media-query-controller.d.ts +1 -1
- package/src/media-query-controller.js +1 -1
- package/src/overflow-controller.d.ts +1 -1
- package/src/overflow-controller.js +3 -3
- package/src/overlay-class-mixin.d.ts +33 -0
- package/src/overlay-class-mixin.js +79 -0
- package/src/polylit-mixin.d.ts +1 -1
- package/src/polylit-mixin.js +9 -4
- package/src/resize-mixin.d.ts +1 -1
- package/src/resize-mixin.js +11 -21
- package/src/slot-child-observe-controller.d.ts +28 -0
- package/src/slot-child-observe-controller.js +176 -0
- package/src/slot-controller.d.ts +33 -5
- package/src/slot-controller.js +103 -40
- package/src/tabindex-mixin.d.ts +1 -1
- package/src/tabindex-mixin.js +1 -1
- package/src/templates.js +1 -1
- package/src/tooltip-controller.d.ts +1 -1
- package/src/tooltip-controller.js +1 -1
- package/src/unique-id-utils.d.ts +1 -1
- package/src/unique-id-utils.js +1 -1
- package/src/virtualizer-iron-list-adapter.js +67 -9
- package/src/virtualizer.js +18 -18
- package/src/dir-helper.d.ts +0 -42
- package/src/dir-helper.js +0 -93
- package/src/slot-mixin.d.ts +0 -18
- package/src/slot-mixin.js +0 -60
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2022 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { SlotController } from './slot-controller.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A controller that observes slotted element mutations, especially ID attribute
|
|
10
|
+
* and the text content, and fires an event to notify host element about those.
|
|
11
|
+
*/
|
|
12
|
+
export class SlotChildObserveController extends SlotController {
|
|
13
|
+
constructor(host, slot, tagName, config = {}) {
|
|
14
|
+
super(host, slot, tagName, { ...config, useUniqueId: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Override to initialize the newly added custom node.
|
|
19
|
+
*
|
|
20
|
+
* @param {Node} node
|
|
21
|
+
* @protected
|
|
22
|
+
* @override
|
|
23
|
+
*/
|
|
24
|
+
initCustomNode(node) {
|
|
25
|
+
this.__updateNodeId(node);
|
|
26
|
+
this.__notifyChange(node);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Override to notify the controller host about removal of
|
|
31
|
+
* the custom node, and to apply the default one if needed.
|
|
32
|
+
*
|
|
33
|
+
* @param {Node} _node
|
|
34
|
+
* @protected
|
|
35
|
+
* @override
|
|
36
|
+
*/
|
|
37
|
+
teardownNode(_node) {
|
|
38
|
+
const node = this.getSlotChild();
|
|
39
|
+
|
|
40
|
+
// Custom node is added to the slot
|
|
41
|
+
if (node && node !== this.defaultNode) {
|
|
42
|
+
this.__notifyChange(node);
|
|
43
|
+
} else {
|
|
44
|
+
this.restoreDefaultNode();
|
|
45
|
+
this.updateDefaultNode(this.node);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Override method inherited from `SlotMixin`
|
|
51
|
+
* to set ID attribute on the default node.
|
|
52
|
+
*
|
|
53
|
+
* @return {Node}
|
|
54
|
+
* @protected
|
|
55
|
+
* @override
|
|
56
|
+
*/
|
|
57
|
+
attachDefaultNode() {
|
|
58
|
+
const node = super.attachDefaultNode();
|
|
59
|
+
|
|
60
|
+
if (node) {
|
|
61
|
+
this.__updateNodeId(node);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return node;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Override to restore default node when a custom one is removed.
|
|
69
|
+
*
|
|
70
|
+
* @protected
|
|
71
|
+
*/
|
|
72
|
+
restoreDefaultNode() {
|
|
73
|
+
// To be implemented
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Override to update default node text on property change.
|
|
78
|
+
*
|
|
79
|
+
* @param {Node} node
|
|
80
|
+
* @protected
|
|
81
|
+
*/
|
|
82
|
+
updateDefaultNode(node) {
|
|
83
|
+
this.__notifyChange(node);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Setup the mutation observer on the node to update ID and notify host.
|
|
88
|
+
* Node doesn't get observed automatically until this method is called.
|
|
89
|
+
*
|
|
90
|
+
* @param {Node} node
|
|
91
|
+
* @protected
|
|
92
|
+
*/
|
|
93
|
+
observeNode(node) {
|
|
94
|
+
// Stop observing the previous node, if any.
|
|
95
|
+
if (this.__nodeObserver) {
|
|
96
|
+
this.__nodeObserver.disconnect();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.__nodeObserver = new MutationObserver((mutations) => {
|
|
100
|
+
mutations.forEach((mutation) => {
|
|
101
|
+
const target = mutation.target;
|
|
102
|
+
|
|
103
|
+
// Ensure the mutation target is the currently connected node
|
|
104
|
+
// to ignore async mutations dispatched for removed element.
|
|
105
|
+
const isCurrentNodeMutation = target === this.node;
|
|
106
|
+
|
|
107
|
+
if (mutation.type === 'attributes') {
|
|
108
|
+
// We use attributeFilter to only observe ID mutation,
|
|
109
|
+
// no need to check for attribute name separately.
|
|
110
|
+
if (isCurrentNodeMutation) {
|
|
111
|
+
this.__updateNodeId(target);
|
|
112
|
+
}
|
|
113
|
+
} else if (isCurrentNodeMutation || target.parentElement === this.node) {
|
|
114
|
+
// Node text content has changed.
|
|
115
|
+
this.__notifyChange(this.node);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Observe changes to node ID attribute, text content and children.
|
|
121
|
+
this.__nodeObserver.observe(node, {
|
|
122
|
+
attributes: true,
|
|
123
|
+
attributeFilter: ['id'],
|
|
124
|
+
childList: true,
|
|
125
|
+
subtree: true,
|
|
126
|
+
characterData: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Returns true if a node is an HTML element with children,
|
|
132
|
+
* or is a defined custom element, or has non-empty text.
|
|
133
|
+
*
|
|
134
|
+
* @param {Node} node
|
|
135
|
+
* @return {boolean}
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
__hasContent(node) {
|
|
139
|
+
if (!node) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
(node.nodeType === Node.ELEMENT_NODE && (customElements.get(node.localName) || node.children.length > 0)) ||
|
|
145
|
+
(node.textContent && node.textContent.trim() !== '')
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Fire an event to notify the controller host about node changes.
|
|
151
|
+
*
|
|
152
|
+
* @param {Node} node
|
|
153
|
+
* @private
|
|
154
|
+
*/
|
|
155
|
+
__notifyChange(node) {
|
|
156
|
+
this.dispatchEvent(
|
|
157
|
+
new CustomEvent('slot-content-changed', {
|
|
158
|
+
detail: { hasContent: this.__hasContent(node), node },
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Set default ID on the node in case it is an HTML element.
|
|
165
|
+
*
|
|
166
|
+
* @param {Node} node
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
__updateNodeId(node) {
|
|
170
|
+
// When in multiple mode, only set ID attribute on the element in default slot.
|
|
171
|
+
const isFirstNode = !this.nodes || node === this.nodes[0];
|
|
172
|
+
if (node.nodeType === Node.ELEMENT_NODE && isFirstNode && !node.id) {
|
|
173
|
+
node.id = this.defaultId;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
package/src/slot-controller.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license
|
|
3
|
-
* Copyright (c) 2021 -
|
|
3
|
+
* Copyright (c) 2021 - 2023 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import type { ReactiveController } from 'lit';
|
|
@@ -16,8 +16,16 @@ export class SlotController extends EventTarget implements ReactiveController {
|
|
|
16
16
|
*/
|
|
17
17
|
node: HTMLElement;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The list of slotted nodes managed by the controller.
|
|
21
|
+
* Only used when `multiple` property is set to `true`.
|
|
22
|
+
*/
|
|
23
|
+
nodes: HTMLElement[];
|
|
24
|
+
|
|
19
25
|
protected initialized: boolean;
|
|
20
26
|
|
|
27
|
+
protected multiple: boolean;
|
|
28
|
+
|
|
21
29
|
protected defaultNode: Node;
|
|
22
30
|
|
|
23
31
|
protected defaultId: string;
|
|
@@ -25,20 +33,40 @@ export class SlotController extends EventTarget implements ReactiveController {
|
|
|
25
33
|
constructor(
|
|
26
34
|
host: HTMLElement,
|
|
27
35
|
slotName: string,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
tagName?: string,
|
|
37
|
+
config?: {
|
|
38
|
+
multiple?: boolean;
|
|
39
|
+
observe?: boolean;
|
|
40
|
+
useUniqueId?: boolean;
|
|
41
|
+
initializer?(host: HTMLElement, node: HTMLElement): void;
|
|
42
|
+
},
|
|
31
43
|
);
|
|
32
44
|
|
|
33
45
|
hostConnected(): void;
|
|
34
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Return the list of nodes matching the slot managed by the controller.
|
|
49
|
+
*/
|
|
50
|
+
getSlotChildren(): Node[];
|
|
51
|
+
|
|
35
52
|
/**
|
|
36
53
|
* Return a reference to the node managed by the controller.
|
|
37
54
|
*/
|
|
38
55
|
getSlotChild(): Node;
|
|
39
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Create and attach default node using the provided tag name, if any.
|
|
59
|
+
*/
|
|
40
60
|
protected attachDefaultNode(): Node | undefined;
|
|
41
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Run both `initCustomNode` and `initNode` for a custom slotted node.
|
|
64
|
+
*/
|
|
65
|
+
protected initAddedNode(node: Node): void;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Run `slotInitializer` for the node managed by the controller.
|
|
69
|
+
*/
|
|
42
70
|
protected initNode(node: Node): void;
|
|
43
71
|
|
|
44
72
|
/**
|
|
@@ -54,5 +82,5 @@ export class SlotController extends EventTarget implements ReactiveController {
|
|
|
54
82
|
/**
|
|
55
83
|
* Setup the observer to manage slot content changes.
|
|
56
84
|
*/
|
|
57
|
-
protected
|
|
85
|
+
protected observeSlot(): void;
|
|
58
86
|
}
|
package/src/slot-controller.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license
|
|
3
|
-
* Copyright (c) 2021 -
|
|
3
|
+
* Copyright (c) 2021 - 2023 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
|
|
7
|
+
import { isEmptyTextNode } from './dom-utils.js';
|
|
7
8
|
import { generateUniqueId } from './unique-id-utils.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -13,64 +14,97 @@ export class SlotController extends EventTarget {
|
|
|
13
14
|
/**
|
|
14
15
|
* Ensure that every instance has unique ID.
|
|
15
16
|
*
|
|
16
|
-
* @param {string} slotName
|
|
17
17
|
* @param {HTMLElement} host
|
|
18
|
+
* @param {string} slotName
|
|
18
19
|
* @return {string}
|
|
19
20
|
* @protected
|
|
20
21
|
*/
|
|
21
|
-
static generateId(
|
|
22
|
+
static generateId(host, slotName) {
|
|
22
23
|
const prefix = slotName || 'default';
|
|
23
24
|
return `${prefix}-${host.localName}-${generateUniqueId()}`;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
constructor(host, slotName,
|
|
27
|
+
constructor(host, slotName, tagName, config = {}) {
|
|
27
28
|
super();
|
|
28
29
|
|
|
30
|
+
const { initializer, multiple, observe, useUniqueId } = config;
|
|
31
|
+
|
|
29
32
|
this.host = host;
|
|
30
33
|
this.slotName = slotName;
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
34
|
+
this.tagName = tagName;
|
|
35
|
+
this.observe = typeof observe === 'boolean' ? observe : true;
|
|
36
|
+
this.multiple = typeof multiple === 'boolean' ? multiple : false;
|
|
37
|
+
this.slotInitializer = initializer;
|
|
38
|
+
|
|
39
|
+
if (multiple) {
|
|
40
|
+
this.nodes = [];
|
|
41
|
+
}
|
|
33
42
|
|
|
34
43
|
// Only generate the default ID if requested by the controller.
|
|
35
44
|
if (useUniqueId) {
|
|
36
|
-
this.defaultId =
|
|
45
|
+
this.defaultId = this.constructor.generateId(host, slotName);
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
hostConnected() {
|
|
41
50
|
if (!this.initialized) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!node) {
|
|
45
|
-
node = this.attachDefaultNode();
|
|
51
|
+
if (this.multiple) {
|
|
52
|
+
this.initMultiple();
|
|
46
53
|
} else {
|
|
47
|
-
this.
|
|
48
|
-
this.initCustomNode(node);
|
|
54
|
+
this.initSingle();
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
this.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.observe();
|
|
57
|
+
if (this.observe) {
|
|
58
|
+
this.observeSlot();
|
|
59
|
+
}
|
|
55
60
|
|
|
56
61
|
this.initialized = true;
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
/** @protected */
|
|
66
|
+
initSingle() {
|
|
67
|
+
let node = this.getSlotChild();
|
|
68
|
+
|
|
69
|
+
if (!node) {
|
|
70
|
+
node = this.attachDefaultNode();
|
|
71
|
+
this.initNode(node);
|
|
72
|
+
} else {
|
|
73
|
+
this.node = node;
|
|
74
|
+
this.initAddedNode(node);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @protected */
|
|
79
|
+
initMultiple() {
|
|
80
|
+
const children = this.getSlotChildren();
|
|
81
|
+
|
|
82
|
+
if (children.length === 0) {
|
|
83
|
+
const defaultNode = this.attachDefaultNode();
|
|
84
|
+
this.nodes = [defaultNode];
|
|
85
|
+
this.initNode(defaultNode);
|
|
86
|
+
} else {
|
|
87
|
+
this.nodes = children;
|
|
88
|
+
children.forEach((node) => {
|
|
89
|
+
this.initAddedNode(node);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
/**
|
|
61
|
-
* Create and attach default node using the
|
|
95
|
+
* Create and attach default node using the provided tag name, if any.
|
|
62
96
|
* @return {Node | undefined}
|
|
63
97
|
* @protected
|
|
64
98
|
*/
|
|
65
99
|
attachDefaultNode() {
|
|
66
|
-
const { host, slotName,
|
|
100
|
+
const { host, slotName, tagName } = this;
|
|
67
101
|
|
|
68
102
|
// Check if the node was created previously and if so, reuse it.
|
|
69
103
|
let node = this.defaultNode;
|
|
70
104
|
|
|
71
|
-
//
|
|
72
|
-
if (!node &&
|
|
73
|
-
node =
|
|
105
|
+
// Tag name is optional, sometimes we don't init default content.
|
|
106
|
+
if (!node && tagName) {
|
|
107
|
+
node = document.createElement(tagName);
|
|
74
108
|
if (node instanceof Element) {
|
|
75
109
|
if (slotName !== '') {
|
|
76
110
|
node.setAttribute('slot', slotName);
|
|
@@ -88,12 +122,12 @@ export class SlotController extends EventTarget {
|
|
|
88
122
|
}
|
|
89
123
|
|
|
90
124
|
/**
|
|
91
|
-
* Return
|
|
125
|
+
* Return the list of nodes matching the slot managed by the controller.
|
|
92
126
|
* @return {Node}
|
|
93
127
|
*/
|
|
94
|
-
|
|
128
|
+
getSlotChildren() {
|
|
95
129
|
const { slotName } = this;
|
|
96
|
-
return Array.from(this.host.childNodes).
|
|
130
|
+
return Array.from(this.host.childNodes).filter((node) => {
|
|
97
131
|
// Either an element (any slot) or a text node (only un-named slot).
|
|
98
132
|
return (
|
|
99
133
|
(node.nodeType === Node.ELEMENT_NODE && node.slot === slotName) ||
|
|
@@ -103,6 +137,16 @@ export class SlotController extends EventTarget {
|
|
|
103
137
|
}
|
|
104
138
|
|
|
105
139
|
/**
|
|
140
|
+
* Return a reference to the node managed by the controller.
|
|
141
|
+
* @return {Node}
|
|
142
|
+
*/
|
|
143
|
+
getSlotChild() {
|
|
144
|
+
return this.getSlotChildren()[0];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Run `slotInitializer` for the node managed by the controller.
|
|
149
|
+
*
|
|
106
150
|
* @param {Node} node
|
|
107
151
|
* @protected
|
|
108
152
|
*/
|
|
@@ -111,7 +155,7 @@ export class SlotController extends EventTarget {
|
|
|
111
155
|
// Don't try to bind `this` to initializer (normally it's arrow function).
|
|
112
156
|
// Instead, pass the host as a first argument to access component's state.
|
|
113
157
|
if (slotInitializer) {
|
|
114
|
-
slotInitializer(this.host
|
|
158
|
+
slotInitializer(node, this.host);
|
|
115
159
|
}
|
|
116
160
|
}
|
|
117
161
|
|
|
@@ -131,19 +175,34 @@ export class SlotController extends EventTarget {
|
|
|
131
175
|
*/
|
|
132
176
|
teardownNode(_node) {}
|
|
133
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Run both `initCustomNode` and `initNode` for a custom slotted node.
|
|
180
|
+
*
|
|
181
|
+
* @param {Node} node
|
|
182
|
+
* @protected
|
|
183
|
+
*/
|
|
184
|
+
initAddedNode(node) {
|
|
185
|
+
if (node !== this.defaultNode) {
|
|
186
|
+
this.initCustomNode(node);
|
|
187
|
+
this.initNode(node);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
134
191
|
/**
|
|
135
192
|
* Setup the observer to manage slot content changes.
|
|
136
193
|
* @protected
|
|
137
194
|
*/
|
|
138
|
-
|
|
195
|
+
observeSlot() {
|
|
139
196
|
const { slotName } = this;
|
|
140
197
|
const selector = slotName === '' ? 'slot:not([name])' : `slot[name=${slotName}]`;
|
|
141
198
|
const slot = this.host.shadowRoot.querySelector(selector);
|
|
142
199
|
|
|
143
200
|
this.__slotObserver = new FlattenedNodesObserver(slot, (info) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
201
|
+
const current = this.multiple ? this.nodes : [this.node];
|
|
202
|
+
|
|
203
|
+
// Calling `slot.assignedNodes()` includes whitespace text nodes in case of default slot:
|
|
204
|
+
// unlike comment nodes, they are not filtered out. So we need to manually ignore them.
|
|
205
|
+
const newNodes = info.addedNodes.filter((node) => !isEmptyTextNode(node) && !current.includes(node));
|
|
147
206
|
|
|
148
207
|
if (info.removedNodes.length) {
|
|
149
208
|
info.removedNodes.forEach((node) => {
|
|
@@ -151,18 +210,22 @@ export class SlotController extends EventTarget {
|
|
|
151
210
|
});
|
|
152
211
|
}
|
|
153
212
|
|
|
154
|
-
if (
|
|
213
|
+
if (newNodes && newNodes.length > 0) {
|
|
155
214
|
// Custom node is added, remove the current one.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (newNode !== this.defaultNode) {
|
|
163
|
-
this.initCustomNode(newNode);
|
|
215
|
+
current.forEach((node) => {
|
|
216
|
+
if (node && node.isConnected) {
|
|
217
|
+
node.parentNode.removeChild(node);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
164
220
|
|
|
165
|
-
|
|
221
|
+
if (this.multiple) {
|
|
222
|
+
this.nodes = newNodes;
|
|
223
|
+
newNodes.forEach((node) => {
|
|
224
|
+
this.initAddedNode(node);
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
this.node = newNodes[0];
|
|
228
|
+
this.initAddedNode(this.node);
|
|
166
229
|
}
|
|
167
230
|
}
|
|
168
231
|
});
|
package/src/tabindex-mixin.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license
|
|
3
|
-
* Copyright (c) 2021 -
|
|
3
|
+
* Copyright (c) 2021 - 2023 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import type { Constructor } from '@open-wc/dedupe-mixin';
|
package/src/tabindex-mixin.js
CHANGED
package/src/templates.js
CHANGED
package/src/unique-id-utils.d.ts
CHANGED
package/src/unique-id-utils.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license
|
|
3
|
-
* Copyright (c) 2021 -
|
|
3
|
+
* Copyright (c) 2021 - 2023 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
+
/* eslint-disable @typescript-eslint/member-ordering */
|
|
7
|
+
// https://github.com/vaadin/eslint-config-vaadin/issues/33
|
|
6
8
|
import { animationFrame, timeOut } from './async.js';
|
|
7
9
|
import { isSafari } from './browser-utils.js';
|
|
8
10
|
import { Debouncer, flush } from './debounce.js';
|
|
@@ -34,6 +36,7 @@ export class IronListAdapter {
|
|
|
34
36
|
this.timeouts = {
|
|
35
37
|
SCROLL_REORDER: 500,
|
|
36
38
|
IGNORE_WHEEL: 500,
|
|
39
|
+
FIX_INVALID_ITEM_POSITIONING: 100,
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
this.__resizeObserver = new ResizeObserver(() => this._resizeHandler());
|
|
@@ -121,6 +124,9 @@ export class IronListAdapter {
|
|
|
121
124
|
this._resizeHandler();
|
|
122
125
|
flush();
|
|
123
126
|
this._scrollHandler();
|
|
127
|
+
if (this.__fixInvalidItemPositioningDebouncer) {
|
|
128
|
+
this.__fixInvalidItemPositioningDebouncer.flush();
|
|
129
|
+
}
|
|
124
130
|
if (this.__scrollReorderDebouncer) {
|
|
125
131
|
this.__scrollReorderDebouncer.flush();
|
|
126
132
|
}
|
|
@@ -184,6 +190,14 @@ export class IronListAdapter {
|
|
|
184
190
|
if (size === this.size) {
|
|
185
191
|
return;
|
|
186
192
|
}
|
|
193
|
+
// Cancel active debouncers
|
|
194
|
+
if (this.__fixInvalidItemPositioningDebouncer) {
|
|
195
|
+
this.__fixInvalidItemPositioningDebouncer.cancel();
|
|
196
|
+
}
|
|
197
|
+
if (this._debouncers && this._debouncers._increasePoolIfNeeded) {
|
|
198
|
+
// Avoid creating unnecessary elements on the following flush()
|
|
199
|
+
this._debouncers._increasePoolIfNeeded.cancel();
|
|
200
|
+
}
|
|
187
201
|
|
|
188
202
|
// Prevent element update while the scroll position is being restored
|
|
189
203
|
this.__preventElementUpdates = true;
|
|
@@ -199,10 +213,6 @@ export class IronListAdapter {
|
|
|
199
213
|
// Change the size
|
|
200
214
|
this.__size = size;
|
|
201
215
|
|
|
202
|
-
// Flush before invoking items change to avoid
|
|
203
|
-
// creating excess elements on the following flush()
|
|
204
|
-
flush();
|
|
205
|
-
|
|
206
216
|
this._itemsChanged({
|
|
207
217
|
path: 'items',
|
|
208
218
|
});
|
|
@@ -344,6 +354,15 @@ export class IronListAdapter {
|
|
|
344
354
|
}
|
|
345
355
|
}
|
|
346
356
|
|
|
357
|
+
if (delta) {
|
|
358
|
+
// There was a change in scroll top. Schedule a check for invalid item positioning.
|
|
359
|
+
this.__fixInvalidItemPositioningDebouncer = Debouncer.debounce(
|
|
360
|
+
this.__fixInvalidItemPositioningDebouncer,
|
|
361
|
+
timeOut.after(this.timeouts.FIX_INVALID_ITEM_POSITIONING),
|
|
362
|
+
() => this.__fixInvalidItemPositioning(),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
347
366
|
if (this.reorderElements) {
|
|
348
367
|
this.__scrollReorderDebouncer = Debouncer.debounce(
|
|
349
368
|
this.__scrollReorderDebouncer,
|
|
@@ -355,9 +374,46 @@ export class IronListAdapter {
|
|
|
355
374
|
this.__previousScrollTop = this._scrollTop;
|
|
356
375
|
|
|
357
376
|
// If the first visible index is not 0 when scrolled to the top,
|
|
358
|
-
//
|
|
359
|
-
if (this._scrollTop === 0 && this.firstVisibleIndex !== 0) {
|
|
360
|
-
this.
|
|
377
|
+
// scroll to index 0 to fix the issue.
|
|
378
|
+
if (this._scrollTop === 0 && this.firstVisibleIndex !== 0 && Math.abs(delta) > 0) {
|
|
379
|
+
this.scrollToIndex(0);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Work around an iron-list issue with invalid item positioning.
|
|
385
|
+
* See https://github.com/vaadin/flow-components/issues/4306
|
|
386
|
+
* @private
|
|
387
|
+
*/
|
|
388
|
+
__fixInvalidItemPositioning() {
|
|
389
|
+
if (!this.scrollTarget.isConnected) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Check if the first physical item element is below the top of the viewport
|
|
394
|
+
const physicalTopBelowTop = this._physicalTop > this._scrollTop;
|
|
395
|
+
// Check if the last physical item element is above the bottom of the viewport
|
|
396
|
+
const physicalBottomAboveBottom = this._physicalBottom < this._scrollBottom;
|
|
397
|
+
|
|
398
|
+
// Check if the first index is visible
|
|
399
|
+
const firstIndexVisible = this.adjustedFirstVisibleIndex === 0;
|
|
400
|
+
// Check if the last index is visible
|
|
401
|
+
const lastIndexVisible = this.adjustedLastVisibleIndex === this.size - 1;
|
|
402
|
+
|
|
403
|
+
if ((physicalTopBelowTop && !firstIndexVisible) || (physicalBottomAboveBottom && !lastIndexVisible)) {
|
|
404
|
+
// Invalid state! Try to recover.
|
|
405
|
+
|
|
406
|
+
const isScrollingDown = physicalBottomAboveBottom;
|
|
407
|
+
// Set the "_ratio" property temporarily to 0 to make iron-list's _getReusables
|
|
408
|
+
// place all the free physical items on one side of the viewport.
|
|
409
|
+
const originalRatio = this._ratio;
|
|
410
|
+
this._ratio = 0;
|
|
411
|
+
// Fake a scroll change to make _scrollHandler place the physical items
|
|
412
|
+
// on the desired side.
|
|
413
|
+
this._scrollPosition = this._scrollTop + (isScrollingDown ? -1 : 1);
|
|
414
|
+
this._scrollHandler();
|
|
415
|
+
// Restore the original "_ratio" value.
|
|
416
|
+
this._ratio = originalRatio;
|
|
361
417
|
}
|
|
362
418
|
}
|
|
363
419
|
|
|
@@ -376,7 +432,9 @@ export class IronListAdapter {
|
|
|
376
432
|
deltaY *= this._scrollPageHeight;
|
|
377
433
|
}
|
|
378
434
|
|
|
379
|
-
|
|
435
|
+
if (!this._deltaYAcc) {
|
|
436
|
+
this._deltaYAcc = 0;
|
|
437
|
+
}
|
|
380
438
|
|
|
381
439
|
if (this._wheelAnimationFrame) {
|
|
382
440
|
// Accumulate wheel delta while a frame is being processed
|