@vaadin/component-base 24.2.0-dev.f254716fe → 24.2.0-rc1

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 CHANGED
@@ -4,3 +4,4 @@ export { DirMixin } from './src/dir-mixin.js';
4
4
  export { ElementMixin } from './src/element-mixin.js';
5
5
  export { ResizeMixin } from './src/resize-mixin.js';
6
6
  export { SlotController } from './src/slot-controller.js';
7
+ export { SlotStylesMixin } from './src/slot-styles-mixin.js';
package/index.js CHANGED
@@ -4,3 +4,4 @@ export { DirMixin } from './src/dir-mixin.js';
4
4
  export { ElementMixin } from './src/element-mixin.js';
5
5
  export { ResizeMixin } from './src/resize-mixin.js';
6
6
  export { SlotController } from './src/slot-controller.js';
7
+ export { SlotStylesMixin } from './src/slot-styles-mixin.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/component-base",
3
- "version": "24.2.0-dev.f254716fe",
3
+ "version": "24.2.0-rc1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -39,8 +39,8 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@esm-bundle/chai": "^4.3.4",
42
- "@vaadin/testing-helpers": "^0.4.3",
42
+ "@vaadin/testing-helpers": "^0.5.0",
43
43
  "sinon": "^13.0.2"
44
44
  },
45
- "gitHead": "da54950b9f8c14c6451ede0d426e16a489c7fb9b"
45
+ "gitHead": "012bef350bbf29865748f4c78338dd17c6f61a74"
46
46
  }
@@ -0,0 +1,10 @@
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
+ export interface CustomElementType extends CustomElementConstructor {
7
+ is: string;
8
+ }
9
+
10
+ export declare function defineCustomElement(CustomElement: CustomElementConstructor): void;
package/src/define.js ADDED
@@ -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
+ export function defineCustomElement(CustomElement) {
7
+ const defined = customElements.get(CustomElement.is);
8
+ if (!defined) {
9
+ customElements.define(CustomElement.is, CustomElement);
10
+ } else {
11
+ const definedVersion = defined.version;
12
+ if (definedVersion && CustomElement.version && definedVersion === CustomElement.version) {
13
+ // Just loading the same thing again
14
+ console.warn(`The component ${CustomElement.is} has been loaded twice`);
15
+ } else {
16
+ console.error(
17
+ `Tried to define ${CustomElement.is} version ${CustomElement.version} when version ${defined.version} is already in use. Something will probably break.`,
18
+ );
19
+ }
20
+ }
21
+ }
@@ -13,6 +13,13 @@
13
13
  */
14
14
  export function getAncestorRootNodes(node: Node): Node[];
15
15
 
16
+ /**
17
+ * Returns the list of flattened elements for the given `node`.
18
+ * This list consists of a node's children and, for any children that are
19
+ * `<slot>` elements, the expanded flattened list of `assignedElements`.
20
+ */
21
+ export function getFlattenedElements(node: Node): Element[];
22
+
16
23
  /**
17
24
  * Traverses the given node and its parents, including those that are across
18
25
  * the shadow root boundaries, until it finds a node that matches the selector.
package/src/dom-utils.js CHANGED
@@ -40,6 +40,27 @@ export function getAncestorRootNodes(node) {
40
40
  return result;
41
41
  }
42
42
 
43
+ /**
44
+ * Returns the list of flattened elements for the given `node`.
45
+ * This list consists of a node's children and, for any children that are
46
+ * `<slot>` elements, the expanded flattened list of `assignedElements`.
47
+ *
48
+ * @param {Node} node
49
+ * @return {Element[]}
50
+ */
51
+ export function getFlattenedElements(node) {
52
+ const result = [];
53
+ let elements;
54
+ if (node.localName === 'slot') {
55
+ elements = node.assignedElements();
56
+ } else {
57
+ result.push(node);
58
+ elements = [...node.children];
59
+ }
60
+ elements.forEach((elem) => result.push(...getFlattenedElements(elem)));
61
+ return result;
62
+ }
63
+
43
64
  /**
44
65
  * Traverses the given node and its parents, including those that are across
45
66
  * the shadow root boundaries, until it finds a node that matches the selector.
@@ -16,6 +16,7 @@ export declare function ElementMixin<T extends Constructor<HTMLElement>>(
16
16
  ): Constructor<DirMixinClass> & Constructor<ElementMixinClass> & T;
17
17
 
18
18
  export declare class ElementMixinClass {
19
+ static is: string;
19
20
  static version: string;
20
21
 
21
22
  protected static finalize(): void;
@@ -45,7 +45,7 @@ const registered = new Set();
45
45
  export const ElementMixin = (superClass) =>
46
46
  class VaadinElementMixin extends DirMixin(superClass) {
47
47
  static get version() {
48
- return '24.2.0-alpha5';
48
+ return '24.2.0-rc1';
49
49
  }
50
50
 
51
51
  /** @protected */
@@ -3,7 +3,6 @@
3
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
- import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
7
6
  import { animationFrame } from './async.js';
8
7
  import { Debouncer } from './debounce.js';
9
8
 
@@ -47,30 +46,41 @@ export class OverflowController {
47
46
  * @protected
48
47
  */
49
48
  observe() {
50
- this.__resizeObserver = new ResizeObserver(() => {
49
+ const { host } = this;
50
+
51
+ this.__resizeObserver = new ResizeObserver((entries) => {
51
52
  this.__debounceOverflow = Debouncer.debounce(this.__debounceOverflow, animationFrame, () => {
52
53
  this.__updateOverflow();
53
54
  });
54
55
  });
55
56
 
56
- this.__resizeObserver.observe(this.host);
57
+ this.__resizeObserver.observe(host);
57
58
 
58
- this.__childObserver = new FlattenedNodesObserver(this.host, (info) => {
59
- info.addedNodes.forEach((node) => {
60
- if (node.nodeType === Node.ELEMENT_NODE) {
61
- this.__resizeObserver.observe(node);
62
- }
63
- });
59
+ // Observe initial children
60
+ [...host.children].forEach((child) => {
61
+ this.__resizeObserver.observe(child);
62
+ });
64
63
 
65
- info.removedNodes.forEach((node) => {
66
- if (node.nodeType === Node.ELEMENT_NODE) {
67
- this.__resizeObserver.unobserve(node);
68
- }
64
+ this.__childObserver = new MutationObserver((mutations) => {
65
+ mutations.forEach(({ addedNodes, removedNodes }) => {
66
+ addedNodes.forEach((node) => {
67
+ if (node.nodeType === Node.ELEMENT_NODE) {
68
+ this.__resizeObserver.observe(node);
69
+ }
70
+ });
71
+
72
+ removedNodes.forEach((node) => {
73
+ if (node.nodeType === Node.ELEMENT_NODE) {
74
+ this.__resizeObserver.unobserve(node);
75
+ }
76
+ });
69
77
  });
70
78
 
71
79
  this.__updateOverflow();
72
80
  });
73
81
 
82
+ this.__childObserver.observe(host, { childList: true });
83
+
74
84
  // Update overflow attribute on scroll
75
85
  this.scrollTarget.addEventListener('scroll', this.__boundOnScroll);
76
86
 
@@ -0,0 +1,15 @@
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
+ * Convenience method for reading a value from a path.
9
+ */
10
+ export function get(path: string, object: object): unknown;
11
+
12
+ /**
13
+ * Convenience method for setting a value to a path.
14
+ */
15
+ export function set(path: string, value: unknown, object: object): void;
@@ -0,0 +1,29 @@
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
+ * Convenience method for reading a value from a path.
9
+ *
10
+ * @param {string} path
11
+ * @param {object} object
12
+ */
13
+ export function get(path, object) {
14
+ return path.split('.').reduce((obj, property) => (obj ? obj[property] : undefined), object);
15
+ }
16
+
17
+ /**
18
+ * Convenience method for setting a value to a path.
19
+ *
20
+ * @param {string} path
21
+ * @param {unknown} value
22
+ * @param {object} object
23
+ */
24
+ export function set(path, value, object) {
25
+ const pathParts = path.split('.');
26
+ const lastPart = pathParts.pop();
27
+ const target = pathParts.reduce((target, part) => target[part], object);
28
+ target[lastPart] = value;
29
+ }
@@ -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 { get, set } from './path-utils.js';
7
8
 
8
9
  const caseMap = {};
9
10
 
@@ -92,15 +93,22 @@ const PolylitMixinImplementation = (superclass) => {
92
93
 
93
94
  let result = defaultDescriptor;
94
95
 
95
- if ('value' in options) {
96
- // Set the default value
97
- this.addCheckedInitializer((instance) => {
98
- if (typeof options.value === 'function') {
99
- instance[name] = options.value.call(instance);
100
- } else {
101
- instance[name] = options.value;
102
- }
103
- });
96
+ if (options.sync) {
97
+ result = {
98
+ get: defaultDescriptor.get,
99
+ set(value) {
100
+ const oldValue = this[name];
101
+ this[key] = value;
102
+ this.requestUpdate(name, oldValue, options);
103
+
104
+ // Enforce synchronous update
105
+ if (this.hasUpdated) {
106
+ this.performUpdate();
107
+ }
108
+ },
109
+ configurable: true,
110
+ enumerable: true,
111
+ };
104
112
  }
105
113
 
106
114
  if (options.readOnly) {
@@ -110,6 +118,10 @@ const PolylitMixinImplementation = (superclass) => {
110
118
  // This is run during construction of the element
111
119
  instance[`_set${upper(name)}`] = function (value) {
112
120
  setter.call(instance, value);
121
+
122
+ if (options.sync) {
123
+ this.performUpdate();
124
+ }
113
125
  };
114
126
  });
115
127
 
@@ -123,6 +135,19 @@ const PolylitMixinImplementation = (superclass) => {
123
135
  };
124
136
  }
125
137
 
138
+ if ('value' in options) {
139
+ // Set the default value
140
+ this.addCheckedInitializer((instance) => {
141
+ const value = typeof options.value === 'function' ? options.value.call(instance) : options.value;
142
+
143
+ if (options.readOnly) {
144
+ instance[`_set${upper(name)}`](value);
145
+ } else {
146
+ instance[name] = value;
147
+ }
148
+ });
149
+ }
150
+
126
151
  if (options.observer) {
127
152
  const method = options.observer;
128
153
 
@@ -175,7 +200,7 @@ const PolylitMixinImplementation = (superclass) => {
175
200
  this.$ = {};
176
201
  }
177
202
 
178
- this.shadowRoot.querySelectorAll('[id]').forEach((node) => {
203
+ this.renderRoot.querySelectorAll('[id]').forEach((node) => {
179
204
  this.$[node.id] = node;
180
205
  });
181
206
  }
@@ -202,8 +227,8 @@ const PolylitMixinImplementation = (superclass) => {
202
227
  }
203
228
 
204
229
  if (!this.__isReadyInvoked) {
205
- this.ready();
206
230
  this.__isReadyInvoked = true;
231
+ this.ready();
207
232
  }
208
233
  }
209
234
 
@@ -254,15 +279,12 @@ const PolylitMixinImplementation = (superclass) => {
254
279
 
255
280
  /** @protected */
256
281
  _get(path, object) {
257
- return path.split('.').reduce((obj, property) => (obj ? obj[property] : undefined), object);
282
+ return get(path, object);
258
283
  }
259
284
 
260
285
  /** @protected */
261
286
  _set(path, value, object) {
262
- const pathParts = path.split('.');
263
- const lastPart = pathParts.pop();
264
- const target = pathParts.reduce((target, part) => target[part], object);
265
- target[lastPart] = value;
287
+ set(path, value, object);
266
288
  }
267
289
  }
268
290
 
@@ -3,8 +3,8 @@
3
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
- import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
7
6
  import { isEmptyTextNode } from './dom-utils.js';
7
+ import { SlotObserver } from './slot-observer.js';
8
8
  import { generateUniqueId } from './unique-id-utils.js';
9
9
 
10
10
  /**
@@ -199,17 +199,17 @@ export class SlotController extends EventTarget {
199
199
  const selector = slotName === '' ? 'slot:not([name])' : `slot[name=${slotName}]`;
200
200
  const slot = this.host.shadowRoot.querySelector(selector);
201
201
 
202
- this.__slotObserver = new FlattenedNodesObserver(slot, (info) => {
202
+ this.__slotObserver = new SlotObserver(slot, ({ addedNodes, removedNodes }) => {
203
203
  const current = this.multiple ? this.nodes : [this.node];
204
204
 
205
205
  // Calling `slot.assignedNodes()` includes whitespace text nodes in case of default slot:
206
206
  // unlike comment nodes, they are not filtered out. So we need to manually ignore them.
207
- const newNodes = info.addedNodes.filter((node) => !isEmptyTextNode(node) && !current.includes(node));
207
+ const newNodes = addedNodes.filter((node) => !isEmptyTextNode(node) && !current.includes(node));
208
208
 
209
- if (info.removedNodes.length) {
210
- this.nodes = current.filter((node) => !info.removedNodes.includes(node));
209
+ if (removedNodes.length) {
210
+ this.nodes = current.filter((node) => !removedNodes.includes(node));
211
211
 
212
- info.removedNodes.forEach((node) => {
212
+ removedNodes.forEach((node) => {
213
213
  this.teardownNode(node);
214
214
  });
215
215
  }
@@ -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
  }
@@ -1,226 +0,0 @@
1
- export class Cache {
2
- /**
3
- * @type {import('../data-provider-controller.js').DataProviderController}
4
- */
5
- controller;
6
-
7
- /**
8
- * The number of items.
9
- *
10
- * @type {number}
11
- */
12
- size = 0;
13
-
14
- /**
15
- * The number of items to display per page.
16
- *
17
- * @type {number}
18
- */
19
- pageSize;
20
-
21
- /**
22
- * The total number of items, including items from expanded sub-caches.
23
- *
24
- * @type {number}
25
- */
26
- effectiveSize = 0;
27
-
28
- /**
29
- * An array of cached items.
30
- *
31
- * @type {object[]}
32
- */
33
- items = [];
34
-
35
- /**
36
- * A map where the key is a requested page and the value is a callback
37
- * that will be called with data once the request is complete.
38
- *
39
- * @type {Map<number, Function>}
40
- */
41
- pendingRequests = new Map();
42
-
43
- /**
44
- * A map where the key is the index of an item in the `items` array
45
- * and the value is a sub-cache associated with that item.
46
- *
47
- * Note, it's intentionally defined as an object instead of a Map
48
- * to ensure that Object.entries() returns an array with keys sorted
49
- * in alphabetical order, rather than the order they were added.
50
- *
51
- * @type {Record<number, Cache>}
52
- */
53
- #subCacheByIndex = {};
54
-
55
- /**
56
- * @param {Cache['controller']} controller
57
- * @param {number} pageSize
58
- * @param {number | undefined} size
59
- * @param {Cache | undefined} parentCache
60
- * @param {number | undefined} parentCacheIndex
61
- */
62
- constructor(controller, pageSize, size, parentCache, parentCacheIndex) {
63
- this.controller = controller;
64
- this.pageSize = pageSize;
65
- this.size = size || 0;
66
- this.effectiveSize = size || 0;
67
- this.parentCache = parentCache;
68
- this.parentCacheIndex = parentCacheIndex;
69
- }
70
-
71
- /**
72
- * An item in the parent cache that the current cache is associated with.
73
- *
74
- * @return {object | undefined}
75
- */
76
- get parentItem() {
77
- return this.parentCache && this.parentCache.items[this.parentCacheIndex];
78
- }
79
-
80
- /**
81
- * Whether the cache or any of its descendant caches have pending requests.
82
- *
83
- * @return {boolean}
84
- */
85
- get isLoading() {
86
- if (this.pendingRequests.size > 0) {
87
- return true;
88
- }
89
-
90
- return Object.values(this.#subCacheByIndex).some((subCache) => subCache.isLoading);
91
- }
92
-
93
- /**
94
- * An array of sub-caches sorted in the same order as their associated items
95
- * appear in the `items` array.
96
- *
97
- * @return {Array<[number, Cache]>}
98
- */
99
- get subCaches() {
100
- return Object.entries(this.#subCacheByIndex).map(([index, subCache]) => {
101
- return [parseInt(index), subCache];
102
- });
103
- }
104
-
105
- /**
106
- * Recalculates the effective size for the cache and its descendant caches recursively.
107
- */
108
- recalculateEffectiveSize() {
109
- this.effectiveSize =
110
- !this.parentItem || this.controller.isExpanded(this.parentItem)
111
- ? this.size +
112
- Object.values(this.#subCacheByIndex).reduce((total, subCache) => {
113
- subCache.recalculateEffectiveSize();
114
- return total + subCache.effectiveSize;
115
- }, 0)
116
- : 0;
117
- }
118
-
119
- /**
120
- * Adds an array of items corresponding to the given page
121
- * to the `items` array.
122
- *
123
- * @param {number} page
124
- * @param {object[]} items
125
- */
126
- setPage(page, items) {
127
- const startIndex = page * this.pageSize;
128
- items.forEach((item, i) => {
129
- this.items[startIndex + i] = item;
130
- });
131
- }
132
-
133
- /**
134
- * Returns whether the given page has been loaded in the cache.
135
- *
136
- * @param {number} page
137
- * @return {boolean}
138
- */
139
- isPageLoaded(page) {
140
- return this.items[page * this.pageSize] !== undefined;
141
- }
142
-
143
- /**
144
- * Retrieves the sub-cache associated with the item at the given index
145
- * in the `items` array.
146
- *
147
- * @param {number} index
148
- * @return {Cache | undefined}
149
- */
150
- getSubCache(index) {
151
- return this.#subCacheByIndex[index];
152
- }
153
-
154
- /**
155
- * Removes the sub-cache associated with the item at the given index
156
- * in the `items` array.
157
- *
158
- * @param {number} index
159
- */
160
- removeSubCache(index) {
161
- const subCache = this.getSubCache(index);
162
- delete this.#subCacheByIndex[index];
163
-
164
- this.controller.dispatchEvent(
165
- new CustomEvent('sub-cache-removed', {
166
- detail: {
167
- cache: this,
168
- subCache,
169
- },
170
- }),
171
- );
172
- }
173
-
174
- /**
175
- * Removes all sub-caches.
176
- */
177
- removeSubCaches() {
178
- this.#subCacheByIndex = {};
179
-
180
- this.controller.dispatchEvent(
181
- new CustomEvent('sub-caches-removed', {
182
- detail: {
183
- cache: this,
184
- },
185
- }),
186
- );
187
- }
188
-
189
- /**
190
- * Creates and associates a sub-cache for the item at the given index
191
- * in the `items` array.
192
- *
193
- * @param {number} index
194
- * @return {Cache}
195
- */
196
- createSubCache(index) {
197
- const subCache = new Cache(this.controller, this.pageSize, 0, this, index);
198
- this.#subCacheByIndex[index] = subCache;
199
-
200
- this.controller.dispatchEvent(
201
- new CustomEvent('sub-cache-created', {
202
- detail: {
203
- cache: this,
204
- subCache,
205
- },
206
- }),
207
- );
208
-
209
- return subCache;
210
- }
211
-
212
- /**
213
- * Retrieves the flattened index corresponding to the given index
214
- * of an item in the `items` array.
215
- *
216
- * @param {number} index
217
- * @return {number}
218
- */
219
- getFlatIndex(index) {
220
- const clampedIndex = Math.max(0, Math.min(this.size - 1, index));
221
-
222
- return this.subCaches.reduce((prev, [index, subCache]) => {
223
- return clampedIndex > index ? prev + subCache.effectiveSize : prev;
224
- }, clampedIndex);
225
- }
226
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * Retreives information for the given flattened index, including:
3
- * - the corresponding cache
4
- * - the associated item (if loaded)
5
- * - the corresponding index in the cache's items array.
6
- * - the page containing the index.
7
- * - the cache level
8
- *
9
- * @param {import('./cache.js').Cache} cache
10
- * @param {number} flatIndex
11
- * @return {{ cache: Cache, item: object | undefined, index: number, page: number, level: number }}
12
- */
13
- export function getFlatIndexInfo(cache, flatIndex, level = 0) {
14
- let levelIndex = flatIndex;
15
-
16
- for (const [index, subCache] of cache.subCaches) {
17
- if (levelIndex <= index) {
18
- break;
19
- } else if (levelIndex <= index + subCache.effectiveSize) {
20
- return getFlatIndexInfo(subCache, levelIndex - index - 1, level + 1);
21
- }
22
- levelIndex -= subCache.effectiveSize;
23
- }
24
-
25
- return {
26
- cache,
27
- item: cache.items[levelIndex],
28
- index: levelIndex,
29
- page: Math.floor(levelIndex / cache.pageSize),
30
- level,
31
- };
32
- }
33
-
34
- /**
35
- * Recursively returns the globally flat index of the item the given indexes point to.
36
- * Each index in the array points to a sub-item of the previous index.
37
- * Using `Infinity` as an index will point to the last item on the level.
38
- *
39
- * @param {!ItemCache} cache
40
- * @param {!Array<number>} indexes
41
- * @param {number} flatIndex
42
- * @return {number}
43
- */
44
- export function getFlatIndexByPath(cache, [levelIndex, ...subIndexes], flatIndex = 0) {
45
- if (levelIndex === Infinity) {
46
- // Treat Infinity as the last index on the level
47
- levelIndex = cache.size - 1;
48
- }
49
-
50
- const flatIndexOnLevel = cache.getFlatIndex(levelIndex);
51
- const subCache = cache.getSubCache(levelIndex);
52
- if (subCache && subCache.effectiveSize > 0 && subIndexes.length) {
53
- return getFlatIndexByPath(subCache, subIndexes, flatIndex + flatIndexOnLevel + 1);
54
- }
55
- return flatIndex + flatIndexOnLevel;
56
- }
@@ -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
- }