@brightspace-ui/core 3.143.1 → 3.144.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.
- package/components/tooltip/demo/tooltip.html +40 -1
- package/components/tooltip/tooltip.js +1378 -837
- package/custom-elements.json +5 -3
- package/package.json +1 -1
@@ -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
|
-
|
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
|
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
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
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
|
-
|
185
|
-
|
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
|
-
|
190
|
-
text-align: right;
|
191
|
-
}
|
61
|
+
if (usePopoverMixin) {
|
192
62
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
235
|
-
|
236
|
-
clip: rect(${pointerOverhang + contentBorderSize}px, 21px, 22px, -3px);
|
237
|
-
}
|
218
|
+
requestAnimationFrame(() => this.#updateTarget());
|
219
|
+
}
|
238
220
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
}
|
221
|
+
disconnectedCallback() {
|
222
|
+
super.disconnectedCallback();
|
223
|
+
if (activeTooltip === this) activeTooltip = null;
|
243
224
|
|
244
|
-
|
245
|
-
|
246
|
-
top: calc(50% - ${pointerLength / 2}px);
|
247
|
-
}
|
225
|
+
this.#removeListeners();
|
226
|
+
window.removeEventListener('resize', this.#handleTargetResizeBound);
|
248
227
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
}
|
228
|
+
clearDismissible(this.#dismissibleId);
|
229
|
+
delayTimeoutId = null;
|
230
|
+
this.#dismissibleId = null;
|
253
231
|
|
254
|
-
|
255
|
-
|
256
|
-
|
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
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
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
|
-
|
274
|
-
|
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
|
-
|
278
|
-
|
279
|
-
}
|
251
|
+
return this.renderPopover(content);
|
252
|
+
}
|
280
253
|
|
281
|
-
|
282
|
-
|
283
|
-
}
|
254
|
+
willUpdate(changedProperties) {
|
255
|
+
super.willUpdate(changedProperties);
|
284
256
|
|
285
|
-
|
286
|
-
|
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
|
-
.
|
290
|
-
|
291
|
-
|
265
|
+
changedProperties.forEach((_, prop) => {
|
266
|
+
if (prop === 'for') this.#updateTarget();
|
267
|
+
else if (prop === 'forceShow') this.#updateShowing();
|
268
|
+
});
|
269
|
+
}
|
292
270
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
}
|
271
|
+
hide() {
|
272
|
+
this.#isHovering = false;
|
273
|
+
this.#isFocusing = false;
|
274
|
+
this.#updateShowing();
|
275
|
+
}
|
299
276
|
|
300
|
-
|
301
|
-
|
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
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
324
|
-
|
325
|
-
|
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
|
-
|
329
|
-
|
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
|
-
.
|
339
|
-
|
340
|
-
|
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
|
-
|
344
|
-
|
345
|
-
|
346
|
-
}
|
333
|
+
this.#targetSizeObserver = new ResizeObserver(this.#handleTargetResizeBound);
|
334
|
+
this.#targetSizeObserver.observe(this.#target);
|
335
|
+
}
|
347
336
|
|
348
|
-
|
349
|
-
|
350
|
-
animation: d2l-tooltip-top-animation 200ms ease;
|
351
|
-
}
|
337
|
+
#findTarget() {
|
338
|
+
const ownerRoot = this.getRootNode();
|
352
339
|
|
353
|
-
|
354
|
-
|
355
|
-
|
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
|
-
|
359
|
-
|
360
|
-
|
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
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
357
|
+
#handleTargetBlur() {
|
358
|
+
this.#isFocusing = false;
|
359
|
+
this.#updateShowing();
|
360
|
+
}
|
367
361
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
padding-right: 1rem;
|
372
|
-
}
|
362
|
+
#handleTargetClick() {
|
363
|
+
if (this.closeOnClick) this.hide();
|
364
|
+
}
|
373
365
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
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
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
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
|
-
|
397
|
-
|
398
|
-
|
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
|
-
|
402
|
-
.
|
403
|
-
|
404
|
-
|
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
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
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
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
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
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
405
|
+
#handleTargetResize() {
|
406
|
+
this.#resizeRunSinceTruncationCheck = true;
|
407
|
+
if (!this.showing) return;
|
408
|
+
super.position();
|
409
|
+
}
|
459
410
|
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
}
|
464
|
-
});
|
465
|
-
}
|
411
|
+
#handleTargetTouchEnd() {
|
412
|
+
clearTimeout(this.#longPressTimeout);
|
413
|
+
}
|
466
414
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
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
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
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
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
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
|
-
|
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
|
-
|
505
|
-
|
506
|
-
|
507
|
-
'vdiff-target': this.showing
|
508
|
-
};
|
496
|
+
#updateShowing() {
|
497
|
+
this.showing = this.#isFocusing || this.#isHovering || this.forceShow || this.#isHoveringTooltip;
|
498
|
+
}
|
509
499
|
|
510
|
-
|
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
|
-
|
531
|
-
super.willUpdate(changedProperties);
|
502
|
+
if (!this.isConnected) return;
|
532
503
|
|
533
|
-
|
534
|
-
if (
|
535
|
-
|
536
|
-
|
537
|
-
|
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
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
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
|
-
|
584
|
+
} else {
|
553
585
|
|
554
|
-
|
555
|
-
return;
|
556
|
-
}
|
586
|
+
// Cleanup: GAUD-7355-tooltip-popover - remove this entire block and unused imports
|
557
587
|
|
558
|
-
|
559
|
-
|
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
|
-
|
563
|
-
|
564
|
-
|
591
|
+
/* rotated 45 degrees */
|
592
|
+
const pointerRotatedLength = Math.SQRT2 * pointerLength;
|
593
|
+
const pointerRotatedOverhang = ((pointerRotatedLength - pointerLength) / 2) + pointerOverhang;
|
565
594
|
|
566
|
-
|
567
|
-
|
595
|
+
const pointerGap = 0; /* spacing between pointer and target */
|
596
|
+
const defaultViewportMargin = 18;
|
597
|
+
const contentBorderRadius = 6;
|
598
|
+
const outlineSize = 1;
|
568
599
|
|
569
|
-
|
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
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
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
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
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
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
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
|
-
}
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
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
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
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
|
-
|
639
|
-
|
1069
|
+
willUpdate(changedProperties) {
|
1070
|
+
super.willUpdate(changedProperties);
|
640
1071
|
|
641
|
-
|
642
|
-
|
643
|
-
|
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
|
-
|
655
|
-
|
656
|
-
|
1081
|
+
hide() {
|
1082
|
+
this._isHovering = false;
|
1083
|
+
this._isFocusing = false;
|
1084
|
+
this._updateShowing();
|
1085
|
+
}
|
657
1086
|
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
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
|
-
|
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
|
-
|
699
|
-
|
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
|
-
|
702
|
-
|
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
|
-
|
705
|
-
|
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
|
-
|
721
|
-
|
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
|
-
|
730
|
-
if (!
|
731
|
-
|
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
|
-
|
738
|
-
|
739
|
-
const
|
740
|
-
|
741
|
-
|
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
|
-
|
744
|
-
|
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
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
if (
|
755
|
-
|
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
|
-
|
764
|
-
|
765
|
-
|
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
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
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
|
-
|
780
|
-
|
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
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
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
|
-
|
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
|
-
|
811
|
-
|
812
|
-
this.hide();
|
1329
|
+
_isAboveOrBelow() {
|
1330
|
+
return this._openDir === 'bottom' || this._openDir === 'top';
|
813
1331
|
}
|
814
|
-
}
|
815
1332
|
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
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
|
-
|
823
|
-
this.
|
824
|
-
} else {
|
825
|
-
this._isFocusing = true;
|
1344
|
+
_onTargetBlur() {
|
1345
|
+
this._isFocusing = false;
|
826
1346
|
this._updateShowing();
|
827
1347
|
}
|
828
|
-
}
|
829
1348
|
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
return;
|
1349
|
+
_onTargetClick() {
|
1350
|
+
if (this.closeOnClick) {
|
1351
|
+
this.hide();
|
1352
|
+
}
|
835
1353
|
}
|
836
1354
|
|
837
|
-
|
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.
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
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
|
-
|
864
|
-
|
865
|
-
|
1369
|
+
_onTargetMouseEnter() {
|
1370
|
+
// came from tooltip so keep showing
|
1371
|
+
if (this.#mouseLeftTooltip) {
|
1372
|
+
this._isHovering = true;
|
1373
|
+
return;
|
1374
|
+
}
|
866
1375
|
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
1376
|
+
this._hoverTimeout = setTimeout(async() => {
|
1377
|
+
if (this.showTruncatedOnly) {
|
1378
|
+
await this._updateTruncating();
|
1379
|
+
if (!this._truncating) return;
|
1380
|
+
}
|
872
1381
|
|
873
|
-
|
874
|
-
|
875
|
-
|
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
|
-
|
887
|
-
this.
|
888
|
-
this.
|
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
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
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
|
-
|
902
|
-
this.
|
903
|
-
|
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
|
-
|
912
|
-
|
913
|
-
|
1406
|
+
_onTargetTouchStart() {
|
1407
|
+
this._longPressTimeout = setTimeout(() => {
|
1408
|
+
this._target.focus();
|
1409
|
+
}, 500);
|
1410
|
+
}
|
914
1411
|
|
915
|
-
|
916
|
-
if (this.
|
917
|
-
|
918
|
-
this._dismissibleId = null;
|
1412
|
+
_removeListeners() {
|
1413
|
+
if (!this._target) {
|
1414
|
+
return;
|
919
1415
|
}
|
920
|
-
|
921
|
-
|
922
|
-
|
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
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
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
|
-
|
939
|
-
|
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
|
-
|
942
|
-
|
1450
|
+
if (this.announced && !isInteractiveTarget(this._target)) announce(this.innerText);
|
1451
|
+
} else {
|
1452
|
+
if (activeTooltip === this) activeTooltip = null;
|
943
1453
|
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
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
|
-
|
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
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
1471
|
+
_updateTarget() {
|
1472
|
+
const newTarget = this._findTarget();
|
1473
|
+
if (this._target === newTarget) {
|
1474
|
+
return;
|
1475
|
+
}
|
996
1476
|
|
997
|
-
|
998
|
-
|
999
|
-
await this.updateComplete;
|
1477
|
+
this._removeListeners();
|
1478
|
+
this._target = newTarget;
|
1000
1479
|
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
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
|
-
|
1009
|
-
|
1010
|
-
|
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
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
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
|
-
|
1020
|
-
|
1536
|
+
cloneContainer.appendChild(clone);
|
1537
|
+
target.appendChild(cloneContainer);
|
1538
|
+
await this.updateComplete;
|
1021
1539
|
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
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
|
-
|
1027
|
-
this
|
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
|
-
}
|
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);
|