@brightspace-ui/core 3.223.0 → 3.224.0

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