@vaadin/component-base 24.2.0-alpha1 → 24.2.0-alpha11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-alpha1",
3
+ "version": "24.2.0-alpha11",
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.2",
42
+ "@vaadin/testing-helpers": "^0.5.0",
43
43
  "sinon": "^13.0.2"
44
44
  },
45
- "gitHead": "0dbb118320203ab6c0c07450a3e718815367589f"
45
+ "gitHead": "a958207d5f6a09ca0e2dcf9f62194b3f92c8766a"
46
46
  }
@@ -15,62 +15,67 @@ import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
15
15
  *
16
16
  * @polymerMixin
17
17
  */
18
- export const ControllerMixin = dedupingMixin(
19
- (superClass) =>
20
- class ControllerMixinClass extends superClass {
21
- constructor() {
22
- super();
18
+ export const ControllerMixin = dedupingMixin((superClass) => {
19
+ // If the superclass extends from LitElement,
20
+ // use its own controllers implementation.
21
+ if (typeof superClass.prototype.addController === 'function') {
22
+ return superClass;
23
+ }
23
24
 
24
- /**
25
- * @type {Set<ReactiveController>}
26
- */
27
- this.__controllers = new Set();
28
- }
25
+ return class ControllerMixinClass extends superClass {
26
+ constructor() {
27
+ super();
29
28
 
30
- /** @protected */
31
- connectedCallback() {
32
- super.connectedCallback();
29
+ /**
30
+ * @type {Set<ReactiveController>}
31
+ */
32
+ this.__controllers = new Set();
33
+ }
33
34
 
34
- this.__controllers.forEach((c) => {
35
- if (c.hostConnected) {
36
- c.hostConnected();
37
- }
38
- });
39
- }
35
+ /** @protected */
36
+ connectedCallback() {
37
+ super.connectedCallback();
40
38
 
41
- /** @protected */
42
- disconnectedCallback() {
43
- super.disconnectedCallback();
39
+ this.__controllers.forEach((c) => {
40
+ if (c.hostConnected) {
41
+ c.hostConnected();
42
+ }
43
+ });
44
+ }
44
45
 
45
- this.__controllers.forEach((c) => {
46
- if (c.hostDisconnected) {
47
- c.hostDisconnected();
48
- }
49
- });
50
- }
46
+ /** @protected */
47
+ disconnectedCallback() {
48
+ super.disconnectedCallback();
51
49
 
52
- /**
53
- * Registers a controller to participate in the element update cycle.
54
- *
55
- * @param {ReactiveController} controller
56
- * @protected
57
- */
58
- addController(controller) {
59
- this.__controllers.add(controller);
60
- // Call hostConnected if a controller is added after the element is attached.
61
- if (this.$ !== undefined && this.isConnected && controller.hostConnected) {
62
- controller.hostConnected();
50
+ this.__controllers.forEach((c) => {
51
+ if (c.hostDisconnected) {
52
+ c.hostDisconnected();
63
53
  }
64
- }
54
+ });
55
+ }
65
56
 
66
- /**
67
- * Removes a controller from the element.
68
- *
69
- * @param {ReactiveController} controller
70
- * @protected
71
- */
72
- removeController(controller) {
73
- this.__controllers.delete(controller);
57
+ /**
58
+ * Registers a controller to participate in the element update cycle.
59
+ *
60
+ * @param {ReactiveController} controller
61
+ * @protected
62
+ */
63
+ addController(controller) {
64
+ this.__controllers.add(controller);
65
+ // Call hostConnected if a controller is added after the element is attached.
66
+ if (this.$ !== undefined && this.isConnected && controller.hostConnected) {
67
+ controller.hostConnected();
74
68
  }
75
- },
76
- );
69
+ }
70
+
71
+ /**
72
+ * Removes a controller from the element.
73
+ *
74
+ * @param {ReactiveController} controller
75
+ * @protected
76
+ */
77
+ removeController(controller) {
78
+ this.__controllers.delete(controller);
79
+ }
80
+ };
81
+ });
@@ -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.
@@ -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-alpha1';
48
+ return '24.2.0-alpha11';
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
 
@@ -103,6 +104,24 @@ const PolylitMixinImplementation = (superclass) => {
103
104
  });
104
105
  }
105
106
 
107
+ if (options.sync) {
108
+ result = {
109
+ get: defaultDescriptor.get,
110
+ set(value) {
111
+ const oldValue = this[name];
112
+ this[key] = value;
113
+ this.requestUpdate(name, oldValue, options);
114
+
115
+ // Enforce synchronous update
116
+ if (this.hasUpdated) {
117
+ this.performUpdate();
118
+ }
119
+ },
120
+ configurable: true,
121
+ enumerable: true,
122
+ };
123
+ }
124
+
106
125
  if (options.readOnly) {
107
126
  const setter = defaultDescriptor.set;
108
127
 
@@ -175,7 +194,7 @@ const PolylitMixinImplementation = (superclass) => {
175
194
  this.$ = {};
176
195
  }
177
196
 
178
- this.shadowRoot.querySelectorAll('[id]').forEach((node) => {
197
+ this.renderRoot.querySelectorAll('[id]').forEach((node) => {
179
198
  this.$[node.id] = node;
180
199
  });
181
200
  }
@@ -202,8 +221,8 @@ const PolylitMixinImplementation = (superclass) => {
202
221
  }
203
222
 
204
223
  if (!this.__isReadyInvoked) {
205
- this.ready();
206
224
  this.__isReadyInvoked = true;
225
+ this.ready();
207
226
  }
208
227
  }
209
228
 
@@ -254,15 +273,12 @@ const PolylitMixinImplementation = (superclass) => {
254
273
 
255
274
  /** @protected */
256
275
  _get(path, object) {
257
- return path.split('.').reduce((obj, property) => (obj ? obj[property] : undefined), object);
276
+ return get(path, object);
258
277
  }
259
278
 
260
279
  /** @protected */
261
280
  _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;
281
+ set(path, value, object);
266
282
  }
267
283
  }
268
284
 
@@ -169,7 +169,7 @@ export class SlotChildObserveController extends SlotController {
169
169
  __updateNodeId(node) {
170
170
  // When in multiple mode, only set ID attribute on the element in default slot.
171
171
  const isFirstNode = !this.nodes || node === this.nodes[0];
172
- if (node.nodeType === Node.ELEMENT_NODE && isFirstNode && !node.id) {
172
+ if (node.nodeType === Node.ELEMENT_NODE && (!this.multiple || isFirstNode) && !node.id) {
173
173
  node.id = this.defaultId;
174
174
  }
175
175
  }
@@ -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;
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): 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
  }
@@ -47,6 +51,20 @@ export class TooltipController extends SlotController {
47
51
  }
48
52
  }
49
53
 
54
+ /**
55
+ * Set an HTML element for linking with the tooltip overlay
56
+ * via `aria-describedby` attribute used by screen readers.
57
+ * @param {HTMLElement} ariaTarget
58
+ */
59
+ setAriaTarget(ariaTarget) {
60
+ this.ariaTarget = ariaTarget;
61
+
62
+ const tooltipNode = this.node;
63
+ if (tooltipNode) {
64
+ tooltipNode.ariaTarget = ariaTarget;
65
+ }
66
+ }
67
+
50
68
  /**
51
69
  * Set a context object to be used by generator.
52
70
  * @param {object} context