@brightspace-ui/core 3.143.1 → 3.144.1

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.
@@ -6,41 +6,17 @@ import { interactiveElements, interactiveRoles, isInteractive } from '../../help
6
6
  import { announce } from '../../helpers/announce.js';
7
7
  import { bodySmallStyles } from '../typography/styles.js';
8
8
  import { classMap } from 'lit/directives/class-map.js';
9
+ import { getFlag } from '../../helpers/flags.js';
9
10
  import { getUniqueId } from '../../helpers/uniqueId.js';
11
+ import { PopoverMixin } from '../popover/popover-mixin.js';
10
12
  import ResizeObserver from 'resize-observer-polyfill/dist/ResizeObserver.es.js';
11
13
  import { RtlMixin } from '../../mixins/rtl/rtl-mixin.js';
12
14
  import { styleMap } from 'lit/directives/style-map.js';
13
15
 
14
- let logAccessibilityWarning = true;
15
-
16
- /* only one tooltip is to be shown at once - track the active tooltip so it can be hidden if necessary */
17
- let activeTooltip = null;
18
-
19
- const pointerLength = 16;
20
- const pointerOverhang = 7; /* how far the pointer extends outside the content */
16
+ const usePopoverMixin = getFlag('GAUD-7355-tooltip-popover', false);
21
17
 
22
- /* rotated 45 degrees */
23
- const pointerRotatedLength = Math.SQRT2 * pointerLength;
24
- const pointerRotatedOverhang = ((pointerRotatedLength - pointerLength) / 2) + pointerOverhang;
25
-
26
- const pointerGap = 0; /* spacing between pointer and target */
27
- const defaultViewportMargin = 18;
28
- const contentBorderRadius = 6;
29
18
  const contentBorderSize = 1;
30
19
  const contentHorizontalPadding = 15;
31
- const outlineSize = 1;
32
-
33
- /* once a user closes a tooltip, ignore delay if they hover adjacent target within this timeout */
34
- let delayTimeoutId;
35
- const resetDelayTimeout = () => {
36
- if (delayTimeoutId) clearTimeout(delayTimeoutId);
37
- delayTimeoutId = setTimeout(() => delayTimeoutId = null, 1000);
38
- };
39
- /* ignore delay if user hovers adjacent target when a tooltip is already open */
40
- const getDelay = delay => {
41
- if (delayTimeoutId) return 0;
42
- else return delay;
43
- };
44
20
 
45
21
  const tooltipInteractiveElements = {
46
22
  ...interactiveElements,
@@ -58,975 +34,1540 @@ const tooltipInteractiveRoles = {
58
34
  'img': true
59
35
  };
60
36
 
61
- const computeTooltipShift = (centerDelta, spaceLeft, spaceRight) => {
37
+ const isInteractiveTarget = (elem) => {
38
+ if (elem.nodeType !== Node.ELEMENT_NODE) return false;
39
+ if (!isFocusable(elem, true, false, true)) return false;
62
40
 
63
- const contentXAdjustment = centerDelta / 2;
64
- if (centerDelta <= 0) {
65
- return contentXAdjustment * -1;
66
- }
67
- if (spaceLeft >= contentXAdjustment && spaceRight >= contentXAdjustment) {
68
- // center with target
69
- return contentXAdjustment * -1;
70
- }
71
- if (spaceLeft <= contentXAdjustment) {
72
- // shift content right (not enough space to center)
73
- return spaceLeft * -1;
74
- } else {
75
- // shift content left (not enough space to center)
76
- return (centerDelta * -1) + spaceRight;
77
- }
41
+ return isInteractive(elem, tooltipInteractiveElements, tooltipInteractiveRoles);
78
42
  };
79
43
 
80
- /**
81
- * A component used to display additional information when users focus or hover on a point of interest.
82
- * @slot - Default content placed inside of the tooltip
83
- * @fires d2l-tooltip-show - Dispatched when the tooltip is opened
84
- * @fires d2l-tooltip-hide - Dispatched when the tooltip is closed
85
- */
86
- class Tooltip extends RtlMixin(LitElement) {
87
-
88
- static get properties() {
89
- return {
90
- /**
91
- * Align the tooltip with either the start or end of its target. If not set, the tooltip will attempt be centered.
92
- * @type {'start'|'end'}
93
- */
94
- align: { type: String, reflect: true },
95
- /**
96
- * ADVANCED: Announce the tooltip innerText when applicable (for use with custom elements)
97
- * @type {boolean}
98
- */
99
- announced: { type: Boolean },
100
- /**
101
- * @ignore
102
- */
103
- boundary: { type: Object },
104
- /**
105
- * ADVANCED: Causes the tooltip to close when its target is clicked
106
- * @type {boolean}
107
- */
108
- closeOnClick: { type: Boolean, attribute: 'close-on-click' },
109
- /**
110
- * Provide a delay in milliseconds to prevent the tooltip from opening immediately when hovered. This delay will only apply to hover, not focus.
111
- * @type {number}
112
- */
113
- delay: { type: Number },
114
- /**
115
- * ADVANCED: Disables focus lock so the tooltip will automatically close when no longer hovered even if it still has focus
116
- * @type {boolean}
117
- */
118
- disableFocusLock: { type: Boolean, attribute: 'disable-focus-lock' },
119
- /**
120
- * REQUIRED: The "id" of the tooltip's target element. Both elements must be within the same shadow root. If not provided, the tooltip's parent element will be used as its target.
121
- * @type {string}
122
- */
123
- for: { type: String },
124
- /**
125
- * ADVANCED: Force the tooltip to stay open as long as it remains "true"
126
- * @type {boolean}
127
- */
128
- forceShow: { type: Boolean, attribute: 'force-show' },
129
- /**
130
- * ADVANCED: Accessibility type for the tooltip to specify whether it is the primary label for the target or a secondary descriptor.
131
- * @type {'label'|'descriptor'}
132
- */
133
- forType: { type: String, attribute: 'for-type' },
134
- /**
135
- * Adjust the size of the gap between the tooltip and its target (px)
136
- * @type {number}
137
- */
138
- offset: { type: Number }, /* tooltipOffset */
139
- /**
140
- * ADVANCED: Only show the tooltip if we detect the target element is truncated
141
- * @type {boolean}
142
- */
143
- showTruncatedOnly: { type: Boolean, attribute: 'show-truncated-only' },
144
- /**
145
- * ADVANCED: Force the tooltip to open in a certain direction. If no position is provided, the tooltip will open in the first position that has enough space for it in the order: bottom, top, right, left.
146
- * @type {'top'|'bottom'|'left'|'right'}
147
- */
148
- position: { type: String },
149
- /**
150
- * @ignore
151
- */
152
- showing: { type: Boolean, reflect: true },
153
- /**
154
- * The style of the tooltip based on the type of information it displays
155
- * @type {'info'|'error'}
156
- */
157
- state: { type: String, reflect: true },
158
- _maxWidth: { type: Number },
159
- _openDir: { type: String, reflect: true, attribute: '_open-dir' },
160
- _tooltipShift: { type: Number },
161
- _viewportMargin: { type: Number }
162
- };
163
- }
44
+ // once a user closes a tooltip, ignore delay if they hover adjacent target within this timeout
45
+ let delayTimeoutId;
46
+ const resetDelayTimeout = () => {
47
+ if (delayTimeoutId) clearTimeout(delayTimeoutId);
48
+ delayTimeoutId = setTimeout(() => delayTimeoutId = null, 1000);
49
+ };
50
+ // ignore delay if user hovers adjacent target when a tooltip is already open
51
+ const getDelay = delay => {
52
+ if (delayTimeoutId) return 0;
53
+ else return delay;
54
+ };
164
55
 
165
- static get styles() {
166
- return [bodySmallStyles, css`
167
- :host {
168
- --d2l-tooltip-background-color: var(--d2l-color-ferrite); /* Deprecated, use state attribute instead */
169
- --d2l-tooltip-border-color: var(--d2l-color-ferrite); /* Deprecated, use state attribute instead */
170
- --d2l-tooltip-outline-color: rgba(255, 255, 255, 0.32);
171
- box-sizing: border-box;
172
- color: white;
173
- display: inline-block;
174
- height: 0;
175
- overflow: hidden;
176
- position: absolute;
177
- text-align: left;
178
- visibility: hidden;
179
- white-space: normal;
180
- width: 0;
181
- z-index: 1001; /* position on top of floating buttons */
182
- }
56
+ let logAccessibilityWarning = true;
183
57
 
184
- :host([state="error"]) {
185
- --d2l-tooltip-background-color: var(--d2l-color-cinnabar);
186
- --d2l-tooltip-border-color: var(--d2l-color-cinnabar);
187
- }
58
+ // only one tooltip is to be shown at once - track the active tooltip so it can be hidden if necessary
59
+ let activeTooltip = null;
188
60
 
189
- :host([dir="rtl"]) {
190
- text-align: right;
191
- }
61
+ if (usePopoverMixin) {
192
62
 
193
- :host([force-show]),
194
- :host([showing]) {
195
- height: auto;
196
- overflow: visible;
197
- visibility: visible;
198
- width: auto;
199
- }
63
+ /**
64
+ * A component used to display additional information when users focus or hover on a point of interest.
65
+ * @slot - Default content placed inside of the tooltip
66
+ * @fires d2l-tooltip-show - Dispatched when the tooltip is opened
67
+ * @fires d2l-tooltip-hide - Dispatched when the tooltip is closed
68
+ */
69
+ class Tooltip extends PopoverMixin(LitElement) {
70
+
71
+ static get properties() {
72
+ return {
73
+ /**
74
+ * Align the tooltip with either the start or end of its target. If not set, the tooltip will attempt be centered.
75
+ * @type {'start'|'end'}
76
+ */
77
+ align: { type: String, reflect: true },
78
+ /**
79
+ * ADVANCED: Announce the tooltip innerText when applicable (for use with custom elements)
80
+ * @type {boolean}
81
+ */
82
+ announced: { type: Boolean },
83
+ /**
84
+ * ADVANCED: Causes the tooltip to close when its target is clicked
85
+ * @type {boolean}
86
+ */
87
+ closeOnClick: { type: Boolean, attribute: 'close-on-click' },
88
+ /**
89
+ * Provide a delay in milliseconds to prevent the tooltip from opening immediately when hovered. This delay will only apply to hover, not focus.
90
+ * @type {number}
91
+ */
92
+ delay: { type: Number },
93
+ /**
94
+ * ADVANCED: Disables focus lock so the tooltip will automatically close when no longer hovered even if it still has focus
95
+ * @type {boolean}
96
+ */
97
+ disableFocusLock: { type: Boolean, attribute: 'disable-focus-lock' },
98
+ /**
99
+ * REQUIRED: The "id" of the tooltip's target element. Both elements must be within the same shadow root. If not provided, the tooltip's parent element will be used as its target.
100
+ * @type {string}
101
+ */
102
+ for: { type: String },
103
+ /**
104
+ * ADVANCED: Force the tooltip to stay open as long as it remains "true"
105
+ * @type {boolean}
106
+ */
107
+ forceShow: { type: Boolean, attribute: 'force-show' },
108
+ /**
109
+ * ADVANCED: Accessibility type for the tooltip to specify whether it is the primary label for the target or a secondary descriptor.
110
+ * @type {'label'|'descriptor'}
111
+ */
112
+ forType: { type: String, attribute: 'for-type' },
113
+ /**
114
+ * Adjust the size of the gap between the tooltip and its target (px)
115
+ * @type {number}
116
+ */
117
+ offset: { type: Number },
118
+ /**
119
+ * ADVANCED: Force the tooltip to open in a certain direction. If no position is provided, the tooltip will open in the first position that has enough space for it in the order: bottom, top, right, left.
120
+ * @type {'top'|'bottom'|'left'|'right'}
121
+ */
122
+ positionLocation: { type: String, attribute: 'position' },
123
+ /**
124
+ * @ignore
125
+ */
126
+ showing: { type: Boolean, reflect: true },
127
+ /**
128
+ * ADVANCED: Only show the tooltip if we detect the target element is truncated
129
+ * @type {boolean}
130
+ */
131
+ showTruncatedOnly: { type: Boolean, attribute: 'show-truncated-only' },
132
+ /**
133
+ * The style of the tooltip based on the type of information it displays
134
+ * @type {'info'|'error'}
135
+ */
136
+ state: { type: String, reflect: true }
137
+ };
138
+ }
200
139
 
201
- .d2l-tooltip-pointer {
202
- border: 1px solid transparent; /* fixes a webket clipping defect */
203
- box-sizing: border-box;
204
- display: inline-block;
205
- height: ${pointerLength}px;
206
- position: absolute;
207
- width: ${pointerLength}px;
208
- z-index: 1;
209
- }
140
+ static get styles() {
141
+ return [super.styles, bodySmallStyles, css`
142
+ :host {
143
+ --d2l-tooltip-background-color: var(--d2l-color-ferrite); /* Deprecated, use state attribute instead */
144
+ --d2l-tooltip-border-color: var(--d2l-color-ferrite); /* Deprecated, use state attribute instead */
145
+ --d2l-tooltip-outline-color: rgba(255, 255, 255, 0.32);
146
+ --d2l-popover-background-color: var(--d2l-tooltip-background-color);
147
+ --d2l-popover-border-color: var(--d2l-tooltip-outline-color);
148
+ --d2l-popover-border-radius: 0.3rem;
149
+ }
150
+ :host([state="error"]) {
151
+ --d2l-tooltip-background-color: var(--d2l-color-cinnabar);
152
+ --d2l-tooltip-border-color: var(--d2l-color-cinnabar);
153
+ }
154
+ .d2l-tooltip-content {
155
+ box-sizing: border-box;
156
+ color: white;
157
+ max-width: 17.5rem;
158
+ min-height: 1.95rem;
159
+ min-width: 2.1rem;
160
+ overflow: hidden;
161
+ overflow-wrap: anywhere;
162
+ padding-block: ${10 - contentBorderSize}px ${11 - contentBorderSize}px;
163
+ padding-inline: ${contentHorizontalPadding - contentBorderSize}px;
164
+ }
165
+ ::slotted(ul),
166
+ ::slotted(ol) {
167
+ padding-inline-start: 1rem;
168
+ }
169
+ @media (max-width: 615px) {
170
+ .d2l-tooltip-content {
171
+ padding-bottom: ${12 - contentBorderSize}px;
172
+ padding-top: ${12 - contentBorderSize}px;
173
+ }
174
+ }
175
+ `];
176
+ }
210
177
 
211
- :host([_open-dir="top"]) .d2l-tooltip-pointer,
212
- :host([_open-dir="bottom"]) .d2l-tooltip-pointer {
213
- left: calc(50% - ${pointerLength / 2}px);
214
- }
178
+ constructor() {
179
+ super();
180
+ this.announced = false;
181
+ this.closeOnClick = false;
182
+ this.delay = 300;
183
+ this.disableFocusLock = false;
184
+ this.forceShow = false;
185
+ this.forType = 'descriptor';
186
+ this.offset = 10;
187
+ this.showTruncatedOnly = false;
188
+ this.state = 'info';
189
+
190
+ this.#handleTargetBlurBound = this.#handleTargetBlur.bind(this);
191
+ this.#handleTargetClickBound = this.#handleTargetClick.bind(this);
192
+ this.#handleTargetFocusBound = this.#handleTargetFocus.bind(this);
193
+ this.#handleTargetMouseEnterBound = this.#handleTargetMouseEnter.bind(this);
194
+ this.#handleTargetMouseLeaveBound = this.#handleTargetMouseLeave.bind(this);
195
+ this.#handleTargetResizeBound = this.#handleTargetResize.bind(this);
196
+ this.#handleTargetTouchEndBound = this.#handleTargetTouchEnd.bind(this);
197
+ this.#handleTargetTouchStartBound = this.#handleTargetTouchStart.bind(this);
198
+ }
215
199
 
216
- :host([_open-dir="top"][align="start"]) .d2l-tooltip-pointer,
217
- :host([_open-dir="bottom"][align="start"]) .d2l-tooltip-pointer,
218
- :host([_open-dir="top"][align="end"][dir="rtl"]) .d2l-tooltip-pointer,
219
- :host([_open-dir="bottom"][align="end"][dir="rtl"]) .d2l-tooltip-pointer {
220
- left: ${contentHorizontalPadding + (pointerRotatedLength - pointerLength) / 2}px; /* needed for browsers that don't support min like Legacy-Edge */
221
- left: min(${contentHorizontalPadding + (pointerRotatedLength - pointerLength) / 2}px, calc(50% - ${pointerLength / 2}px));
222
- right: auto;
200
+ /** @ignore */
201
+ get showing() {
202
+ return this.#showing;
203
+ }
204
+ set showing(val) {
205
+ const oldVal = this.#showing;
206
+ if (oldVal !== val) {
207
+ this.#showing = val;
208
+ this.requestUpdate('showing', oldVal);
209
+ this.#showingChanged(val, oldVal !== undefined); // don't dispatch hide event when initializing
223
210
  }
211
+ }
224
212
 
225
- :host([_open-dir="top"][align="end"]) .d2l-tooltip-pointer,
226
- :host([_open-dir="bottom"][align="end"]) .d2l-tooltip-pointer,
227
- :host([_open-dir="top"][align="start"][dir="rtl"]) .d2l-tooltip-pointer,
228
- :host([_open-dir="bottom"][align="start"][dir="rtl"]) .d2l-tooltip-pointer {
229
- left: auto;
230
- right: ${contentHorizontalPadding + (pointerRotatedLength - pointerLength) / 2}px; /* needed for browsers that don't support min like Legacy-Edge */
231
- right: min(${contentHorizontalPadding + (pointerRotatedLength - pointerLength) / 2}px, calc(50% - ${pointerLength / 2}px));
232
- }
213
+ connectedCallback() {
214
+ super.connectedCallback();
215
+ this.showing = false;
216
+ window.addEventListener('resize', this.#handleTargetResizeBound);
233
217
 
234
- :host([_open-dir="top"]) .d2l-tooltip-pointer {
235
- bottom: -${pointerOverhang}px;
236
- clip: rect(${pointerOverhang + contentBorderSize}px, 21px, 22px, -3px);
237
- }
218
+ requestAnimationFrame(() => this.#updateTarget());
219
+ }
238
220
 
239
- :host([_open-dir="bottom"]) .d2l-tooltip-pointer {
240
- clip: rect(-5px, 21px, ${pointerOverhang + contentBorderSize}px, -7px);
241
- top: -${pointerOverhang}px;
242
- }
221
+ disconnectedCallback() {
222
+ super.disconnectedCallback();
223
+ if (activeTooltip === this) activeTooltip = null;
243
224
 
244
- :host([_open-dir="left"]) .d2l-tooltip-pointer,
245
- :host([_open-dir="right"]) .d2l-tooltip-pointer {
246
- top: calc(50% - ${pointerLength / 2}px);
247
- }
225
+ this.#removeListeners();
226
+ window.removeEventListener('resize', this.#handleTargetResizeBound);
248
227
 
249
- :host([_open-dir="left"]) .d2l-tooltip-pointer {
250
- clip: rect(-3px, 21px, 21px, ${pointerOverhang + contentBorderSize}px);
251
- right: -${pointerOverhang}px;
252
- }
228
+ clearDismissible(this.#dismissibleId);
229
+ delayTimeoutId = null;
230
+ this.#dismissibleId = null;
253
231
 
254
- :host([_open-dir="right"]) .d2l-tooltip-pointer {
255
- clip: rect(-3px, ${pointerOverhang + contentBorderSize}px, 21px, -3px);
256
- left: -${pointerOverhang}px;
232
+ if (this.#target) {
233
+ elemIdListRemove(this.#target, 'aria-labelledby', this.id);
234
+ elemIdListRemove(this.#target, 'aria-describedby', this.id);
257
235
  }
236
+ }
258
237
 
259
- .d2l-tooltip-pointer > div {
260
- background-color: var(--d2l-tooltip-background-color);
261
- border: ${contentBorderSize}px solid var(--d2l-tooltip-border-color);
262
- border-radius: 0.1rem;
263
- box-sizing: border-box;
264
- height: ${pointerLength}px;
265
- left: -1px;
266
- position: absolute;
267
- top: -1px;
268
- -webkit-transform: rotate(45deg);
269
- transform: rotate(45deg);
270
- width: ${pointerLength}px;
271
- }
238
+ firstUpdated(changedProperties) {
239
+ super.firstUpdated(changedProperties);
240
+ this.addEventListener('mouseenter', this.#handleTooltipMouseEnter);
241
+ this.addEventListener('mouseleave', this.#handleTooltipMouseLeave);
242
+ }
272
243
 
273
- :host([_open-dir="top"]) .d2l-tooltip-pointer-outline {
274
- clip: rect(${pointerOverhang + contentBorderSize + outlineSize * 2}px, 21px, 22px, -3px);
275
- }
244
+ render() {
245
+ const content = html`
246
+ <div class="d2l-tooltip-content d2l-body-small" role="text">
247
+ <slot></slot>
248
+ </div>
249
+ `;
276
250
 
277
- :host([_open-dir="bottom"]) .d2l-tooltip-pointer-outline {
278
- clip: rect(-4px, 21px, ${pointerOverhang + contentBorderSize - outlineSize * 2}px, -7px);
279
- }
251
+ return this.renderPopover(content);
252
+ }
280
253
 
281
- :host([_open-dir="left"]) .d2l-tooltip-pointer-outline {
282
- clip: rect(-3px, 21px, 21px, ${pointerOverhang + contentBorderSize + outlineSize * 2}px);
283
- }
254
+ willUpdate(changedProperties) {
255
+ super.willUpdate(changedProperties);
284
256
 
285
- :host([_open-dir="right"]) .d2l-tooltip-pointer-outline {
286
- clip: rect(-3px, ${pointerOverhang + contentBorderSize - outlineSize * 2}px, 21px, -4px);
257
+ if (changedProperties.has('align') || changedProperties.has('forceShow') || changedProperties.has('offset') || changedProperties.has('positionLocation')) {
258
+ super.configure({
259
+ noAutoClose: this.forceShow,
260
+ offset: (this.offset !== undefined ? Number.parseInt(this.offset) : undefined),
261
+ position: { location: this.#adaptPositionLocation(this.positionLocation), span: this.#adaptPositionSpan(this.align) },
262
+ });
287
263
  }
288
264
 
289
- .d2l-tooltip-pointer-outline > div {
290
- outline: ${outlineSize}px solid var(--d2l-tooltip-outline-color);
291
- }
265
+ changedProperties.forEach((_, prop) => {
266
+ if (prop === 'for') this.#updateTarget();
267
+ else if (prop === 'forceShow') this.#updateShowing();
268
+ });
269
+ }
292
270
 
293
- .d2l-tooltip-position {
294
- display: inline-block;
295
- height: 0;
296
- position: absolute;
297
- width: 17.5rem;
298
- }
271
+ hide() {
272
+ this.#isHovering = false;
273
+ this.#isFocusing = false;
274
+ this.#updateShowing();
275
+ }
299
276
 
300
- :host([_open-dir="left"]) .d2l-tooltip-position {
301
- right: 100%;
302
- }
303
- :host([_open-dir="right"][dir="rtl"]) .d2l-tooltip-position {
304
- left: 100%;
305
- }
277
+ show() {
278
+ this.showing = true;
279
+ }
306
280
 
307
- .d2l-tooltip-content {
308
- background-color: var(--d2l-tooltip-background-color);
309
- border: ${contentBorderSize}px solid var(--d2l-tooltip-border-color);
310
- border-radius: ${contentBorderRadius}px;
311
- box-sizing: border-box;
312
- max-width: 17.5rem;
313
- min-height: 1.95rem;
314
- min-width: 2.1rem;
315
- outline: ${outlineSize}px solid var(--d2l-tooltip-outline-color);
316
- overflow: hidden;
317
- overflow-wrap: anywhere;
318
- padding-block: ${10 - contentBorderSize}px ${11 - contentBorderSize}px;
319
- padding-inline: ${contentHorizontalPadding - contentBorderSize}px;
320
- position: absolute;
281
+ #dismissibleId = null;
282
+ #handleTargetBlurBound;
283
+ #handleTargetClickBound;
284
+ #handleTargetFocusBound;
285
+ #handleTargetMouseEnterBound;
286
+ #handleTargetMouseLeaveBound;
287
+ #handleTargetResizeBound;
288
+ #handleTargetTouchEndBound;
289
+ #handleTargetTouchStartBound;
290
+ #hoverTimeout;
291
+ #isFocusing = false;
292
+ #isHovering = false;
293
+ #isHoveringTooltip = false;
294
+ #isTruncating = false;
295
+ #longPressTimeout;
296
+ #mouseLeaveTimeout;
297
+ #mouseLeftTooltip = false;
298
+ #resizeRunSinceTruncationCheck = false;
299
+ #showing;
300
+ #target;
301
+ #targetSizeObserver;
302
+
303
+ #adaptPositionLocation(val) {
304
+ switch (val) {
305
+ case 'bottom': return 'block-end';
306
+ case 'left': return 'inline-start';
307
+ case 'right': return 'inline-end';
308
+ case 'top': return 'block-start';
309
+ default: return 'block-end';
321
310
  }
311
+ }
322
312
 
323
- /* increase specificty for Legacy-Edge so the d2l-body-small color doesn't override it */
324
- .d2l-tooltip-content.d2l-tooltip-content {
325
- color: inherit;
313
+ #adaptPositionSpan(val) {
314
+ switch (val) {
315
+ case 'start': return 'end';
316
+ case 'end': return 'start';
317
+ default: return 'all';
326
318
  }
319
+ }
327
320
 
328
- :host([_open-dir="top"]) .d2l-tooltip-content {
329
- bottom: 100%;
330
- }
331
- :host([_open-dir="left"]) .d2l-tooltip-content {
332
- right: 0;
333
- }
334
- :host([_open-dir="right"][dir="rtl"]) .d2l-tooltip-content {
335
- left: 0;
336
- }
321
+ #addListeners() {
322
+ if (!this.#target) return;
337
323
 
338
- .d2l-tooltip-container {
339
- height: 100%;
340
- width: 100%;
341
- }
324
+ this.#target.addEventListener('mouseenter', this.#handleTargetMouseEnterBound);
325
+ this.#target.addEventListener('mouseleave', this.#handleTargetMouseLeaveBound);
326
+ this.#target.addEventListener('focus', this.#handleTargetFocusBound);
327
+ this.#target.addEventListener('blur', this.#handleTargetBlurBound);
328
+ this.#target.addEventListener('click', this.#handleTargetClickBound);
329
+ this.#target.addEventListener('touchstart', this.#handleTargetTouchStartBound, { passive: true });
330
+ this.#target.addEventListener('touchcancel', this.#handleTargetTouchEndBound);
331
+ this.#target.addEventListener('touchend', this.#handleTargetTouchEndBound);
342
332
 
343
- :host([_open-dir="bottom"][showing]) .d2l-tooltip-container {
344
- -webkit-animation: d2l-tooltip-bottom-animation 200ms ease;
345
- animation: d2l-tooltip-bottom-animation 200ms ease;
346
- }
333
+ this.#targetSizeObserver = new ResizeObserver(this.#handleTargetResizeBound);
334
+ this.#targetSizeObserver.observe(this.#target);
335
+ }
347
336
 
348
- :host([_open-dir="top"][showing]) .d2l-tooltip-container {
349
- -webkit-animation: d2l-tooltip-top-animation 200ms ease;
350
- animation: d2l-tooltip-top-animation 200ms ease;
351
- }
337
+ #findTarget() {
338
+ const ownerRoot = this.getRootNode();
352
339
 
353
- :host([_open-dir="left"][showing]) .d2l-tooltip-container {
354
- -webkit-animation: d2l-tooltip-left-animation 200ms ease;
355
- animation: d2l-tooltip-left-animation 200ms ease;
356
- }
340
+ let target;
341
+ if (this.for) {
342
+ const targetSelector = `#${cssEscape(this.for)}`;
343
+ target = ownerRoot.querySelector(targetSelector);
344
+ target = target || ownerRoot?.host?.querySelector(targetSelector);
345
+ } else {
346
+ const parentNode = this.parentNode;
347
+ target = parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE ? ownerRoot.host : parentNode;
357
348
 
358
- :host([_open-dir="right"][showing]) .d2l-tooltip-container {
359
- -webkit-animation: d2l-tooltip-right-animation 200ms ease;
360
- animation: d2l-tooltip-right-animation 200ms ease;
349
+ // reduce console pollution since Safari + VO prevents inadequate SR experience for tooltips during form validation when using 'for'
350
+ if (!(target.tagName === 'D2L-INPUT-TEXT' && target?.invalid)) {
351
+ console.warn('<d2l-tooltip>: missing required attribute "for"');
352
+ }
361
353
  }
354
+ return target;
355
+ }
362
356
 
363
- ::slotted(ul),
364
- ::slotted(ol) {
365
- padding-left: 1rem;
366
- }
357
+ #handleTargetBlur() {
358
+ this.#isFocusing = false;
359
+ this.#updateShowing();
360
+ }
367
361
 
368
- :host([dir="rtl"]) ::slotted(ul),
369
- :host([dir="rtl"]) ::slotted(ol) {
370
- padding-left: 0;
371
- padding-right: 1rem;
372
- }
362
+ #handleTargetClick() {
363
+ if (this.closeOnClick) this.hide();
364
+ }
373
365
 
374
- @media (prefers-reduced-motion: reduce) {
375
- :host([_open-dir="bottom"][showing]) .d2l-tooltip-container,
376
- :host([_open-dir="top"][showing]) .d2l-tooltip-container,
377
- :host([_open-dir="left"][showing]) .d2l-tooltip-container,
378
- :host([_open-dir="right"][showing]) .d2l-tooltip-container {
379
- -webkit-animation: none;
380
- animation: none;
381
- }
366
+ async #handleTargetFocus() {
367
+ if (this.showTruncatedOnly) {
368
+ await this.#updateTruncating();
369
+ if (!this.#isTruncating) return;
382
370
  }
383
371
 
384
- @keyframes d2l-tooltip-top-animation {
385
- 0% { opacity: 0; transform: translate(0, -10px); }
386
- 100% { opacity: 1; transform: translate(0, 0); }
387
- }
388
- @keyframes d2l-tooltip-bottom-animation {
389
- 0% { opacity: 0; transform: translate(0, 10px); }
390
- 100% { opacity: 1; transform: translate(0, 0); }
391
- }
392
- @keyframes d2l-tooltip-left-animation {
393
- 0% { opacity: 0; transform: translate(-10px, 0); }
394
- 100% { opacity: 1; transform: translate(0, 0); }
372
+ if (this.disableFocusLock) {
373
+ this.showing = true;
374
+ } else {
375
+ this.#isFocusing = true;
376
+ this.#updateShowing();
395
377
  }
396
- @keyframes d2l-tooltip-right-animation {
397
- 0% { opacity: 0; transform: translate(10px, 0); }
398
- 100% { opacity: 1; transform: translate(0, 0); }
378
+ }
379
+
380
+ #handleTargetMouseEnter() {
381
+ // came from tooltip so keep showing
382
+ if (this.#mouseLeftTooltip) {
383
+ this.#isHovering = true;
384
+ return;
399
385
  }
400
386
 
401
- @media (max-width: 615px) {
402
- .d2l-tooltip-content {
403
- padding-bottom: ${12 - contentBorderSize}px;
404
- padding-top: ${12 - contentBorderSize}px;
387
+ this.#hoverTimeout = setTimeout(async() => {
388
+ if (this.showTruncatedOnly) {
389
+ await this.#updateTruncating();
390
+ if (!this.#isTruncating) return;
405
391
  }
406
- }
407
- `];
408
- }
409
392
 
410
- constructor() {
411
- super();
412
-
413
- this._onTargetBlur = this._onTargetBlur.bind(this);
414
- this._onTargetFocus = this._onTargetFocus.bind(this);
415
- this._onTargetMouseEnter = this._onTargetMouseEnter.bind(this);
416
- this._onTargetMouseLeave = this._onTargetMouseLeave.bind(this);
417
- this._onTargetResize = this._onTargetResize.bind(this);
418
- this._onTargetClick = this._onTargetClick.bind(this);
419
- this._onTargetTouchStart = this._onTargetTouchStart.bind(this);
420
- this._onTargetTouchEnd = this._onTargetTouchEnd.bind(this);
421
-
422
- this.announced = false;
423
- this.closeOnClick = false;
424
- this.delay = 300;
425
- this.disableFocusLock = false;
426
- this.forceShow = false;
427
- this.forType = 'descriptor';
428
- this.offset = pointerRotatedOverhang + pointerGap;
429
- this.showTruncatedOnly = false;
430
- this.state = 'info';
431
-
432
- this._dismissibleId = null;
433
- this._isFocusing = false;
434
- this._isHovering = false;
435
- this._resizeRunSinceTruncationCheck = false;
436
- this._viewportMargin = defaultViewportMargin;
437
-
438
- this.#isHoveringTooltip = false;
439
- this.#mouseLeftTooltip = false;
440
- }
393
+ this.#isHovering = true;
394
+ this.#updateShowing();
395
+ }, getDelay(this.delay));
396
+ }
441
397
 
442
- /** @ignore */
443
- get showing() {
444
- return this._showing;
445
- }
446
- set showing(val) {
447
- const oldVal = this._showing;
448
- if (oldVal !== val) {
449
- this._showing = val;
450
- this.requestUpdate('showing', oldVal);
451
- this._showingChanged(val, oldVal !== undefined); // don't dispatch hide event when initializing
398
+ #handleTargetMouseLeave() {
399
+ clearTimeout(this.#hoverTimeout);
400
+ this.#isHovering = false;
401
+ if (this.showing) resetDelayTimeout();
402
+ setTimeout(() => this.#updateShowing(), 100); // delay to allow for mouseenter to fire if hovering on tooltip
452
403
  }
453
- }
454
404
 
455
- connectedCallback() {
456
- super.connectedCallback();
457
- this.showing = false;
458
- window.addEventListener('resize', this._onTargetResize);
405
+ #handleTargetResize() {
406
+ this.#resizeRunSinceTruncationCheck = true;
407
+ if (!this.showing) return;
408
+ super.position();
409
+ }
459
410
 
460
- requestAnimationFrame(() => {
461
- if (this.isConnected) {
462
- this._updateTarget();
463
- }
464
- });
465
- }
411
+ #handleTargetTouchEnd() {
412
+ clearTimeout(this.#longPressTimeout);
413
+ }
466
414
 
467
- disconnectedCallback() {
468
- super.disconnectedCallback();
469
- if (activeTooltip === this) activeTooltip = null;
470
- this._removeListeners();
471
- window.removeEventListener('resize', this._onTargetResize);
472
- clearDismissible(this._dismissibleId);
473
- delayTimeoutId = null;
474
- this._dismissibleId = null;
475
- if (this._target) {
476
- elemIdListRemove(this._target, 'aria-labelledby', this.id);
477
- elemIdListRemove(this._target, 'aria-describedby', this.id);
415
+ #handleTargetTouchStart() {
416
+ this.#longPressTimeout = setTimeout(() => {
417
+ this.#target.focus();
418
+ }, 500);
478
419
  }
479
- }
480
420
 
481
- async getUpdateComplete() {
482
- const fontsPromise = document.fonts ? document.fonts.ready : Promise.resolve();
483
- await super.getUpdateComplete();
484
- /* wait for the fonts to load because browsers have a font block period
485
- where they will render an invisible fallback font face that may result in
486
- improper width calculations before the real font is loaded */
487
- await fontsPromise;
488
- }
421
+ #handleTooltipMouseEnter() {
422
+ if (!this.showing) return;
423
+ this.#isHoveringTooltip = true;
424
+ this.#updateShowing();
425
+ }
489
426
 
490
- render() {
491
- const tooltipPositionStyle = {
492
- maxWidth: this._maxWidth ? `${this._maxWidth}px` : null
493
- };
494
- if (this._tooltipShift !== undefined) {
495
- if (this._isAboveOrBelow()) {
496
- const isRtl = this.getAttribute('dir') === 'rtl';
497
- tooltipPositionStyle.left = !isRtl ? `${this._tooltipShift}px` : null;
498
- tooltipPositionStyle.right = !isRtl ? null : `${this._tooltipShift}px`;
427
+ #handleTooltipMouseLeave() {
428
+ clearTimeout(this.#mouseLeaveTimeout);
429
+
430
+ this.#isHoveringTooltip = false;
431
+ this.#mouseLeftTooltip = true;
432
+ resetDelayTimeout();
433
+
434
+ this.#mouseLeaveTimeout = setTimeout(() => {
435
+ this.#mouseLeftTooltip = false;
436
+ this.#updateShowing();
437
+ }, 100); // delay to allow for mouseenter to fire if hovering on target
438
+ }
439
+
440
+ #removeListeners() {
441
+ if (!this.#target) return;
442
+
443
+ this.#target.removeEventListener('mouseenter', this.#handleTargetMouseEnterBound);
444
+ this.#target.removeEventListener('mouseleave', this.#handleTargetMouseLeaveBound);
445
+ this.#target.removeEventListener('focus', this.#handleTargetFocusBound);
446
+ this.#target.removeEventListener('blur', this.#handleTargetBlurBound);
447
+ this.#target.removeEventListener('click', this.#handleTargetClickBound);
448
+ this.#target.removeEventListener('touchstart', this.#handleTargetTouchStartBound);
449
+ this.#target.removeEventListener('touchcancel', this.#handleTargetTouchEndBound);
450
+ this.#target.removeEventListener('touchend', this.#handleTargetTouchEndBound);
451
+
452
+ if (this.#targetSizeObserver) {
453
+ this.#targetSizeObserver.disconnect();
454
+ this.#targetSizeObserver = null;
455
+ }
456
+ }
457
+
458
+ async #showingChanged(newValue, dispatch) {
459
+ clearTimeout(this.#hoverTimeout);
460
+ clearTimeout(this.#longPressTimeout);
461
+
462
+ if (newValue) {
463
+ if (!this.forceShow) {
464
+ if (activeTooltip) activeTooltip.hide();
465
+ activeTooltip = this;
466
+ }
467
+
468
+ this.#dismissibleId = setDismissible(() => this.hide());
469
+ this.setAttribute('aria-hidden', 'false');
470
+ await this.updateComplete;
471
+
472
+ super.open(this.#target, false);
473
+
474
+ if (dispatch) {
475
+ this.dispatchEvent(new CustomEvent('d2l-tooltip-show', { bubbles: true, composed: true }));
476
+ }
477
+
478
+ if (this.announced && !isInteractiveTarget(this.#target)) announce(this.innerText);
499
479
  } else {
500
- tooltipPositionStyle.top = `${this._tooltipShift}px`;
480
+ if (activeTooltip === this) activeTooltip = null;
481
+
482
+ this.setAttribute('aria-hidden', 'true');
483
+ if (this.#dismissibleId) {
484
+ clearDismissible(this.#dismissibleId);
485
+ this.#dismissibleId = null;
486
+ }
487
+
488
+ super.close();
489
+
490
+ if (dispatch) {
491
+ this.dispatchEvent(new CustomEvent('d2l-tooltip-hide', { bubbles: true, composed: true }));
492
+ }
501
493
  }
502
494
  }
503
495
 
504
- const contentClasses = {
505
- 'd2l-tooltip-content': true,
506
- 'd2l-body-small': true,
507
- 'vdiff-target': this.showing
508
- };
496
+ #updateShowing() {
497
+ this.showing = this.#isFocusing || this.#isHovering || this.forceShow || this.#isHoveringTooltip;
498
+ }
509
499
 
510
- // Note: role="text" is a workaround for Safari. Otherwise, list-item content is not announced with VoiceOver
511
- return html`
512
- <div class="d2l-tooltip-container">
513
- <div class="d2l-tooltip-position" style=${styleMap(tooltipPositionStyle)}>
514
- <div class="${classMap(contentClasses)}" @mouseenter="${this.#onTooltipMouseEnter}" @mouseleave="${this.#onTooltipMouseLeave}">
515
- <div role="text">
516
- <slot></slot>
517
- </div>
518
- </div>
519
- </div>
520
- <div class="d2l-tooltip-pointer d2l-tooltip-pointer-outline">
521
- <div></div>
522
- </div>
523
- <div class="d2l-tooltip-pointer" @mouseenter="${this.#onTooltipMouseEnter}" @mouseleave="${this.#onTooltipMouseLeave}">
524
- <div></div>
525
- </div>
526
- </div>`
527
- ;
528
- }
500
+ #updateTarget() {
529
501
 
530
- willUpdate(changedProperties) {
531
- super.willUpdate(changedProperties);
502
+ if (!this.isConnected) return;
532
503
 
533
- changedProperties.forEach((_, prop) => {
534
- if (prop === 'for') {
535
- this._updateTarget();
536
- } else if (prop === 'forceShow') {
537
- this._updateShowing();
504
+ const newTarget = this.#findTarget();
505
+ if (this.#target === newTarget) return;
506
+
507
+ this.#removeListeners();
508
+ this.#target = newTarget;
509
+
510
+ if (this.#target) {
511
+ const targetDisabled = this.#target.hasAttribute('disabled') || this.#target.getAttribute('aria-disabled') === 'true';
512
+
513
+ const isTargetInteractive = isInteractiveTarget(this.#target);
514
+ this.id = this.id || getUniqueId();
515
+ this.setAttribute('role', 'tooltip');
516
+
517
+ if (this.forType === 'label') {
518
+ elemIdListAdd(this.#target, 'aria-labelledby', this.id);
519
+ } else if (!this.announced || isTargetInteractive) {
520
+ elemIdListAdd(this.#target, 'aria-describedby', this.id);
521
+ }
522
+
523
+ if (logAccessibilityWarning && !isTargetInteractive && !this.announced) {
524
+ console.warn(
525
+ 'd2l-tooltip may be being used in a non-accessible manner; it should be attached to interactive elements like \'a\', \'button\',' +
526
+ '\'input\'', '\'select\', \'textarea\' or static / custom elements if a role has been set and the element is focusable.',
527
+ this.#target
528
+ );
529
+ logAccessibilityWarning = false;
530
+ }
531
+
532
+ if (this.showing) {
533
+ if (!super._opened) super.open(this.#target, false);
534
+ else super.position();
535
+ } else if (!targetDisabled && isComposedAncestor(this.#target, getComposedActiveElement())) {
536
+ this.#handleTargetFocusBound();
537
+ }
538
538
  }
539
- });
540
- }
539
+ this.#addListeners();
540
+ }
541
541
 
542
- hide() {
543
- this._isHovering = false;
544
- this._isFocusing = false;
545
- this._updateShowing();
546
- }
542
+ /**
543
+ * This solution appends a clone of the target to the target in order to retain target styles.
544
+ * A possible consequence of this is unexpected behaviours for web components that have slots.
545
+ * If this becomes an issue, it would also likely be possible to append the clone to document.body
546
+ * and get the expected styles through getComputedStyle.
547
+ */
548
+ async #updateTruncating() {
549
+ // if no resize has happened since truncation was previously calculated the result will not have changed
550
+ if (!this.#resizeRunSinceTruncationCheck || !this.showTruncatedOnly) return;
551
+
552
+ const target = this.#target;
553
+ const cloneContainer = document.createElement('div');
554
+ cloneContainer.style.position = 'absolute';
555
+ cloneContainer.style.overflow = 'hidden';
556
+ cloneContainer.style.whiteSpace = 'nowrap';
557
+ cloneContainer.style.width = '1px';
558
+ cloneContainer.style.insetInlineStart = '-10000px';
559
+
560
+ const clone = target.cloneNode(true);
561
+ clone.removeAttribute('id');
562
+ clone.style.maxWidth = 'none';
563
+ clone.style.display = 'inline-block';
564
+
565
+ cloneContainer.appendChild(clone);
566
+ target.appendChild(cloneContainer);
567
+ await this.updateComplete;
568
+
569
+ // if the clone is a web component it needs to update to fill in any slots
570
+ const customElem = customElements.get(clone.localName);
571
+ if (customElem !== undefined) {
572
+ clone.requestUpdate();
573
+ await clone.updateComplete;
574
+ }
575
+
576
+ this.#isTruncating = (clone.scrollWidth - target.offsetWidth) > 2; // Safari adds 1px to scrollWidth necessitating a subtraction comparison.
577
+ this.#resizeRunSinceTruncationCheck = false;
578
+ target.removeChild(cloneContainer);
579
+ }
547
580
 
548
- show() {
549
- this.showing = true;
550
581
  }
582
+ customElements.define('d2l-tooltip', Tooltip);
551
583
 
552
- async updatePosition() {
584
+ } else {
553
585
 
554
- if (!this._target) {
555
- return;
556
- }
586
+ // Cleanup: GAUD-7355-tooltip-popover - remove this entire block and unused imports
557
587
 
558
- const offsetParent = getOffsetParent(this);
559
- const targetRect = this._target.getBoundingClientRect();
560
- const spaceAround = this._computeSpaceAround(offsetParent, targetRect);
588
+ const pointerLength = 16;
589
+ const pointerOverhang = 7; /* how far the pointer extends outside the content */
561
590
 
562
- // Compute the size of the spaces above, below, left and right and find which space to fit the tooltip in
563
- const content = this._getContent();
564
- if (content === null) return;
591
+ /* rotated 45 degrees */
592
+ const pointerRotatedLength = Math.SQRT2 * pointerLength;
593
+ const pointerRotatedOverhang = ((pointerRotatedLength - pointerLength) / 2) + pointerOverhang;
565
594
 
566
- const spaces = this._computeAvailableSpaces(targetRect, spaceAround);
567
- const space = await this._fitContentToSpace(content, spaces);
595
+ const pointerGap = 0; /* spacing between pointer and target */
596
+ const defaultViewportMargin = 18;
597
+ const contentBorderRadius = 6;
598
+ const outlineSize = 1;
568
599
 
569
- const contentRect = content.getBoundingClientRect();
570
- // + 1 because scrollWidth does not give sub-pixel measurements and half a pixel may cause text to unexpectedly wrap
571
- this._maxWidth = Math.min(content.scrollWidth + 2 * contentBorderSize, 350) + 1;
572
- this._openDir = space.dir;
600
+ const computeTooltipShift = (centerDelta, spaceLeft, spaceRight) => {
573
601
 
574
- // Compute the x and y position of the tooltip relative to its target
575
- let offsetTop, offsetLeft;
576
- if (offsetParent && offsetParent.tagName !== 'BODY') {
577
- const offsetRect = offsetParent.getBoundingClientRect();
578
- offsetTop = offsetRect.top + offsetParent.clientTop - offsetParent.scrollTop;
579
- offsetLeft = offsetRect.left + offsetParent.clientLeft - offsetParent.scrollLeft;
580
- } else {
581
- offsetTop = -document.documentElement.scrollTop;
582
- offsetLeft = -document.documentElement.scrollLeft;
583
- }
584
- const top = targetRect.top - offsetTop;
585
- const left = targetRect.left - offsetLeft;
586
-
587
- let positionRect;
588
- if (this._isAboveOrBelow()) {
589
- positionRect = {
590
- left,
591
- top: this._openDir === 'top' ? top - this.offset : top + targetRect.height + this.offset,
592
- width: targetRect.width,
593
- height: 0,
594
- };
602
+ const contentXAdjustment = centerDelta / 2;
603
+ if (centerDelta <= 0) {
604
+ return contentXAdjustment * -1;
605
+ }
606
+ if (spaceLeft >= contentXAdjustment && spaceRight >= contentXAdjustment) {
607
+ // center with target
608
+ return contentXAdjustment * -1;
609
+ }
610
+ if (spaceLeft <= contentXAdjustment) {
611
+ // shift content right (not enough space to center)
612
+ return spaceLeft * -1;
595
613
  } else {
596
- positionRect = {
597
- left: this._openDir === 'left' ? left - this.offset : left + targetRect.width + this.offset,
598
- top,
599
- height: targetRect.height,
600
- width: 0,
614
+ // shift content left (not enough space to center)
615
+ return (centerDelta * -1) + spaceRight;
616
+ }
617
+ };
618
+
619
+ /**
620
+ * A component used to display additional information when users focus or hover on a point of interest.
621
+ * @slot - Default content placed inside of the tooltip
622
+ * @fires d2l-tooltip-show - Dispatched when the tooltip is opened
623
+ * @fires d2l-tooltip-hide - Dispatched when the tooltip is closed
624
+ */
625
+ class Tooltip extends RtlMixin(LitElement) {
626
+
627
+ static get properties() {
628
+ return {
629
+ /**
630
+ * Align the tooltip with either the start or end of its target. If not set, the tooltip will attempt be centered.
631
+ * @type {'start'|'end'}
632
+ */
633
+ align: { type: String, reflect: true },
634
+ /**
635
+ * ADVANCED: Announce the tooltip innerText when applicable (for use with custom elements)
636
+ * @type {boolean}
637
+ */
638
+ announced: { type: Boolean },
639
+ /**
640
+ * @ignore
641
+ */
642
+ boundary: { type: Object },
643
+ /**
644
+ * ADVANCED: Causes the tooltip to close when its target is clicked
645
+ * @type {boolean}
646
+ */
647
+ closeOnClick: { type: Boolean, attribute: 'close-on-click' },
648
+ /**
649
+ * Provide a delay in milliseconds to prevent the tooltip from opening immediately when hovered. This delay will only apply to hover, not focus.
650
+ * @type {number}
651
+ */
652
+ delay: { type: Number },
653
+ /**
654
+ * ADVANCED: Disables focus lock so the tooltip will automatically close when no longer hovered even if it still has focus
655
+ * @type {boolean}
656
+ */
657
+ disableFocusLock: { type: Boolean, attribute: 'disable-focus-lock' },
658
+ /**
659
+ * REQUIRED: The "id" of the tooltip's target element. Both elements must be within the same shadow root. If not provided, the tooltip's parent element will be used as its target.
660
+ * @type {string}
661
+ */
662
+ for: { type: String },
663
+ /**
664
+ * ADVANCED: Force the tooltip to stay open as long as it remains "true"
665
+ * @type {boolean}
666
+ */
667
+ forceShow: { type: Boolean, attribute: 'force-show' },
668
+ /**
669
+ * ADVANCED: Accessibility type for the tooltip to specify whether it is the primary label for the target or a secondary descriptor.
670
+ * @type {'label'|'descriptor'}
671
+ */
672
+ forType: { type: String, attribute: 'for-type' },
673
+ /**
674
+ * Adjust the size of the gap between the tooltip and its target (px)
675
+ * @type {number}
676
+ */
677
+ offset: { type: Number }, /* tooltipOffset */
678
+ /**
679
+ * ADVANCED: Only show the tooltip if we detect the target element is truncated
680
+ * @type {boolean}
681
+ */
682
+ showTruncatedOnly: { type: Boolean, attribute: 'show-truncated-only' },
683
+ /**
684
+ * ADVANCED: Force the tooltip to open in a certain direction. If no position is provided, the tooltip will open in the first position that has enough space for it in the order: bottom, top, right, left.
685
+ * @type {'top'|'bottom'|'left'|'right'}
686
+ */
687
+ position: { type: String },
688
+ /**
689
+ * @ignore
690
+ */
691
+ showing: { type: Boolean, reflect: true },
692
+ /**
693
+ * The style of the tooltip based on the type of information it displays
694
+ * @type {'info'|'error'}
695
+ */
696
+ state: { type: String, reflect: true },
697
+ _maxWidth: { type: Number },
698
+ _openDir: { type: String, reflect: true, attribute: '_open-dir' },
699
+ _tooltipShift: { type: Number },
700
+ _viewportMargin: { type: Number }
601
701
  };
602
702
  }
603
703
 
604
- // Compute how much the tooltip is shifted relative to its pointer
605
- if (this._isAboveOrBelow() && (this.align === 'start' || this.align === 'end')) {
606
- const shift = Math.min((targetRect.width / 2) - (contentHorizontalPadding + pointerRotatedLength / 2), 0);
607
- if (this.align === 'start') {
608
- this._tooltipShift = shift;
609
- } else {
610
- this._tooltipShift = targetRect.width - this._maxWidth - shift;
704
+ static get styles() {
705
+ return [bodySmallStyles, css`
706
+ :host {
707
+ --d2l-tooltip-background-color: var(--d2l-color-ferrite); /* Deprecated, use state attribute instead */
708
+ --d2l-tooltip-border-color: var(--d2l-color-ferrite); /* Deprecated, use state attribute instead */
709
+ --d2l-tooltip-outline-color: rgba(255, 255, 255, 0.32);
710
+ box-sizing: border-box;
711
+ color: white;
712
+ display: inline-block;
713
+ height: 0;
714
+ overflow: hidden;
715
+ position: absolute;
716
+ text-align: left;
717
+ visibility: hidden;
718
+ white-space: normal;
719
+ width: 0;
720
+ z-index: 1001; /* position on top of floating buttons */
721
+ }
722
+
723
+ :host([state="error"]) {
724
+ --d2l-tooltip-background-color: var(--d2l-color-cinnabar);
725
+ --d2l-tooltip-border-color: var(--d2l-color-cinnabar);
726
+ }
727
+
728
+ :host([dir="rtl"]) {
729
+ text-align: right;
730
+ }
731
+
732
+ :host([force-show]),
733
+ :host([showing]) {
734
+ height: auto;
735
+ overflow: visible;
736
+ visibility: visible;
737
+ width: auto;
738
+ }
739
+
740
+ .d2l-tooltip-pointer {
741
+ border: 1px solid transparent; /* fixes a webket clipping defect */
742
+ box-sizing: border-box;
743
+ display: inline-block;
744
+ height: ${pointerLength}px;
745
+ position: absolute;
746
+ width: ${pointerLength}px;
747
+ z-index: 1;
748
+ }
749
+
750
+ :host([_open-dir="top"]) .d2l-tooltip-pointer,
751
+ :host([_open-dir="bottom"]) .d2l-tooltip-pointer {
752
+ left: calc(50% - ${pointerLength / 2}px);
753
+ }
754
+
755
+ :host([_open-dir="top"][align="start"]) .d2l-tooltip-pointer,
756
+ :host([_open-dir="bottom"][align="start"]) .d2l-tooltip-pointer,
757
+ :host([_open-dir="top"][align="end"][dir="rtl"]) .d2l-tooltip-pointer,
758
+ :host([_open-dir="bottom"][align="end"][dir="rtl"]) .d2l-tooltip-pointer {
759
+ left: ${contentHorizontalPadding + (pointerRotatedLength - pointerLength) / 2}px; /* needed for browsers that don't support min like Legacy-Edge */
760
+ left: min(${contentHorizontalPadding + (pointerRotatedLength - pointerLength) / 2}px, calc(50% - ${pointerLength / 2}px));
761
+ right: auto;
762
+ }
763
+
764
+ :host([_open-dir="top"][align="end"]) .d2l-tooltip-pointer,
765
+ :host([_open-dir="bottom"][align="end"]) .d2l-tooltip-pointer,
766
+ :host([_open-dir="top"][align="start"][dir="rtl"]) .d2l-tooltip-pointer,
767
+ :host([_open-dir="bottom"][align="start"][dir="rtl"]) .d2l-tooltip-pointer {
768
+ left: auto;
769
+ right: ${contentHorizontalPadding + (pointerRotatedLength - pointerLength) / 2}px; /* needed for browsers that don't support min like Legacy-Edge */
770
+ right: min(${contentHorizontalPadding + (pointerRotatedLength - pointerLength) / 2}px, calc(50% - ${pointerLength / 2}px));
771
+ }
772
+
773
+ :host([_open-dir="top"]) .d2l-tooltip-pointer {
774
+ bottom: -${pointerOverhang}px;
775
+ clip: rect(${pointerOverhang + contentBorderSize}px, 21px, 22px, -3px);
776
+ }
777
+
778
+ :host([_open-dir="bottom"]) .d2l-tooltip-pointer {
779
+ clip: rect(-5px, 21px, ${pointerOverhang + contentBorderSize}px, -7px);
780
+ top: -${pointerOverhang}px;
781
+ }
782
+
783
+ :host([_open-dir="left"]) .d2l-tooltip-pointer,
784
+ :host([_open-dir="right"]) .d2l-tooltip-pointer {
785
+ top: calc(50% - ${pointerLength / 2}px);
786
+ }
787
+
788
+ :host([_open-dir="left"]) .d2l-tooltip-pointer {
789
+ clip: rect(-3px, 21px, 21px, ${pointerOverhang + contentBorderSize}px);
790
+ right: -${pointerOverhang}px;
791
+ }
792
+
793
+ :host([_open-dir="right"]) .d2l-tooltip-pointer {
794
+ clip: rect(-3px, ${pointerOverhang + contentBorderSize}px, 21px, -3px);
795
+ left: -${pointerOverhang}px;
796
+ }
797
+
798
+ .d2l-tooltip-pointer > div {
799
+ background-color: var(--d2l-tooltip-background-color);
800
+ border: ${contentBorderSize}px solid var(--d2l-tooltip-border-color);
801
+ border-radius: 0.1rem;
802
+ box-sizing: border-box;
803
+ height: ${pointerLength}px;
804
+ left: -1px;
805
+ position: absolute;
806
+ top: -1px;
807
+ -webkit-transform: rotate(45deg);
808
+ transform: rotate(45deg);
809
+ width: ${pointerLength}px;
810
+ }
811
+
812
+ :host([_open-dir="top"]) .d2l-tooltip-pointer-outline {
813
+ clip: rect(${pointerOverhang + contentBorderSize + outlineSize * 2}px, 21px, 22px, -3px);
814
+ }
815
+
816
+ :host([_open-dir="bottom"]) .d2l-tooltip-pointer-outline {
817
+ clip: rect(-4px, 21px, ${pointerOverhang + contentBorderSize - outlineSize * 2}px, -7px);
818
+ }
819
+
820
+ :host([_open-dir="left"]) .d2l-tooltip-pointer-outline {
821
+ clip: rect(-3px, 21px, 21px, ${pointerOverhang + contentBorderSize + outlineSize * 2}px);
822
+ }
823
+
824
+ :host([_open-dir="right"]) .d2l-tooltip-pointer-outline {
825
+ clip: rect(-3px, ${pointerOverhang + contentBorderSize - outlineSize * 2}px, 21px, -4px);
826
+ }
827
+
828
+ .d2l-tooltip-pointer-outline > div {
829
+ outline: ${outlineSize}px solid var(--d2l-tooltip-outline-color);
830
+ }
831
+
832
+ .d2l-tooltip-position {
833
+ display: inline-block;
834
+ height: 0;
835
+ position: absolute;
836
+ width: 17.5rem;
837
+ }
838
+
839
+ :host([_open-dir="left"]) .d2l-tooltip-position {
840
+ right: 100%;
841
+ }
842
+ :host([_open-dir="right"][dir="rtl"]) .d2l-tooltip-position {
843
+ left: 100%;
844
+ }
845
+
846
+ .d2l-tooltip-content {
847
+ background-color: var(--d2l-tooltip-background-color);
848
+ border: ${contentBorderSize}px solid var(--d2l-tooltip-border-color);
849
+ border-radius: ${contentBorderRadius}px;
850
+ box-sizing: border-box;
851
+ max-width: 17.5rem;
852
+ min-height: 1.95rem;
853
+ min-width: 2.1rem;
854
+ outline: ${outlineSize}px solid var(--d2l-tooltip-outline-color);
855
+ overflow: hidden;
856
+ overflow-wrap: anywhere;
857
+ padding-block: ${10 - contentBorderSize}px ${11 - contentBorderSize}px;
858
+ padding-inline: ${contentHorizontalPadding - contentBorderSize}px;
859
+ position: absolute;
860
+ }
861
+
862
+ /* increase specificty for Legacy-Edge so the d2l-body-small color doesn't override it */
863
+ .d2l-tooltip-content.d2l-tooltip-content {
864
+ color: inherit;
865
+ }
866
+
867
+ :host([_open-dir="top"]) .d2l-tooltip-content {
868
+ bottom: 100%;
869
+ }
870
+ :host([_open-dir="left"]) .d2l-tooltip-content {
871
+ right: 0;
872
+ }
873
+ :host([_open-dir="right"][dir="rtl"]) .d2l-tooltip-content {
874
+ left: 0;
875
+ }
876
+
877
+ .d2l-tooltip-container {
878
+ height: 100%;
879
+ width: 100%;
880
+ }
881
+
882
+ :host([_open-dir="bottom"][showing]) .d2l-tooltip-container {
883
+ -webkit-animation: d2l-tooltip-bottom-animation 200ms ease;
884
+ animation: d2l-tooltip-bottom-animation 200ms ease;
885
+ }
886
+
887
+ :host([_open-dir="top"][showing]) .d2l-tooltip-container {
888
+ -webkit-animation: d2l-tooltip-top-animation 200ms ease;
889
+ animation: d2l-tooltip-top-animation 200ms ease;
890
+ }
891
+
892
+ :host([_open-dir="left"][showing]) .d2l-tooltip-container {
893
+ -webkit-animation: d2l-tooltip-left-animation 200ms ease;
894
+ animation: d2l-tooltip-left-animation 200ms ease;
895
+ }
896
+
897
+ :host([_open-dir="right"][showing]) .d2l-tooltip-container {
898
+ -webkit-animation: d2l-tooltip-right-animation 200ms ease;
899
+ animation: d2l-tooltip-right-animation 200ms ease;
900
+ }
901
+
902
+ ::slotted(ul),
903
+ ::slotted(ol) {
904
+ padding-left: 1rem;
905
+ }
906
+
907
+ :host([dir="rtl"]) ::slotted(ul),
908
+ :host([dir="rtl"]) ::slotted(ol) {
909
+ padding-left: 0;
910
+ padding-right: 1rem;
911
+ }
912
+
913
+ @media (prefers-reduced-motion: reduce) {
914
+ :host([_open-dir="bottom"][showing]) .d2l-tooltip-container,
915
+ :host([_open-dir="top"][showing]) .d2l-tooltip-container,
916
+ :host([_open-dir="left"][showing]) .d2l-tooltip-container,
917
+ :host([_open-dir="right"][showing]) .d2l-tooltip-container {
918
+ -webkit-animation: none;
919
+ animation: none;
920
+ }
921
+ }
922
+
923
+ @keyframes d2l-tooltip-top-animation {
924
+ 0% { opacity: 0; transform: translate(0, -10px); }
925
+ 100% { opacity: 1; transform: translate(0, 0); }
926
+ }
927
+ @keyframes d2l-tooltip-bottom-animation {
928
+ 0% { opacity: 0; transform: translate(0, 10px); }
929
+ 100% { opacity: 1; transform: translate(0, 0); }
930
+ }
931
+ @keyframes d2l-tooltip-left-animation {
932
+ 0% { opacity: 0; transform: translate(-10px, 0); }
933
+ 100% { opacity: 1; transform: translate(0, 0); }
934
+ }
935
+ @keyframes d2l-tooltip-right-animation {
936
+ 0% { opacity: 0; transform: translate(10px, 0); }
937
+ 100% { opacity: 1; transform: translate(0, 0); }
938
+ }
939
+
940
+ @media (max-width: 615px) {
941
+ .d2l-tooltip-content {
942
+ padding-bottom: ${12 - contentBorderSize}px;
943
+ padding-top: ${12 - contentBorderSize}px;
944
+ }
945
+ }
946
+ `];
947
+ }
948
+
949
+ constructor() {
950
+ super();
951
+
952
+ this._onTargetBlur = this._onTargetBlur.bind(this);
953
+ this._onTargetFocus = this._onTargetFocus.bind(this);
954
+ this._onTargetMouseEnter = this._onTargetMouseEnter.bind(this);
955
+ this._onTargetMouseLeave = this._onTargetMouseLeave.bind(this);
956
+ this._onTargetResize = this._onTargetResize.bind(this);
957
+ this._onTargetClick = this._onTargetClick.bind(this);
958
+ this._onTargetTouchStart = this._onTargetTouchStart.bind(this);
959
+ this._onTargetTouchEnd = this._onTargetTouchEnd.bind(this);
960
+
961
+ this.announced = false;
962
+ this.closeOnClick = false;
963
+ this.delay = 300;
964
+ this.disableFocusLock = false;
965
+ this.forceShow = false;
966
+ this.forType = 'descriptor';
967
+ this.offset = pointerRotatedOverhang + pointerGap;
968
+ this.showTruncatedOnly = false;
969
+ this.state = 'info';
970
+
971
+ this._dismissibleId = null;
972
+ this._isFocusing = false;
973
+ this._isHovering = false;
974
+ this._resizeRunSinceTruncationCheck = false;
975
+ this._viewportMargin = defaultViewportMargin;
976
+
977
+ this.#isHoveringTooltip = false;
978
+ this.#mouseLeftTooltip = false;
979
+ }
980
+
981
+ /** @ignore */
982
+ get showing() {
983
+ return this._showing;
984
+ }
985
+ set showing(val) {
986
+ const oldVal = this._showing;
987
+ if (oldVal !== val) {
988
+ this._showing = val;
989
+ this.requestUpdate('showing', oldVal);
990
+ this._showingChanged(val, oldVal !== undefined); // don't dispatch hide event when initializing
611
991
  }
612
- } else {
613
- let spaceLeft, spaceRight, centerDelta, maxShift, minShift;
614
- if (this._isAboveOrBelow()) {
615
- const isRtl = this.getAttribute('dir') === 'rtl';
616
- spaceLeft = !isRtl ? spaceAround.left : spaceAround.right;
617
- spaceRight = !isRtl ? spaceAround.right : spaceAround.left;
618
- centerDelta = this._maxWidth - targetRect.width;
619
- maxShift = targetRect.width / 2;
620
- minShift = maxShift - this._maxWidth;
621
- } else {
622
- spaceLeft = spaceAround.above;
623
- spaceRight = spaceAround.below;
624
- centerDelta = contentRect.height - targetRect.height;
625
- maxShift = targetRect.height / 2;
626
- minShift = maxShift - contentRect.height;
992
+ }
993
+
994
+ connectedCallback() {
995
+ super.connectedCallback();
996
+ this.showing = false;
997
+ window.addEventListener('resize', this._onTargetResize);
998
+
999
+ requestAnimationFrame(() => {
1000
+ if (this.isConnected) {
1001
+ this._updateTarget();
1002
+ }
1003
+ });
1004
+ }
1005
+
1006
+ disconnectedCallback() {
1007
+ super.disconnectedCallback();
1008
+ if (activeTooltip === this) activeTooltip = null;
1009
+ this._removeListeners();
1010
+ window.removeEventListener('resize', this._onTargetResize);
1011
+ clearDismissible(this._dismissibleId);
1012
+ delayTimeoutId = null;
1013
+ this._dismissibleId = null;
1014
+ if (this._target) {
1015
+ elemIdListRemove(this._target, 'aria-labelledby', this.id);
1016
+ elemIdListRemove(this._target, 'aria-describedby', this.id);
627
1017
  }
628
- const shift = computeTooltipShift(centerDelta, spaceLeft, spaceRight);
629
- const shiftMargin = (pointerRotatedLength / 2) + contentBorderRadius;
630
- this._tooltipShift = Math.min(Math.max(shift, minShift + shiftMargin), maxShift - shiftMargin);
631
- }
632
- this.style.left = `${positionRect.left}px`;
633
- this.style.top = `${positionRect.top}px`;
634
- this.style.width = `${positionRect.width}px`;
635
- this.style.height = `${positionRect.height}px`;
636
- }
1018
+ }
1019
+
1020
+ async getUpdateComplete() {
1021
+ const fontsPromise = document.fonts ? document.fonts.ready : Promise.resolve();
1022
+ await super.getUpdateComplete();
1023
+ /* wait for the fonts to load because browsers have a font block period
1024
+ where they will render an invisible fallback font face that may result in
1025
+ improper width calculations before the real font is loaded */
1026
+ await fontsPromise;
1027
+ }
1028
+
1029
+ render() {
1030
+ const tooltipPositionStyle = {
1031
+ maxWidth: this._maxWidth ? `${this._maxWidth}px` : null
1032
+ };
1033
+ if (this._tooltipShift !== undefined) {
1034
+ if (this._isAboveOrBelow()) {
1035
+ const isRtl = this.getAttribute('dir') === 'rtl';
1036
+ tooltipPositionStyle.left = !isRtl ? `${this._tooltipShift}px` : null;
1037
+ tooltipPositionStyle.right = !isRtl ? null : `${this._tooltipShift}px`;
1038
+ } else {
1039
+ tooltipPositionStyle.top = `${this._tooltipShift}px`;
1040
+ }
1041
+ }
1042
+
1043
+ const contentClasses = {
1044
+ 'd2l-tooltip-content': true,
1045
+ 'd2l-body-small': true,
1046
+ 'vdiff-target': this.showing
1047
+ };
1048
+
1049
+ // Note: role="text" is a workaround for Safari. Otherwise, list-item content is not announced with VoiceOver
1050
+ return html`
1051
+ <div class="d2l-tooltip-container">
1052
+ <div class="d2l-tooltip-position" style=${styleMap(tooltipPositionStyle)}>
1053
+ <div class="${classMap(contentClasses)}" @mouseenter="${this.#onTooltipMouseEnter}" @mouseleave="${this.#onTooltipMouseLeave}">
1054
+ <div role="text">
1055
+ <slot></slot>
1056
+ </div>
1057
+ </div>
1058
+ </div>
1059
+ <div class="d2l-tooltip-pointer d2l-tooltip-pointer-outline">
1060
+ <div></div>
1061
+ </div>
1062
+ <div class="d2l-tooltip-pointer" @mouseenter="${this.#onTooltipMouseEnter}" @mouseleave="${this.#onTooltipMouseLeave}">
1063
+ <div></div>
1064
+ </div>
1065
+ </div>`
1066
+ ;
1067
+ }
637
1068
 
638
- #isHoveringTooltip;
639
- #mouseLeftTooltip;
1069
+ willUpdate(changedProperties) {
1070
+ super.willUpdate(changedProperties);
640
1071
 
641
- _addListeners() {
642
- if (!this._target) {
643
- return;
1072
+ changedProperties.forEach((_, prop) => {
1073
+ if (prop === 'for') {
1074
+ this._updateTarget();
1075
+ } else if (prop === 'forceShow') {
1076
+ this._updateShowing();
1077
+ }
1078
+ });
644
1079
  }
645
- this._target.addEventListener('mouseenter', this._onTargetMouseEnter);
646
- this._target.addEventListener('mouseleave', this._onTargetMouseLeave);
647
- this._target.addEventListener('focus', this._onTargetFocus);
648
- this._target.addEventListener('blur', this._onTargetBlur);
649
- this._target.addEventListener('click', this._onTargetClick);
650
- this._target.addEventListener('touchstart', this._onTargetTouchStart, { passive: true });
651
- this._target.addEventListener('touchcancel', this._onTargetTouchEnd);
652
- this._target.addEventListener('touchend', this._onTargetTouchEnd);
653
1080
 
654
- this._targetSizeObserver = new ResizeObserver(this._onTargetResize);
655
- this._targetSizeObserver.observe(this._target);
656
- }
1081
+ hide() {
1082
+ this._isHovering = false;
1083
+ this._isFocusing = false;
1084
+ this._updateShowing();
1085
+ }
657
1086
 
658
- _computeAvailableSpaces(targetRect, spaceAround) {
659
- const verticalWidth = Math.max(spaceAround.left + targetRect.width + spaceAround.right, 0);
660
- const horizontalHeight = Math.max(spaceAround.above + targetRect.height + spaceAround.below, 0);
661
- const spaces = [
662
- { dir: 'bottom', width: verticalWidth, height: Math.max(spaceAround.below - this.offset, 0) },
663
- { dir: 'top', width: verticalWidth, height: Math.max(spaceAround.above - this.offset, 0) },
664
- { dir: 'right', width: Math.max(spaceAround.right - this.offset, 0), height: horizontalHeight },
665
- { dir: 'left', width: Math.max(spaceAround.left - this.offset, 0), height: horizontalHeight }
666
- ];
667
- if (this.getAttribute('dir') === 'rtl') {
668
- const tmp = spaces[2];
669
- spaces[2] = spaces[3];
670
- spaces[3] = tmp;
671
- }
672
- return spaces;
673
- }
1087
+ show() {
1088
+ this.showing = true;
1089
+ }
1090
+
1091
+ async updatePosition() {
674
1092
 
675
- _computeSpaceAround(offsetParent, targetRect) {
676
-
677
- const boundingContainer = getBoundingAncestor(this);
678
- const bounded = (boundingContainer !== document.documentElement);
679
- const boundingContainerRect = boundingContainer.getBoundingClientRect();
680
-
681
- const spaceAround = (bounded ? {
682
- above: targetRect.top - boundingContainerRect.top - this._viewportMargin,
683
- below: boundingContainerRect.bottom - targetRect.bottom - this._viewportMargin,
684
- left: targetRect.left - boundingContainerRect.left - this._viewportMargin,
685
- right: boundingContainerRect.right - targetRect.right - this._viewportMargin
686
- } : {
687
- above: targetRect.top - this._viewportMargin,
688
- below: window.innerHeight - targetRect.bottom - this._viewportMargin,
689
- left: targetRect.left - this._viewportMargin,
690
- right: document.documentElement.clientWidth - targetRect.right - this._viewportMargin
691
- });
692
-
693
- if (this.boundary && offsetParent) {
694
- const offsetRect = offsetParent.getBoundingClientRect();
695
- if (!isNaN(this.boundary.left)) {
696
- spaceAround.left = Math.min(targetRect.left - offsetRect.left - this.boundary.left, spaceAround.left);
1093
+ if (!this._target) {
1094
+ return;
697
1095
  }
698
- if (!isNaN(this.boundary.right)) {
699
- spaceAround.right = Math.min(offsetRect.right - targetRect.right - this.boundary.right, spaceAround.right);
1096
+
1097
+ const offsetParent = getOffsetParent(this);
1098
+ const targetRect = this._target.getBoundingClientRect();
1099
+ const spaceAround = this._computeSpaceAround(offsetParent, targetRect);
1100
+
1101
+ // Compute the size of the spaces above, below, left and right and find which space to fit the tooltip in
1102
+ const content = this._getContent();
1103
+ if (content === null) return;
1104
+
1105
+ const spaces = this._computeAvailableSpaces(targetRect, spaceAround);
1106
+ const space = await this._fitContentToSpace(content, spaces);
1107
+
1108
+ const contentRect = content.getBoundingClientRect();
1109
+ // + 1 because scrollWidth does not give sub-pixel measurements and half a pixel may cause text to unexpectedly wrap
1110
+ this._maxWidth = Math.min(content.scrollWidth + 2 * contentBorderSize, 350) + 1;
1111
+ this._openDir = space.dir;
1112
+
1113
+ // Compute the x and y position of the tooltip relative to its target
1114
+ let offsetTop, offsetLeft;
1115
+ if (offsetParent && offsetParent.tagName !== 'BODY') {
1116
+ const offsetRect = offsetParent.getBoundingClientRect();
1117
+ offsetTop = offsetRect.top + offsetParent.clientTop - offsetParent.scrollTop;
1118
+ offsetLeft = offsetRect.left + offsetParent.clientLeft - offsetParent.scrollLeft;
1119
+ } else {
1120
+ offsetTop = -document.documentElement.scrollTop;
1121
+ offsetLeft = -document.documentElement.scrollLeft;
700
1122
  }
701
- if (!isNaN(this.boundary.top)) {
702
- spaceAround.above = Math.min(targetRect.top - offsetRect.top - this.boundary.top, spaceAround.above);
1123
+ const top = targetRect.top - offsetTop;
1124
+ const left = targetRect.left - offsetLeft;
1125
+
1126
+ let positionRect;
1127
+ if (this._isAboveOrBelow()) {
1128
+ positionRect = {
1129
+ left,
1130
+ top: this._openDir === 'top' ? top - this.offset : top + targetRect.height + this.offset,
1131
+ width: targetRect.width,
1132
+ height: 0,
1133
+ };
1134
+ } else {
1135
+ positionRect = {
1136
+ left: this._openDir === 'left' ? left - this.offset : left + targetRect.width + this.offset,
1137
+ top,
1138
+ height: targetRect.height,
1139
+ width: 0,
1140
+ };
703
1141
  }
704
- if (!isNaN(this.boundary.bottom)) {
705
- spaceAround.below = Math.min(offsetRect.bottom - targetRect.bottom - this.boundary.bottom, spaceAround.below);
1142
+
1143
+ // Compute how much the tooltip is shifted relative to its pointer
1144
+ if (this._isAboveOrBelow() && (this.align === 'start' || this.align === 'end')) {
1145
+ const shift = Math.min((targetRect.width / 2) - (contentHorizontalPadding + pointerRotatedLength / 2), 0);
1146
+ if (this.align === 'start') {
1147
+ this._tooltipShift = shift;
1148
+ } else {
1149
+ this._tooltipShift = targetRect.width - this._maxWidth - shift;
1150
+ }
1151
+ } else {
1152
+ let spaceLeft, spaceRight, centerDelta, maxShift, minShift;
1153
+ if (this._isAboveOrBelow()) {
1154
+ const isRtl = this.getAttribute('dir') === 'rtl';
1155
+ spaceLeft = !isRtl ? spaceAround.left : spaceAround.right;
1156
+ spaceRight = !isRtl ? spaceAround.right : spaceAround.left;
1157
+ centerDelta = this._maxWidth - targetRect.width;
1158
+ maxShift = targetRect.width / 2;
1159
+ minShift = maxShift - this._maxWidth;
1160
+ } else {
1161
+ spaceLeft = spaceAround.above;
1162
+ spaceRight = spaceAround.below;
1163
+ centerDelta = contentRect.height - targetRect.height;
1164
+ maxShift = targetRect.height / 2;
1165
+ minShift = maxShift - contentRect.height;
1166
+ }
1167
+ const shift = computeTooltipShift(centerDelta, spaceLeft, spaceRight);
1168
+ const shiftMargin = (pointerRotatedLength / 2) + contentBorderRadius;
1169
+ this._tooltipShift = Math.min(Math.max(shift, minShift + shiftMargin), maxShift - shiftMargin);
706
1170
  }
1171
+ this.style.left = `${positionRect.left}px`;
1172
+ this.style.top = `${positionRect.top}px`;
1173
+ this.style.width = `${positionRect.width}px`;
1174
+ this.style.height = `${positionRect.height}px`;
707
1175
  }
708
- const isRTL = this.getAttribute('dir') === 'rtl';
709
- if ((this.align === 'start' && !isRTL) || (this.align === 'end' && isRTL)) {
710
- spaceAround.left = 0;
711
- } else if ((this.align === 'start' && isRTL) || (this.align === 'end' && !isRTL)) {
712
- spaceAround.right = 0;
713
- }
714
- return spaceAround;
715
- }
716
-
717
- _findTarget() {
718
- const ownerRoot = this.getRootNode();
719
1176
 
720
- let target;
721
- if (this.for) {
722
- const targetSelector = `#${cssEscape(this.for)}`;
723
- target = ownerRoot.querySelector(targetSelector);
724
- target = target || ownerRoot?.host?.querySelector(targetSelector);
725
- } else {
726
- const parentNode = this.parentNode;
727
- target = parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE ? ownerRoot.host : parentNode;
1177
+ #isHoveringTooltip;
1178
+ #mouseLeftTooltip;
728
1179
 
729
- // reduce console pollution since Safari + VO prevents inadequate SR experience for tooltips during form validation when using 'for'
730
- if (!(target.tagName === 'D2L-INPUT-TEXT' && target?.invalid)) {
731
- console.warn('<d2l-tooltip>: missing required attribute "for"');
1180
+ _addListeners() {
1181
+ if (!this._target) {
1182
+ return;
732
1183
  }
1184
+ this._target.addEventListener('mouseenter', this._onTargetMouseEnter);
1185
+ this._target.addEventListener('mouseleave', this._onTargetMouseLeave);
1186
+ this._target.addEventListener('focus', this._onTargetFocus);
1187
+ this._target.addEventListener('blur', this._onTargetBlur);
1188
+ this._target.addEventListener('click', this._onTargetClick);
1189
+ this._target.addEventListener('touchstart', this._onTargetTouchStart, { passive: true });
1190
+ this._target.addEventListener('touchcancel', this._onTargetTouchEnd);
1191
+ this._target.addEventListener('touchend', this._onTargetTouchEnd);
1192
+
1193
+ this._targetSizeObserver = new ResizeObserver(this._onTargetResize);
1194
+ this._targetSizeObserver.observe(this._target);
733
1195
  }
734
- return target;
735
- }
736
1196
 
737
- async _fitByBestFit(content, spaces) {
738
- for (let i = 0; i < spaces.length; ++i) {
739
- const space = spaces[i];
740
- this._maxWidth = space.width;
741
- await this.updateComplete;
1197
+ _computeAvailableSpaces(targetRect, spaceAround) {
1198
+ const verticalWidth = Math.max(spaceAround.left + targetRect.width + spaceAround.right, 0);
1199
+ const horizontalHeight = Math.max(spaceAround.above + targetRect.height + spaceAround.below, 0);
1200
+ const spaces = [
1201
+ { dir: 'bottom', width: verticalWidth, height: Math.max(spaceAround.below - this.offset, 0) },
1202
+ { dir: 'top', width: verticalWidth, height: Math.max(spaceAround.above - this.offset, 0) },
1203
+ { dir: 'right', width: Math.max(spaceAround.right - this.offset, 0), height: horizontalHeight },
1204
+ { dir: 'left', width: Math.max(spaceAround.left - this.offset, 0), height: horizontalHeight }
1205
+ ];
1206
+ if (this.getAttribute('dir') === 'rtl') {
1207
+ const tmp = spaces[2];
1208
+ spaces[2] = spaces[3];
1209
+ spaces[3] = tmp;
1210
+ }
1211
+ return spaces;
1212
+ }
742
1213
 
743
- if (content.scrollWidth + 2 * contentBorderSize <= Math.ceil(space.width) && content.scrollHeight + 2 * contentBorderSize <= Math.ceil(space.height)) {
744
- return space;
1214
+ _computeSpaceAround(offsetParent, targetRect) {
1215
+
1216
+ const boundingContainer = getBoundingAncestor(this);
1217
+ const bounded = (boundingContainer !== document.documentElement);
1218
+ const boundingContainerRect = boundingContainer.getBoundingClientRect();
1219
+
1220
+ const spaceAround = (bounded ? {
1221
+ above: targetRect.top - boundingContainerRect.top - this._viewportMargin,
1222
+ below: boundingContainerRect.bottom - targetRect.bottom - this._viewportMargin,
1223
+ left: targetRect.left - boundingContainerRect.left - this._viewportMargin,
1224
+ right: boundingContainerRect.right - targetRect.right - this._viewportMargin
1225
+ } : {
1226
+ above: targetRect.top - this._viewportMargin,
1227
+ below: window.innerHeight - targetRect.bottom - this._viewportMargin,
1228
+ left: targetRect.left - this._viewportMargin,
1229
+ right: document.documentElement.clientWidth - targetRect.right - this._viewportMargin
1230
+ });
1231
+
1232
+ if (this.boundary && offsetParent) {
1233
+ const offsetRect = offsetParent.getBoundingClientRect();
1234
+ if (!isNaN(this.boundary.left)) {
1235
+ spaceAround.left = Math.min(targetRect.left - offsetRect.left - this.boundary.left, spaceAround.left);
1236
+ }
1237
+ if (!isNaN(this.boundary.right)) {
1238
+ spaceAround.right = Math.min(offsetRect.right - targetRect.right - this.boundary.right, spaceAround.right);
1239
+ }
1240
+ if (!isNaN(this.boundary.top)) {
1241
+ spaceAround.above = Math.min(targetRect.top - offsetRect.top - this.boundary.top, spaceAround.above);
1242
+ }
1243
+ if (!isNaN(this.boundary.bottom)) {
1244
+ spaceAround.below = Math.min(offsetRect.bottom - targetRect.bottom - this.boundary.bottom, spaceAround.below);
1245
+ }
745
1246
  }
1247
+ const isRTL = this.getAttribute('dir') === 'rtl';
1248
+ if ((this.align === 'start' && !isRTL) || (this.align === 'end' && isRTL)) {
1249
+ spaceAround.left = 0;
1250
+ } else if ((this.align === 'start' && isRTL) || (this.align === 'end' && !isRTL)) {
1251
+ spaceAround.right = 0;
1252
+ }
1253
+ return spaceAround;
746
1254
  }
747
- return undefined;
748
- }
749
1255
 
750
- async _fitByLargestSpace(spaces) {
751
- let largest = spaces[0];
752
- for (let i = 1; i < spaces.length; ++i) {
753
- const space = spaces[i];
754
- if (space.width * space.height > largest.width * largest.height) {
755
- largest = space;
1256
+ _findTarget() {
1257
+ const ownerRoot = this.getRootNode();
1258
+
1259
+ let target;
1260
+ if (this.for) {
1261
+ const targetSelector = `#${cssEscape(this.for)}`;
1262
+ target = ownerRoot.querySelector(targetSelector);
1263
+ target = target || ownerRoot?.host?.querySelector(targetSelector);
1264
+ } else {
1265
+ const parentNode = this.parentNode;
1266
+ target = parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE ? ownerRoot.host : parentNode;
1267
+
1268
+ // reduce console pollution since Safari + VO prevents inadequate SR experience for tooltips during form validation when using 'for'
1269
+ if (!(target.tagName === 'D2L-INPUT-TEXT' && target?.invalid)) {
1270
+ console.warn('<d2l-tooltip>: missing required attribute "for"');
1271
+ }
756
1272
  }
1273
+ return target;
757
1274
  }
758
- this._maxWidth = largest.width;
759
- await this.updateComplete;
760
- return largest;
761
- }
762
1275
 
763
- async _fitByManualPosition(spaces) {
764
- const space = spaces.filter(space => space.dir === this.position)[0];
765
- if (!space) {
1276
+ async _fitByBestFit(content, spaces) {
1277
+ for (let i = 0; i < spaces.length; ++i) {
1278
+ const space = spaces[i];
1279
+ this._maxWidth = space.width;
1280
+ await this.updateComplete;
1281
+
1282
+ if (content.scrollWidth + 2 * contentBorderSize <= Math.ceil(space.width) && content.scrollHeight + 2 * contentBorderSize <= Math.ceil(space.height)) {
1283
+ return space;
1284
+ }
1285
+ }
766
1286
  return undefined;
767
1287
  }
768
- this._maxWidth = space.width;
769
- await this.updateComplete;
770
- return space;
771
- }
772
1288
 
773
- async _fitContentToSpace(content, spaces) {
774
- // Legacy manual positioning based on the position attribute to allow for backwards compatibility
775
- let space = await this._fitByManualPosition(spaces);
776
- if (space) {
777
- return space;
1289
+ async _fitByLargestSpace(spaces) {
1290
+ let largest = spaces[0];
1291
+ for (let i = 1; i < spaces.length; ++i) {
1292
+ const space = spaces[i];
1293
+ if (space.width * space.height > largest.width * largest.height) {
1294
+ largest = space;
1295
+ }
1296
+ }
1297
+ this._maxWidth = largest.width;
1298
+ await this.updateComplete;
1299
+ return largest;
778
1300
  }
779
- space = await this._fitByBestFit(content, spaces);
780
- if (space) {
1301
+
1302
+ async _fitByManualPosition(spaces) {
1303
+ const space = spaces.filter(space => space.dir === this.position)[0];
1304
+ if (!space) {
1305
+ return undefined;
1306
+ }
1307
+ this._maxWidth = space.width;
1308
+ await this.updateComplete;
781
1309
  return space;
782
1310
  }
783
- return this._fitByLargestSpace(spaces);
784
- }
785
-
786
- _getContent() {
787
- return this.shadowRoot && this.shadowRoot.querySelector('.d2l-tooltip-content');
788
- }
789
-
790
- _isAboveOrBelow() {
791
- return this._openDir === 'bottom' || this._openDir === 'top';
792
- }
793
1311
 
794
- _isInteractive(ele) {
795
- if (!isFocusable(ele, true, false, true)) {
796
- return false;
797
- }
798
- if (ele.nodeType !== Node.ELEMENT_NODE) {
799
- return false;
1312
+ async _fitContentToSpace(content, spaces) {
1313
+ // Legacy manual positioning based on the position attribute to allow for backwards compatibility
1314
+ let space = await this._fitByManualPosition(spaces);
1315
+ if (space) {
1316
+ return space;
1317
+ }
1318
+ space = await this._fitByBestFit(content, spaces);
1319
+ if (space) {
1320
+ return space;
1321
+ }
1322
+ return this._fitByLargestSpace(spaces);
800
1323
  }
801
1324
 
802
- return isInteractive(ele, tooltipInteractiveElements, tooltipInteractiveRoles);
803
- }
804
-
805
- _onTargetBlur() {
806
- this._isFocusing = false;
807
- this._updateShowing();
808
- }
1325
+ _getContent() {
1326
+ return this.shadowRoot && this.shadowRoot.querySelector('.d2l-tooltip-content');
1327
+ }
809
1328
 
810
- _onTargetClick() {
811
- if (this.closeOnClick) {
812
- this.hide();
1329
+ _isAboveOrBelow() {
1330
+ return this._openDir === 'bottom' || this._openDir === 'top';
813
1331
  }
814
- }
815
1332
 
816
- async _onTargetFocus() {
817
- if (this.showTruncatedOnly) {
818
- await this._updateTruncating();
819
- if (!this._truncating) return;
1333
+ _isInteractive(ele) {
1334
+ if (!isFocusable(ele, true, false, true)) {
1335
+ return false;
1336
+ }
1337
+ if (ele.nodeType !== Node.ELEMENT_NODE) {
1338
+ return false;
1339
+ }
1340
+
1341
+ return isInteractive(ele, tooltipInteractiveElements, tooltipInteractiveRoles);
820
1342
  }
821
1343
 
822
- if (this.disableFocusLock) {
823
- this.showing = true;
824
- } else {
825
- this._isFocusing = true;
1344
+ _onTargetBlur() {
1345
+ this._isFocusing = false;
826
1346
  this._updateShowing();
827
1347
  }
828
- }
829
1348
 
830
- _onTargetMouseEnter() {
831
- // came from tooltip so keep showing
832
- if (this.#mouseLeftTooltip) {
833
- this._isHovering = true;
834
- return;
1349
+ _onTargetClick() {
1350
+ if (this.closeOnClick) {
1351
+ this.hide();
1352
+ }
835
1353
  }
836
1354
 
837
- this._hoverTimeout = setTimeout(async() => {
1355
+ async _onTargetFocus() {
838
1356
  if (this.showTruncatedOnly) {
839
1357
  await this._updateTruncating();
840
1358
  if (!this._truncating) return;
841
1359
  }
842
1360
 
843
- this._isHovering = true;
844
- this._updateShowing();
845
- }, getDelay(this.delay));
846
- }
847
-
848
- _onTargetMouseLeave() {
849
- clearTimeout(this._hoverTimeout);
850
- this._isHovering = false;
851
- if (this.showing) resetDelayTimeout();
852
- setTimeout(() => this._updateShowing(), 100); // delay to allow for mouseenter to fire if hovering on tooltip
853
- }
854
-
855
- _onTargetResize() {
856
- this._resizeRunSinceTruncationCheck = true;
857
- if (!this.showing) {
858
- return;
1361
+ if (this.disableFocusLock) {
1362
+ this.showing = true;
1363
+ } else {
1364
+ this._isFocusing = true;
1365
+ this._updateShowing();
1366
+ }
859
1367
  }
860
- this.updatePosition();
861
- }
862
1368
 
863
- _onTargetTouchEnd() {
864
- clearTimeout(this._longPressTimeout);
865
- }
1369
+ _onTargetMouseEnter() {
1370
+ // came from tooltip so keep showing
1371
+ if (this.#mouseLeftTooltip) {
1372
+ this._isHovering = true;
1373
+ return;
1374
+ }
866
1375
 
867
- _onTargetTouchStart() {
868
- this._longPressTimeout = setTimeout(() => {
869
- this._target.focus();
870
- }, 500);
871
- }
1376
+ this._hoverTimeout = setTimeout(async() => {
1377
+ if (this.showTruncatedOnly) {
1378
+ await this._updateTruncating();
1379
+ if (!this._truncating) return;
1380
+ }
872
1381
 
873
- _removeListeners() {
874
- if (!this._target) {
875
- return;
1382
+ this._isHovering = true;
1383
+ this._updateShowing();
1384
+ }, getDelay(this.delay));
876
1385
  }
877
- this._target.removeEventListener('mouseenter', this._onTargetMouseEnter);
878
- this._target.removeEventListener('mouseleave', this._onTargetMouseLeave);
879
- this._target.removeEventListener('focus', this._onTargetFocus);
880
- this._target.removeEventListener('blur', this._onTargetBlur);
881
- this._target.removeEventListener('click', this._onTargetClick);
882
- this._target.removeEventListener('touchstart', this._onTargetTouchStart);
883
- this._target.removeEventListener('touchcancel', this._onTargetTouchEnd);
884
- this._target.removeEventListener('touchend', this._onTargetTouchEnd);
885
1386
 
886
- if (this._targetSizeObserver) {
887
- this._targetSizeObserver.disconnect();
888
- this._targetSizeObserver = null;
1387
+ _onTargetMouseLeave() {
1388
+ clearTimeout(this._hoverTimeout);
1389
+ this._isHovering = false;
1390
+ if (this.showing) resetDelayTimeout();
1391
+ setTimeout(() => this._updateShowing(), 100); // delay to allow for mouseenter to fire if hovering on tooltip
889
1392
  }
890
- }
891
1393
 
892
- async _showingChanged(newValue, dispatch) {
893
- clearTimeout(this._hoverTimeout);
894
- clearTimeout(this._longPressTimeout);
895
- if (newValue) {
896
- if (!this.forceShow) {
897
- if (activeTooltip) activeTooltip.hide();
898
- activeTooltip = this;
1394
+ _onTargetResize() {
1395
+ this._resizeRunSinceTruncationCheck = true;
1396
+ if (!this.showing) {
1397
+ return;
899
1398
  }
1399
+ this.updatePosition();
1400
+ }
900
1401
 
901
- this._dismissibleId = setDismissible(() => this.hide());
902
- this.setAttribute('aria-hidden', 'false');
903
- await this.updateComplete;
904
- await this.updatePosition();
905
- if (dispatch) {
906
- this.dispatchEvent(new CustomEvent(
907
- 'd2l-tooltip-show', { bubbles: true, composed: true }
908
- ));
909
- }
1402
+ _onTargetTouchEnd() {
1403
+ clearTimeout(this._longPressTimeout);
1404
+ }
910
1405
 
911
- if (this.announced && !this._isInteractive(this._target)) announce(this.innerText);
912
- } else {
913
- if (activeTooltip === this) activeTooltip = null;
1406
+ _onTargetTouchStart() {
1407
+ this._longPressTimeout = setTimeout(() => {
1408
+ this._target.focus();
1409
+ }, 500);
1410
+ }
914
1411
 
915
- this.setAttribute('aria-hidden', 'true');
916
- if (this._dismissibleId) {
917
- clearDismissible(this._dismissibleId);
918
- this._dismissibleId = null;
1412
+ _removeListeners() {
1413
+ if (!this._target) {
1414
+ return;
919
1415
  }
920
- if (dispatch) {
921
- this.dispatchEvent(new CustomEvent(
922
- 'd2l-tooltip-hide', { bubbles: true, composed: true }
923
- ));
1416
+ this._target.removeEventListener('mouseenter', this._onTargetMouseEnter);
1417
+ this._target.removeEventListener('mouseleave', this._onTargetMouseLeave);
1418
+ this._target.removeEventListener('focus', this._onTargetFocus);
1419
+ this._target.removeEventListener('blur', this._onTargetBlur);
1420
+ this._target.removeEventListener('click', this._onTargetClick);
1421
+ this._target.removeEventListener('touchstart', this._onTargetTouchStart);
1422
+ this._target.removeEventListener('touchcancel', this._onTargetTouchEnd);
1423
+ this._target.removeEventListener('touchend', this._onTargetTouchEnd);
1424
+
1425
+ if (this._targetSizeObserver) {
1426
+ this._targetSizeObserver.disconnect();
1427
+ this._targetSizeObserver = null;
924
1428
  }
925
1429
  }
926
- }
927
-
928
- _updateShowing() {
929
- this.showing = this._isFocusing || this._isHovering || this.forceShow || this.#isHoveringTooltip;
930
- }
931
1430
 
932
- _updateTarget() {
933
- const newTarget = this._findTarget();
934
- if (this._target === newTarget) {
935
- return;
936
- }
1431
+ async _showingChanged(newValue, dispatch) {
1432
+ clearTimeout(this._hoverTimeout);
1433
+ clearTimeout(this._longPressTimeout);
1434
+ if (newValue) {
1435
+ if (!this.forceShow) {
1436
+ if (activeTooltip) activeTooltip.hide();
1437
+ activeTooltip = this;
1438
+ }
937
1439
 
938
- this._removeListeners();
939
- this._target = newTarget;
1440
+ this._dismissibleId = setDismissible(() => this.hide());
1441
+ this.setAttribute('aria-hidden', 'false');
1442
+ await this.updateComplete;
1443
+ await this.updatePosition();
1444
+ if (dispatch) {
1445
+ this.dispatchEvent(new CustomEvent(
1446
+ 'd2l-tooltip-show', { bubbles: true, composed: true }
1447
+ ));
1448
+ }
940
1449
 
941
- if (this._target) {
942
- const targetDisabled = this._target.hasAttribute('disabled') || this._target.getAttribute('aria-disabled') === 'true';
1450
+ if (this.announced && !isInteractiveTarget(this._target)) announce(this.innerText);
1451
+ } else {
1452
+ if (activeTooltip === this) activeTooltip = null;
943
1453
 
944
- const isTargetInteractive = this._isInteractive(this._target);
945
- this.id = this.id || getUniqueId();
946
- this.setAttribute('role', 'tooltip');
947
- if (this.forType === 'label') {
948
- elemIdListAdd(this._target, 'aria-labelledby', this.id);
949
- } else if (!this.announced || isTargetInteractive) {
950
- elemIdListAdd(this._target, 'aria-describedby', this.id);
951
- }
952
- if (logAccessibilityWarning && !isTargetInteractive && !this.announced) {
953
- console.warn(
954
- 'd2l-tooltip may be being used in a non-accessible manner; it should be attached to interactive elements like \'a\', \'button\',' +
955
- '\'input\'', '\'select\', \'textarea\' or static / custom elements if a role has been set and the element is focusable.',
956
- this._target
957
- );
958
- logAccessibilityWarning = false;
959
- }
960
- if (this.showing) {
961
- this.updatePosition();
962
- } else if (!targetDisabled && isComposedAncestor(this._target, getComposedActiveElement())) {
963
- this._onTargetFocus();
1454
+ this.setAttribute('aria-hidden', 'true');
1455
+ if (this._dismissibleId) {
1456
+ clearDismissible(this._dismissibleId);
1457
+ this._dismissibleId = null;
1458
+ }
1459
+ if (dispatch) {
1460
+ this.dispatchEvent(new CustomEvent(
1461
+ 'd2l-tooltip-hide', { bubbles: true, composed: true }
1462
+ ));
1463
+ }
964
1464
  }
965
1465
  }
966
- this._addListeners();
967
- }
968
1466
 
969
- /**
970
- * This solution appends a clone of the target to the target in order to retain target styles.
971
- * A possible consequence of this is unexpected behaviours for web components that have slots.
972
- * If this becomes an issue, it would also likely be possible to append the clone to document.body
973
- * and get the expected styles through getComputedStyle.
974
- */
975
- async _updateTruncating() {
976
- // if no resize has happened since truncation was previously calculated the result will not have changed
977
- if (!this._resizeRunSinceTruncationCheck || !this.showTruncatedOnly) return;
978
-
979
- const target = this._target;
980
- const cloneContainer = document.createElement('div');
981
- cloneContainer.style.position = 'absolute';
982
- cloneContainer.style.overflow = 'hidden';
983
- cloneContainer.style.whiteSpace = 'nowrap';
984
- cloneContainer.style.width = '1px';
985
-
986
- if (this.getAttribute('dir') === 'rtl') {
987
- cloneContainer.style.right = '-10000px';
988
- } else {
989
- cloneContainer.style.left = '-10000px';
1467
+ _updateShowing() {
1468
+ this.showing = this._isFocusing || this._isHovering || this.forceShow || this.#isHoveringTooltip;
990
1469
  }
991
1470
 
992
- const clone = target.cloneNode(true);
993
- clone.removeAttribute('id');
994
- clone.style.maxWidth = 'none';
995
- clone.style.display = 'inline-block';
1471
+ _updateTarget() {
1472
+ const newTarget = this._findTarget();
1473
+ if (this._target === newTarget) {
1474
+ return;
1475
+ }
996
1476
 
997
- cloneContainer.appendChild(clone);
998
- target.appendChild(cloneContainer);
999
- await this.updateComplete;
1477
+ this._removeListeners();
1478
+ this._target = newTarget;
1000
1479
 
1001
- // if the clone is a web component it needs to update to fill in any slots
1002
- const customElm = customElements.get(clone.localName);
1003
- if (customElm !== undefined) {
1004
- clone.requestUpdate();
1005
- await clone.updateComplete;
1480
+ if (this._target) {
1481
+ const targetDisabled = this._target.hasAttribute('disabled') || this._target.getAttribute('aria-disabled') === 'true';
1482
+
1483
+ const isTargetInteractive = isInteractiveTarget(this._target);
1484
+ this.id = this.id || getUniqueId();
1485
+ this.setAttribute('role', 'tooltip');
1486
+ if (this.forType === 'label') {
1487
+ elemIdListAdd(this._target, 'aria-labelledby', this.id);
1488
+ } else if (!this.announced || isTargetInteractive) {
1489
+ elemIdListAdd(this._target, 'aria-describedby', this.id);
1490
+ }
1491
+ if (logAccessibilityWarning && !isTargetInteractive && !this.announced) {
1492
+ console.warn(
1493
+ 'd2l-tooltip may be being used in a non-accessible manner; it should be attached to interactive elements like \'a\', \'button\',' +
1494
+ '\'input\'', '\'select\', \'textarea\' or static / custom elements if a role has been set and the element is focusable.',
1495
+ this._target
1496
+ );
1497
+ logAccessibilityWarning = false;
1498
+ }
1499
+ if (this.showing) {
1500
+ this.updatePosition();
1501
+ } else if (!targetDisabled && isComposedAncestor(this._target, getComposedActiveElement())) {
1502
+ this._onTargetFocus();
1503
+ }
1504
+ }
1505
+ this._addListeners();
1006
1506
  }
1007
1507
 
1008
- this._truncating = (clone.scrollWidth - target.offsetWidth) > 2; // Safari adds 1px to scrollWidth necessitating a subtraction comparison.
1009
- this._resizeRunSinceTruncationCheck = false;
1010
- target.removeChild(cloneContainer);
1011
- }
1508
+ /**
1509
+ * This solution appends a clone of the target to the target in order to retain target styles.
1510
+ * A possible consequence of this is unexpected behaviours for web components that have slots.
1511
+ * If this becomes an issue, it would also likely be possible to append the clone to document.body
1512
+ * and get the expected styles through getComputedStyle.
1513
+ */
1514
+ async _updateTruncating() {
1515
+ // if no resize has happened since truncation was previously calculated the result will not have changed
1516
+ if (!this._resizeRunSinceTruncationCheck || !this.showTruncatedOnly) return;
1517
+
1518
+ const target = this._target;
1519
+ const cloneContainer = document.createElement('div');
1520
+ cloneContainer.style.position = 'absolute';
1521
+ cloneContainer.style.overflow = 'hidden';
1522
+ cloneContainer.style.whiteSpace = 'nowrap';
1523
+ cloneContainer.style.width = '1px';
1524
+
1525
+ if (this.getAttribute('dir') === 'rtl') {
1526
+ cloneContainer.style.right = '-10000px';
1527
+ } else {
1528
+ cloneContainer.style.left = '-10000px';
1529
+ }
1012
1530
 
1013
- #onTooltipMouseEnter() {
1014
- if (!this.showing) return;
1015
- this.#isHoveringTooltip = true;
1016
- this._updateShowing();
1017
- }
1531
+ const clone = target.cloneNode(true);
1532
+ clone.removeAttribute('id');
1533
+ clone.style.maxWidth = 'none';
1534
+ clone.style.display = 'inline-block';
1018
1535
 
1019
- #onTooltipMouseLeave() {
1020
- clearTimeout(this._mouseLeaveTimeout);
1536
+ cloneContainer.appendChild(clone);
1537
+ target.appendChild(cloneContainer);
1538
+ await this.updateComplete;
1021
1539
 
1022
- this.#isHoveringTooltip = false;
1023
- this.#mouseLeftTooltip = true;
1024
- resetDelayTimeout();
1540
+ // if the clone is a web component it needs to update to fill in any slots
1541
+ const customElm = customElements.get(clone.localName);
1542
+ if (customElm !== undefined) {
1543
+ clone.requestUpdate();
1544
+ await clone.updateComplete;
1545
+ }
1025
1546
 
1026
- this._mouseLeaveTimeout = setTimeout(() => {
1027
- this.#mouseLeftTooltip = false;
1547
+ this._truncating = (clone.scrollWidth - target.offsetWidth) > 2; // Safari adds 1px to scrollWidth necessitating a subtraction comparison.
1548
+ this._resizeRunSinceTruncationCheck = false;
1549
+ target.removeChild(cloneContainer);
1550
+ }
1551
+
1552
+ #onTooltipMouseEnter() {
1553
+ if (!this.showing) return;
1554
+ this.#isHoveringTooltip = true;
1028
1555
  this._updateShowing();
1029
- }, 100); // delay to allow for mouseenter to fire if hovering on target
1556
+ }
1557
+
1558
+ #onTooltipMouseLeave() {
1559
+ clearTimeout(this._mouseLeaveTimeout);
1560
+
1561
+ this.#isHoveringTooltip = false;
1562
+ this.#mouseLeftTooltip = true;
1563
+ resetDelayTimeout();
1564
+
1565
+ this._mouseLeaveTimeout = setTimeout(() => {
1566
+ this.#mouseLeftTooltip = false;
1567
+ this._updateShowing();
1568
+ }, 100); // delay to allow for mouseenter to fire if hovering on target
1569
+ }
1030
1570
  }
1571
+ customElements.define('d2l-tooltip', Tooltip);
1572
+
1031
1573
  }
1032
- customElements.define('d2l-tooltip', Tooltip);