@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.
Files changed (65) hide show
  1. package/index.d.ts +0 -1
  2. package/index.js +0 -1
  3. package/package.json +2 -2
  4. package/src/a11y-announcer.d.ts +1 -1
  5. package/src/a11y-announcer.js +1 -1
  6. package/src/active-mixin.d.ts +1 -1
  7. package/src/active-mixin.js +1 -1
  8. package/src/browser-utils.js +7 -7
  9. package/src/controller-mixin.d.ts +1 -1
  10. package/src/controller-mixin.js +1 -1
  11. package/src/delegate-focus-mixin.d.ts +48 -0
  12. package/src/delegate-focus-mixin.js +228 -0
  13. package/src/delegate-state-mixin.d.ts +20 -0
  14. package/src/delegate-state-mixin.js +125 -0
  15. package/src/dir-mixin.d.ts +2 -4
  16. package/src/dir-mixin.js +7 -29
  17. package/src/dir-utils.d.ts +19 -0
  18. package/src/dir-utils.js +36 -0
  19. package/src/disabled-mixin.d.ts +1 -1
  20. package/src/disabled-mixin.js +1 -1
  21. package/src/dom-utils.d.ts +1 -1
  22. package/src/dom-utils.js +1 -1
  23. package/src/element-mixin.d.ts +1 -1
  24. package/src/element-mixin.js +11 -5
  25. package/src/focus-mixin.d.ts +1 -1
  26. package/src/focus-mixin.js +1 -1
  27. package/src/focus-trap-controller.d.ts +1 -1
  28. package/src/focus-trap-controller.js +22 -22
  29. package/src/focus-utils.d.ts +1 -1
  30. package/src/focus-utils.js +1 -1
  31. package/src/gestures.js +1 -1
  32. package/src/iron-list-core.js +32 -12
  33. package/src/keyboard-direction-mixin.d.ts +1 -1
  34. package/src/keyboard-direction-mixin.js +12 -12
  35. package/src/keyboard-mixin.d.ts +1 -1
  36. package/src/keyboard-mixin.js +1 -1
  37. package/src/list-mixin.d.ts +50 -0
  38. package/src/list-mixin.js +343 -0
  39. package/src/media-query-controller.d.ts +1 -1
  40. package/src/media-query-controller.js +1 -1
  41. package/src/overflow-controller.d.ts +1 -1
  42. package/src/overflow-controller.js +3 -3
  43. package/src/overlay-class-mixin.d.ts +33 -0
  44. package/src/overlay-class-mixin.js +79 -0
  45. package/src/polylit-mixin.d.ts +1 -1
  46. package/src/polylit-mixin.js +9 -4
  47. package/src/resize-mixin.d.ts +1 -1
  48. package/src/resize-mixin.js +11 -21
  49. package/src/slot-child-observe-controller.d.ts +28 -0
  50. package/src/slot-child-observe-controller.js +176 -0
  51. package/src/slot-controller.d.ts +33 -5
  52. package/src/slot-controller.js +103 -40
  53. package/src/tabindex-mixin.d.ts +1 -1
  54. package/src/tabindex-mixin.js +1 -1
  55. package/src/templates.js +1 -1
  56. package/src/tooltip-controller.d.ts +1 -1
  57. package/src/tooltip-controller.js +1 -1
  58. package/src/unique-id-utils.d.ts +1 -1
  59. package/src/unique-id-utils.js +1 -1
  60. package/src/virtualizer-iron-list-adapter.js +67 -9
  61. package/src/virtualizer.js +18 -18
  62. package/src/dir-helper.d.ts +0 -42
  63. package/src/dir-helper.js +0 -93
  64. package/src/slot-mixin.d.ts +0 -18
  65. 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
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
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
- slotFactory?: () => HTMLElement,
29
- slotInitializer?: (host: HTMLElement, node: HTMLElement) => void,
30
- useUniqueId?: boolean,
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 observe(): void;
85
+ protected observeSlot(): void;
58
86
  }
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
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(slotName, host) {
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, slotFactory, slotInitializer, useUniqueId) {
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.slotFactory = slotFactory;
32
- this.slotInitializer = slotInitializer;
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 = SlotController.generateId(slotName, host);
45
+ this.defaultId = this.constructor.generateId(host, slotName);
37
46
  }
38
47
  }
39
48
 
40
49
  hostConnected() {
41
50
  if (!this.initialized) {
42
- let node = this.getSlotChild();
43
-
44
- if (!node) {
45
- node = this.attachDefaultNode();
51
+ if (this.multiple) {
52
+ this.initMultiple();
46
53
  } else {
47
- this.node = node;
48
- this.initCustomNode(node);
54
+ this.initSingle();
49
55
  }
50
56
 
51
- this.initNode(node);
52
-
53
- // TODO: Consider making this behavior opt-in to improve performance.
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 slot factory.
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, slotFactory } = this;
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
- // Slot factory is optional, some slots don't have default content.
72
- if (!node && slotFactory) {
73
- node = slotFactory(host);
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 a reference to the node managed by the controller.
125
+ * Return the list of nodes matching the slot managed by the controller.
92
126
  * @return {Node}
93
127
  */
94
- getSlotChild() {
128
+ getSlotChildren() {
95
129
  const { slotName } = this;
96
- return Array.from(this.host.childNodes).find((node) => {
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, node);
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
- observe() {
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
- // TODO: support default slot with multiple nodes (e.g. confirm-dialog)
145
- const current = this.node;
146
- const newNode = info.addedNodes.find((node) => node !== current);
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 (newNode) {
213
+ if (newNodes && newNodes.length > 0) {
155
214
  // Custom node is added, remove the current one.
156
- if (current && current.isConnected) {
157
- this.host.removeChild(current);
158
- }
159
-
160
- this.node = newNode;
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
- this.initNode(newNode);
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
  });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
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';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
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 { DisabledMixin } from './disabled-mixin.js';
package/src/templates.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
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
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2022 Vaadin Ltd.
3
+ * Copyright (c) 2022 - 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 { SlotController } from './slot-controller.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2022 Vaadin Ltd.
3
+ * Copyright (c) 2022 - 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 { SlotController } from './slot-controller.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
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
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
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
 
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
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
- // add some scroll offset to enable the user to continue scrolling.
359
- if (this._scrollTop === 0 && this.firstVisibleIndex !== 0) {
360
- this._scrollTop = 1;
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
- this._deltaYAcc = this._deltaYAcc || 0;
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