@vaadin/component-base 24.2.0-dev.f254716fe → 24.3.0-alpha2

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.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+
7
+ /**
8
+ * A helper for observing slot changes.
9
+ */
10
+ export class SlotObserver {
11
+ constructor(
12
+ slot: HTMLSlotElement,
13
+ callback: (info: { addedNodes: Node[]; movedNodes: Node[]; removedNodes: Node[] }) => void,
14
+ );
15
+
16
+ /**
17
+ * Activates an observer. This method is automatically called when
18
+ * a `SlotObserver` is created. It should only be called to re-activate
19
+ * an observer that has been deactivated via the `disconnect` method.
20
+ */
21
+ connect(): void;
22
+
23
+ /**
24
+ * Deactivates the observer. After calling this method the observer callback
25
+ * will not be called when changes to slotted nodes occur. The `connect` method
26
+ * may be subsequently called to reactivate the observer.
27
+ */
28
+ disconnect(): void;
29
+
30
+ /**
31
+ * Run the observer callback synchronously.
32
+ */
33
+ flush(): void;
34
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+
7
+ /**
8
+ * A helper for observing slot changes.
9
+ */
10
+ export class SlotObserver {
11
+ constructor(slot, callback) {
12
+ /** @type HTMLSlotElement */
13
+ this.slot = slot;
14
+
15
+ /** @type Function */
16
+ this.callback = callback;
17
+
18
+ /** @type {Node[]} */
19
+ this._storedNodes = [];
20
+
21
+ this._connected = false;
22
+ this._scheduled = false;
23
+
24
+ this._boundSchedule = () => {
25
+ this._schedule();
26
+ };
27
+
28
+ this.connect();
29
+ this._schedule();
30
+ }
31
+
32
+ /**
33
+ * Activates an observer. This method is automatically called when
34
+ * a `SlotObserver` is created. It should only be called to re-activate
35
+ * an observer that has been deactivated via the `disconnect` method.
36
+ */
37
+ connect() {
38
+ this.slot.addEventListener('slotchange', this._boundSchedule);
39
+ this._connected = true;
40
+ }
41
+
42
+ /**
43
+ * Deactivates the observer. After calling this method the observer callback
44
+ * will not be called when changes to slotted nodes occur. The `connect` method
45
+ * may be subsequently called to reactivate the observer.
46
+ */
47
+ disconnect() {
48
+ this.slot.removeEventListener('slotchange', this._boundSchedule);
49
+ this._connected = false;
50
+ }
51
+
52
+ /** @private */
53
+ _schedule() {
54
+ if (!this._scheduled) {
55
+ this._scheduled = true;
56
+
57
+ queueMicrotask(() => {
58
+ this.flush();
59
+ });
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Run the observer callback synchronously.
65
+ */
66
+ flush() {
67
+ if (!this._connected) {
68
+ return;
69
+ }
70
+
71
+ this._scheduled = false;
72
+
73
+ this._processNodes();
74
+ }
75
+
76
+ /** @private */
77
+ _processNodes() {
78
+ const currentNodes = this.slot.assignedNodes({ flatten: true });
79
+
80
+ let addedNodes = [];
81
+ const removedNodes = [];
82
+ const movedNodes = [];
83
+
84
+ if (currentNodes.length) {
85
+ addedNodes = currentNodes.filter((node) => !this._storedNodes.includes(node));
86
+ }
87
+
88
+ if (this._storedNodes.length) {
89
+ this._storedNodes.forEach((node, index) => {
90
+ const idx = currentNodes.indexOf(node);
91
+ if (idx === -1) {
92
+ removedNodes.push(node);
93
+ } else if (idx !== index) {
94
+ movedNodes.push(node);
95
+ }
96
+ });
97
+ }
98
+
99
+ if (addedNodes.length || removedNodes.length || movedNodes.length) {
100
+ this.callback({ addedNodes, movedNodes, removedNodes });
101
+ }
102
+
103
+ this._storedNodes = currentNodes;
104
+ }
105
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import type { Constructor } from '@open-wc/dedupe-mixin';
7
+
8
+ /**
9
+ * Mixin to insert styles into the outer scope to handle slotted components.
10
+ * This is useful e.g. to hide native `<input type="number">` controls.
11
+ */
12
+ export declare function SlotStylesMixin<T extends Constructor<HTMLElement>>(
13
+ base: T,
14
+ ): Constructor<SlotStylesMixinClass> & T;
15
+
16
+ export declare class SlotStylesMixinClass {
17
+ /**
18
+ * List of styles to insert into root.
19
+ */
20
+ protected readonly slotStyles: string[];
21
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
7
+
8
+ const stylesMap = new WeakMap();
9
+
10
+ /**
11
+ * Get all the styles inserted into root.
12
+ * @param {DocumentOrShadowRoot} root
13
+ * @return {Set<string>}
14
+ */
15
+ function getRootStyles(root) {
16
+ if (!stylesMap.has(root)) {
17
+ stylesMap.set(root, new Set());
18
+ }
19
+
20
+ return stylesMap.get(root);
21
+ }
22
+
23
+ /**
24
+ * Insert styles into the root.
25
+ * @param {string} styles
26
+ * @param {DocumentOrShadowRoot} root
27
+ */
28
+ function insertStyles(styles, root) {
29
+ const style = document.createElement('style');
30
+ style.textContent = styles;
31
+
32
+ if (root === document) {
33
+ document.head.appendChild(style);
34
+ } else {
35
+ root.insertBefore(style, root.firstChild);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Mixin to insert styles into the outer scope to handle slotted components.
41
+ * This is useful e.g. to hide native `<input type="number">` controls.
42
+ *
43
+ * @polymerMixin
44
+ */
45
+ export const SlotStylesMixin = dedupingMixin(
46
+ (superclass) =>
47
+ class SlotStylesMixinClass extends superclass {
48
+ /**
49
+ * List of styles to insert into root.
50
+ * @protected
51
+ */
52
+ get slotStyles() {
53
+ return {};
54
+ }
55
+
56
+ /** @protected */
57
+ connectedCallback() {
58
+ super.connectedCallback();
59
+
60
+ this.__applySlotStyles();
61
+ }
62
+
63
+ /** @private */
64
+ __applySlotStyles() {
65
+ const root = this.getRootNode();
66
+ const rootStyles = getRootStyles(root);
67
+
68
+ this.slotStyles.forEach((styles) => {
69
+ if (!rootStyles.has(styles)) {
70
+ insertStyles(styles, root);
71
+ rootStyles.add(styles);
72
+ }
73
+ });
74
+ }
75
+ },
76
+ );
@@ -23,6 +23,13 @@ type TooltipPosition =
23
23
  * A controller that manages the slotted tooltip element.
24
24
  */
25
25
  export class TooltipController extends SlotController {
26
+ /**
27
+ * An HTML element for linking with the tooltip overlay
28
+ * via `aria-describedby` attribute used by screen readers.
29
+ * When not set, defaults to `target`.
30
+ */
31
+ ariaTarget: HTMLElement | HTMLElement[];
32
+
26
33
  /**
27
34
  * Object with properties passed to `generator`
28
35
  * function to be used for generating tooltip text.
@@ -51,6 +58,12 @@ export class TooltipController extends SlotController {
51
58
  */
52
59
  target: HTMLElement;
53
60
 
61
+ /**
62
+ * Set an HTML element for linking with the tooltip overlay
63
+ * via `aria-describedby` attribute used by screen readers.
64
+ */
65
+ setAriaTarget(ariaTarget: HTMLElement | HTMLElement[]): void;
66
+
54
67
  /**
55
68
  * Set a context object to be used by generator.
56
69
  */
@@ -26,6 +26,10 @@ export class TooltipController extends SlotController {
26
26
  initCustomNode(tooltipNode) {
27
27
  tooltipNode.target = this.target;
28
28
 
29
+ if (this.ariaTarget !== undefined) {
30
+ tooltipNode.ariaTarget = this.ariaTarget;
31
+ }
32
+
29
33
  if (this.context !== undefined) {
30
34
  tooltipNode.context = this.context;
31
35
  }
@@ -45,6 +49,33 @@ export class TooltipController extends SlotController {
45
49
  if (this.shouldShow !== undefined) {
46
50
  tooltipNode.shouldShow = this.shouldShow;
47
51
  }
52
+
53
+ this.__notifyChange();
54
+ }
55
+
56
+ /**
57
+ * Override to notify the host when the tooltip is removed.
58
+ *
59
+ * @param {Node} tooltipNode
60
+ * @protected
61
+ * @override
62
+ */
63
+ teardownNode() {
64
+ this.__notifyChange();
65
+ }
66
+
67
+ /**
68
+ * Set an HTML element for linking with the tooltip overlay
69
+ * via `aria-describedby` attribute used by screen readers.
70
+ * @param {HTMLElement} ariaTarget
71
+ */
72
+ setAriaTarget(ariaTarget) {
73
+ this.ariaTarget = ariaTarget;
74
+
75
+ const tooltipNode = this.node;
76
+ if (tooltipNode) {
77
+ tooltipNode.ariaTarget = ariaTarget;
78
+ }
48
79
  }
49
80
 
50
81
  /**
@@ -127,4 +158,9 @@ export class TooltipController extends SlotController {
127
158
  tooltipNode.target = target;
128
159
  }
129
160
  }
161
+
162
+ /** @private */
163
+ __notifyChange() {
164
+ this.dispatchEvent(new CustomEvent('tooltip-changed', { detail: { node: this.node } }));
165
+ }
130
166
  }
@@ -82,10 +82,24 @@ export class IronListAdapter {
82
82
  return this.lastVisibleIndex + this._vidxOffset;
83
83
  }
84
84
 
85
+ __hasPlaceholders() {
86
+ return this.__getVisibleElements().some((el) => el.__virtualizerPlaceholder);
87
+ }
88
+
85
89
  scrollToIndex(index) {
86
90
  if (typeof index !== 'number' || isNaN(index) || this.size === 0 || !this.scrollTarget.offsetHeight) {
87
91
  return;
88
92
  }
93
+ delete this.__pendingScrollToIndex;
94
+
95
+ if (this._physicalCount <= 3 /* iron-list-core.DEFAULT_PHYSICAL_COUNT */) {
96
+ // The condition here is a performance improvement to avoid an unnecessary
97
+ // re-render when the physical item pool is already covered.
98
+
99
+ // Finish rendering at the current scroll position before scrolling
100
+ this.flush();
101
+ }
102
+
89
103
  index = this._clamp(index, 0, this.size - 1);
90
104
 
91
105
  const visibleElementCount = this.__getVisibleElements().length;
@@ -113,6 +127,12 @@ export class IronListAdapter {
113
127
  this._scrollTop -= this.__getIndexScrollOffset(index) || 0;
114
128
  }
115
129
  this._scrollHandler();
130
+
131
+ if (this.__hasPlaceholders()) {
132
+ // After rendering synchronously, there are still placeholders in the DOM.
133
+ // Try again after the next elements update.
134
+ this.__pendingScrollToIndex = index;
135
+ }
116
136
  }
117
137
 
118
138
  flush() {
@@ -199,8 +219,9 @@ export class IronListAdapter {
199
219
 
200
220
  __updateElement(el, index, forceSameIndexUpdates) {
201
221
  // Clean up temporary placeholder sizing
202
- if (el.style.paddingTop) {
222
+ if (el.__virtualizerPlaceholder) {
203
223
  el.style.paddingTop = '';
224
+ el.__virtualizerPlaceholder = false;
204
225
  }
205
226
 
206
227
  if (!this.__preventElementUpdates && (el.__lastUpdatedIndex !== index || forceSameIndexUpdates)) {
@@ -224,6 +245,7 @@ export class IronListAdapter {
224
245
  // Assign a temporary placeholder sizing to elements that would otherwise end up having
225
246
  // no height.
226
247
  el.style.paddingTop = `${this.__placeholderHeight}px`;
248
+ el.__virtualizerPlaceholder = true;
227
249
 
228
250
  // Manually schedule the resize handler to make sure the placeholder padding is
229
251
  // cleared in case the resize observer never triggers.
@@ -241,6 +263,10 @@ export class IronListAdapter {
241
263
  this.__placeholderHeight = Math.round(filteredHeights.reduce((a, b) => a + b, 0) / filteredHeights.length);
242
264
  }
243
265
  });
266
+
267
+ if (this.__pendingScrollToIndex !== undefined && !this.__hasPlaceholders()) {
268
+ this.scrollToIndex(this.__pendingScrollToIndex);
269
+ }
244
270
  }
245
271
 
246
272
  __getIndexScrollOffset(index) {
@@ -1,126 +0,0 @@
1
- import { Cache } from './data-provider-controller/cache.js';
2
- import { getFlatIndexByPath, getFlatIndexInfo } from './data-provider-controller/helpers.js';
3
-
4
- export class DataProviderController extends EventTarget {
5
- constructor(host, { size, pageSize, isExpanded, dataProvider, dataProviderParams }) {
6
- super();
7
- this.host = host;
8
- this.size = size;
9
- this.pageSize = pageSize;
10
- this.isExpanded = isExpanded;
11
- this.dataProvider = dataProvider;
12
- this.dataProviderParams = dataProviderParams;
13
- this.rootCache = this.#createRootCache();
14
- }
15
-
16
- get effectiveSize() {
17
- return this.rootCache.effectiveSize;
18
- }
19
-
20
- isLoading() {
21
- return this.rootCache.isLoading;
22
- }
23
-
24
- setSize(size) {
25
- const delta = size - this.rootCache.size;
26
- this.size = size;
27
- this.rootCache.size += delta;
28
- this.rootCache.effectiveSize += delta;
29
- }
30
-
31
- setPageSize(pageSize) {
32
- this.pageSize = pageSize;
33
- this.clearCache();
34
- }
35
-
36
- setDataProvider(dataProvider) {
37
- this.dataProvider = dataProvider;
38
- this.clearCache();
39
- }
40
-
41
- recalculateEffectiveSize() {
42
- this.rootCache.recalculateEffectiveSize();
43
- }
44
-
45
- clearCache() {
46
- this.rootCache = this.#createRootCache();
47
- }
48
-
49
- getFlatIndexInfo(flatIndex) {
50
- return getFlatIndexInfo(this.rootCache, flatIndex);
51
- }
52
-
53
- getFlatIndexByPath(path) {
54
- return getFlatIndexByPath(this.rootCache, path);
55
- }
56
-
57
- ensureFlatIndexLoaded(flatIndex) {
58
- const { cache, page, item } = this.getFlatIndexInfo(flatIndex);
59
-
60
- if (!item) {
61
- this.#loadCachePage(cache, page);
62
- }
63
- }
64
-
65
- ensureFlatIndexChildrenLoaded(flatIndex) {
66
- const { cache, item, index } = this.getFlatIndexInfo(flatIndex);
67
-
68
- if (item && this.isExpanded(item)) {
69
- let subCache = cache.getSubCache(index);
70
- if (!subCache) {
71
- subCache = cache.createSubCache(index);
72
- }
73
-
74
- if (!subCache.isPageLoaded(0)) {
75
- this.#loadCachePage(subCache, 0);
76
- }
77
- }
78
- }
79
-
80
- ensureFirstPageLoaded() {
81
- if (!this.rootCache.isPageLoaded(0)) {
82
- this.#loadCachePage(this.rootCache, 0);
83
- }
84
- }
85
-
86
- #createRootCache() {
87
- return new Cache(this, this.pageSize, this.size);
88
- }
89
-
90
- #loadCachePage(cache, page) {
91
- if (!this.dataProvider || cache.pendingRequests.has(page)) {
92
- return;
93
- }
94
-
95
- const params = {
96
- page,
97
- pageSize: this.pageSize,
98
- parentItem: cache.parentItem,
99
- ...this.dataProviderParams(),
100
- };
101
-
102
- const callback = (items, size) => {
103
- if (size !== undefined) {
104
- cache.size = size;
105
- } else if (params.parentItem) {
106
- cache.size = items.length;
107
- }
108
-
109
- cache.setPage(page, items);
110
-
111
- this.recalculateEffectiveSize();
112
-
113
- this.dispatchEvent(new CustomEvent('page-received'));
114
-
115
- cache.pendingRequests.delete(page);
116
-
117
- this.dispatchEvent(new CustomEvent('page-loaded'));
118
- };
119
-
120
- cache.pendingRequests.set(page, callback);
121
-
122
- this.dispatchEvent(new CustomEvent('page-requested'));
123
-
124
- this.dataProvider(params, callback);
125
- }
126
- }