@brightspace-ui/core 3.74.1 → 3.75.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/popover/demo/popover.html +242 -17
- package/components/popover/popover-mixin.js +556 -50
- package/components/selection/demo/selection.html +2 -2
- package/components/selection/selection-action-dropdown.js +5 -4
- package/components/selection/selection-action-mixin.js +26 -3
- package/components/selection/selection-action.js +4 -3
- package/custom-elements.json +125 -6
- package/helpers/mathjax.js +4 -1
- package/lang/ar.js +2 -1
- package/lang/cy.js +2 -1
- package/lang/da.js +2 -1
- package/lang/de.js +2 -1
- package/lang/en-gb.js +2 -1
- package/lang/en.js +2 -1
- package/lang/es-es.js +2 -1
- package/lang/es.js +2 -1
- package/lang/fr-fr.js +2 -1
- package/lang/fr.js +2 -1
- package/lang/hi.js +2 -1
- package/lang/ja.js +2 -1
- package/lang/ko.js +2 -1
- package/lang/nl.js +2 -1
- package/lang/pt.js +2 -1
- package/lang/sv.js +2 -1
- package/lang/tr.js +2 -1
- package/lang/zh-cn.js +2 -1
- package/lang/zh-tw.js +2 -1
- package/package.json +1 -1
@@ -1,10 +1,19 @@
|
|
1
1
|
import '../colors/colors.js';
|
2
2
|
import '../focus-trap/focus-trap.js';
|
3
3
|
import { clearDismissible, setDismissible } from '../../helpers/dismissible.js';
|
4
|
-
import { css, html } from 'lit';
|
4
|
+
import { css, html, nothing } from 'lit';
|
5
5
|
import { getComposedActiveElement, getFirstFocusableDescendant, getPreviousFocusableAncestor } from '../../helpers/focus.js';
|
6
|
-
import { isComposedAncestor } from '../../helpers/dom.js';
|
7
|
-
|
6
|
+
import { getComposedParent, isComposedAncestor } from '../../helpers/dom.js';
|
7
|
+
import { _offscreenStyleDeclarations } from '../offscreen/offscreen.js';
|
8
|
+
import { styleMap } from 'lit/directives/style-map.js';
|
9
|
+
|
10
|
+
const defaultPreferredPosition = {
|
11
|
+
location: 'block-end', // block-start, block-end
|
12
|
+
span: 'all', // start, end, all
|
13
|
+
allowFlip: true
|
14
|
+
};
|
15
|
+
const pointerLength = 16;
|
16
|
+
const pointerRotatedLength = Math.SQRT2 * parseFloat(pointerLength);
|
8
17
|
const isSupported = ('popover' in HTMLElement.prototype);
|
9
18
|
|
10
19
|
// eslint-disable-next-line no-console
|
@@ -14,11 +23,27 @@ export const PopoverMixin = superclass => class extends superclass {
|
|
14
23
|
|
15
24
|
static get properties() {
|
16
25
|
return {
|
26
|
+
_contentHeight: { state: true },
|
27
|
+
_location: { type: String, reflect: true, attribute: '_location' },
|
28
|
+
_margin: { state: true },
|
29
|
+
_maxHeight: { state: true },
|
30
|
+
_maxWidth: { state: true },
|
31
|
+
_minHeight: { state: true },
|
32
|
+
_minWidth: { state: true },
|
17
33
|
_noAutoClose: { state: true },
|
34
|
+
_noAutoFit: { state: true },
|
18
35
|
_noAutoFocus: { state: true },
|
36
|
+
_noPointer: { state: true },
|
37
|
+
_offscreen: { type: Boolean, reflect: true, attribute: '_offscreen' },
|
38
|
+
_offset: { state: true },
|
19
39
|
_opened: { type: Boolean, reflect: true, attribute: '_opened' },
|
40
|
+
_pointerPosition: { state: true },
|
41
|
+
_position: { state: true },
|
42
|
+
_preferredPosition: { state: true },
|
43
|
+
_rtl: { state: true },
|
20
44
|
_trapFocus: { state: true },
|
21
|
-
_useNativePopover: { type: String, reflect: true, attribute: 'popover' }
|
45
|
+
_useNativePopover: { type: String, reflect: true, attribute: 'popover' },
|
46
|
+
_width: { state: true }
|
22
47
|
};
|
23
48
|
}
|
24
49
|
|
@@ -31,18 +56,18 @@ export const PopoverMixin = superclass => class extends superclass {
|
|
31
56
|
--d2l-popover-default-border-radius: 0.3rem;
|
32
57
|
--d2l-popover-default-foreground-color: var(--d2l-color-ferrite);
|
33
58
|
--d2l-popover-default-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.15);
|
34
|
-
background-color: transparent;
|
35
|
-
border: none;
|
59
|
+
background-color: transparent; /* override popover default */
|
60
|
+
border: none; /* override popover */
|
36
61
|
box-sizing: border-box;
|
37
62
|
color: var(--d2l-popover-foreground-color, var(--d2l-popover-default-foreground-color));
|
38
63
|
display: none;
|
39
|
-
height: fit-content;
|
40
|
-
inset: 0;
|
41
|
-
margin:
|
42
|
-
overflow: visible;
|
43
|
-
padding: 0;
|
44
|
-
position: fixed;
|
45
|
-
width: fit-content;
|
64
|
+
height: fit-content; /* normalize popover */
|
65
|
+
inset: 0; /* normalize popover */
|
66
|
+
margin: 0; /* override popover */
|
67
|
+
overflow: visible; /* override popover */
|
68
|
+
padding: 0; /* override popover */
|
69
|
+
position: fixed; /* normalize popover */
|
70
|
+
width: fit-content; /* normalize popover */
|
46
71
|
}
|
47
72
|
:host([hidden]) {
|
48
73
|
display: none;
|
@@ -53,14 +78,56 @@ export const PopoverMixin = superclass => class extends superclass {
|
|
53
78
|
:host([_opened]) {
|
54
79
|
display: inline-block;
|
55
80
|
}
|
81
|
+
:host([_location="block-start"]) {
|
82
|
+
bottom: 0;
|
83
|
+
top: auto;
|
84
|
+
}
|
56
85
|
|
57
|
-
.content {
|
86
|
+
.content-position {
|
87
|
+
display: inline-block;
|
88
|
+
position: absolute;
|
89
|
+
}
|
90
|
+
.content-width {
|
58
91
|
background-color: var(--d2l-popover-background-color, var(--d2l-popover-default-background-color));
|
59
92
|
border: 1px solid var(--d2l-popover-border-color, var(--d2l-popover-default-border-color));
|
60
93
|
border-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
|
61
94
|
box-shadow: var(--d2l-popover-shadow, var(--d2l-popover-default-shadow));
|
62
95
|
box-sizing: border-box;
|
96
|
+
max-width: 370px;
|
97
|
+
min-width: 70px;
|
98
|
+
width: 100vw;
|
99
|
+
}
|
100
|
+
.content-container {
|
101
|
+
box-sizing: border-box;
|
102
|
+
display: inline-block;
|
103
|
+
max-width: 100%;
|
63
104
|
outline: none;
|
105
|
+
overflow-y: auto;
|
106
|
+
}
|
107
|
+
|
108
|
+
.pointer {
|
109
|
+
clip: rect(-5px, 21px, 8px, -7px);
|
110
|
+
display: inline-block;
|
111
|
+
position: absolute;
|
112
|
+
z-index: 1;
|
113
|
+
}
|
114
|
+
|
115
|
+
.pointer > div {
|
116
|
+
background-color: var(--d2l-popover-background-color, var(--d2l-popover-default-background-color));
|
117
|
+
border: 1px solid var(--d2l-popover-border-color, var(--d2l-popover-default-border-color));
|
118
|
+
border-radius: 0.1rem;
|
119
|
+
box-shadow: -4px -4px 12px -5px rgba(32, 33, 34, 0.2); /* ferrite */
|
120
|
+
height: ${pointerLength}px;
|
121
|
+
transform: rotate(45deg);
|
122
|
+
width: ${pointerLength}px;
|
123
|
+
}
|
124
|
+
|
125
|
+
:host([_location="block-start"]) .pointer {
|
126
|
+
clip: rect(9px, 21px, 22px, -3px);
|
127
|
+
}
|
128
|
+
|
129
|
+
:host([_location="block-start"]) .pointer > div {
|
130
|
+
box-shadow: 4px 4px 12px -5px rgba(32, 33, 34, 0.2); /* ferrite */
|
64
131
|
}
|
65
132
|
|
66
133
|
@keyframes d2l-popover-animation {
|
@@ -72,6 +139,10 @@ export const PopoverMixin = superclass => class extends superclass {
|
|
72
139
|
animation: var(--d2l-popover-animation-name, var(--d2l-popover-default-animation-name)) 300ms ease;
|
73
140
|
}
|
74
141
|
}
|
142
|
+
|
143
|
+
:host([_offscreen]) {
|
144
|
+
${_offscreenStyleDeclarations}
|
145
|
+
}
|
75
146
|
`;
|
76
147
|
}
|
77
148
|
|
@@ -79,19 +150,26 @@ export const PopoverMixin = superclass => class extends superclass {
|
|
79
150
|
super();
|
80
151
|
this.configure();
|
81
152
|
this._useNativePopover = isSupported ? 'manual' : undefined;
|
82
|
-
this
|
83
|
-
this
|
153
|
+
this.#handleAncestorMutationBound = this.#handleAncestorMutation.bind(this);
|
154
|
+
this.#handleAutoCloseClickBound = this.#handleAutoCloseClick.bind(this);
|
155
|
+
this.#handleAutoCloseFocusBound = this.#handleAutoCloseFocus.bind(this);
|
156
|
+
this.#handleResizeBound = this.#handleResize.bind(this);
|
157
|
+
this.#repositionBound = this.#reposition.bind(this);
|
84
158
|
}
|
85
159
|
|
86
160
|
connectedCallback() {
|
87
161
|
super.connectedCallback();
|
88
|
-
if (this._opened)
|
162
|
+
if (this._opened) {
|
163
|
+
this.#addAutoCloseHandlers();
|
164
|
+
this.#addRepositionHandlers();
|
165
|
+
}
|
89
166
|
}
|
90
167
|
|
91
168
|
disconnectedCallback() {
|
92
169
|
super.disconnectedCallback();
|
93
|
-
this
|
94
|
-
this
|
170
|
+
this.#removeAutoCloseHandlers();
|
171
|
+
this.#removeRepositionHandlers();
|
172
|
+
this.#clearDismissible();
|
95
173
|
}
|
96
174
|
|
97
175
|
async close() {
|
@@ -102,22 +180,44 @@ export const PopoverMixin = superclass => class extends superclass {
|
|
102
180
|
if (this._useNativePopover) this.hidePopover();
|
103
181
|
|
104
182
|
this._previousFocusableAncestor = null;
|
105
|
-
this
|
106
|
-
this
|
183
|
+
this.#removeAutoCloseHandlers();
|
184
|
+
this.#removeRepositionHandlers();
|
185
|
+
this.#clearDismissible();
|
107
186
|
await this.updateComplete; // wait before applying focus to opener
|
108
|
-
this
|
187
|
+
this.#focusOpener();
|
109
188
|
this.dispatchEvent(new CustomEvent('d2l-popover-close', { bubbles: true, composed: true }));
|
189
|
+
|
110
190
|
}
|
111
191
|
|
112
192
|
configure(properties) {
|
193
|
+
this._margin = properties?.margin ?? 18;
|
194
|
+
this._maxHeight = properties?.maxHeight;
|
195
|
+
this._maxWidth = properties?.maxWidth;
|
196
|
+
this._minHeight = properties?.minHeight;
|
197
|
+
this._minWidth = properties?.minWidth;
|
113
198
|
this._noAutoClose = properties?.noAutoClose ?? false;
|
199
|
+
this._noAutoFit = properties?.noAutoFit ?? false;
|
114
200
|
this._noAutoFocus = properties?.noAutoFocus ?? false;
|
201
|
+
this._noPointer = properties?.noPointer ?? false;
|
202
|
+
this._offset = properties?.offset ?? 16;
|
203
|
+
if (!properties) {
|
204
|
+
this._preferredPosition = defaultPreferredPosition;
|
205
|
+
} else if (this._preferredPosition?.location !== properties.position?.location
|
206
|
+
|| this._preferredPosition?.span !== properties.position?.span
|
207
|
+
|| this._preferredPosition?.allowFlip !== properties.position?.allowFlip) {
|
208
|
+
this._preferredPosition = {
|
209
|
+
location: properties?.position?.location ?? 'block-end',
|
210
|
+
span: properties?.position?.span ?? 'all',
|
211
|
+
allowFlip: properties?.position?.allowFlip ?? true
|
212
|
+
};
|
213
|
+
}
|
115
214
|
this._trapFocus = properties?.trapFocus ?? false;
|
116
215
|
}
|
117
216
|
|
118
217
|
async open(applyFocus = true) {
|
119
218
|
if (this._opened) return;
|
120
219
|
|
220
|
+
this._rtl = document.documentElement.getAttribute('dir') === 'rtl';
|
121
221
|
this._applyFocus = applyFocus !== undefined ? applyFocus : true;
|
122
222
|
this._opened = true;
|
123
223
|
|
@@ -127,10 +227,73 @@ export const PopoverMixin = superclass => class extends superclass {
|
|
127
227
|
this._previousFocusableAncestor = getPreviousFocusableAncestor(this, false, false);
|
128
228
|
|
129
229
|
this._opener = getComposedActiveElement();
|
130
|
-
this
|
230
|
+
this.#addAutoCloseHandlers();
|
231
|
+
|
232
|
+
await this.#position();
|
233
|
+
|
131
234
|
this._dismissibleId = setDismissible(() => this.close());
|
132
|
-
|
235
|
+
|
236
|
+
this.#focusContent(this);
|
237
|
+
|
238
|
+
this.#addRepositionHandlers();
|
239
|
+
|
133
240
|
this.dispatchEvent(new CustomEvent('d2l-popover-open', { bubbles: true, composed: true }));
|
241
|
+
|
242
|
+
}
|
243
|
+
|
244
|
+
renderPopover(content) {
|
245
|
+
|
246
|
+
const stylesMap = this.#getStyleMaps();
|
247
|
+
const widthStyle = stylesMap['width'];
|
248
|
+
const contentStyle = stylesMap['content'];
|
249
|
+
|
250
|
+
content = html`
|
251
|
+
<div class="content-width vdiff-target" style=${styleMap(widthStyle)}>
|
252
|
+
<div class="content-container" style=${styleMap(contentStyle)}>${content}</div>
|
253
|
+
</div>
|
254
|
+
`;
|
255
|
+
|
256
|
+
if (this._trapFocus) {
|
257
|
+
content = html`
|
258
|
+
<d2l-focus-trap @d2l-focus-trap-enter="${this.#handleFocusTrapEnter}" ?trap="${this._opened}">
|
259
|
+
${content}
|
260
|
+
</d2l-focus-trap>
|
261
|
+
`;
|
262
|
+
}
|
263
|
+
|
264
|
+
const positionStyles = {};
|
265
|
+
if (this._position) {
|
266
|
+
for (const prop in this._position) {
|
267
|
+
positionStyles[prop] = `${this._position[prop]}px`;
|
268
|
+
}
|
269
|
+
}
|
270
|
+
|
271
|
+
content = html`
|
272
|
+
<div class="content-position" style=${styleMap(positionStyles)}>
|
273
|
+
${content}
|
274
|
+
</div>
|
275
|
+
`;
|
276
|
+
|
277
|
+
const pointerPositionStyles = {};
|
278
|
+
if (this._pointerPosition) {
|
279
|
+
for (const prop in this._pointerPosition) {
|
280
|
+
pointerPositionStyles[prop] = `${this._pointerPosition[prop]}px`;
|
281
|
+
}
|
282
|
+
}
|
283
|
+
|
284
|
+
const pointer = !this._noPointer ? html`
|
285
|
+
<div class="pointer" style="${styleMap(pointerPositionStyles)}">
|
286
|
+
<div></div>
|
287
|
+
</div>
|
288
|
+
` : nothing;
|
289
|
+
|
290
|
+
return html`${content}${pointer}`;
|
291
|
+
|
292
|
+
}
|
293
|
+
|
294
|
+
async resize() {
|
295
|
+
if (!this._opened) return;
|
296
|
+
await this.#position();
|
134
297
|
}
|
135
298
|
|
136
299
|
toggleOpen(applyFocus = true) {
|
@@ -138,49 +301,289 @@ export const PopoverMixin = superclass => class extends superclass {
|
|
138
301
|
else return this.open(!this._noAutoFocus && applyFocus);
|
139
302
|
}
|
140
303
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
304
|
+
#handleAncestorMutationBound;
|
305
|
+
#handleAutoCloseClickBound;
|
306
|
+
#handleAutoCloseFocusBound;
|
307
|
+
#handleResizeBound;
|
308
|
+
#repositionBound;
|
309
|
+
|
310
|
+
#addAutoCloseHandlers() {
|
311
|
+
this.addEventListener('blur', this.#handleAutoCloseFocusBound, { capture: true });
|
312
|
+
document.body.addEventListener('focus', this.#handleAutoCloseFocusBound, { capture: true });
|
313
|
+
document.addEventListener('click', this.#handleAutoCloseClickBound, { capture: true });
|
314
|
+
}
|
315
|
+
|
316
|
+
#addRepositionHandlers() {
|
317
|
+
|
318
|
+
const isScrollable = (node, prop) => {
|
319
|
+
const value = window.getComputedStyle(node, null).getPropertyValue(prop);
|
320
|
+
return (value === 'scroll' || value === 'auto');
|
321
|
+
};
|
322
|
+
|
323
|
+
this.#removeRepositionHandlers();
|
324
|
+
|
325
|
+
window.addEventListener('resize', this.#handleResizeBound);
|
326
|
+
|
327
|
+
this._ancestorMutationObserver ??= new MutationObserver(this.#handleAncestorMutationBound);
|
328
|
+
const mutationConfig = { attributes: true, childList: true, subtree: true };
|
329
|
+
|
330
|
+
let node = this;
|
331
|
+
this._scrollablesObserved = [];
|
332
|
+
while (node) {
|
333
|
+
|
334
|
+
// observe scrollables
|
335
|
+
let observeScrollable = false;
|
336
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
337
|
+
observeScrollable = isScrollable(node, 'overflow-y') || isScrollable(node, 'overflow-x');
|
338
|
+
} else if (node.nodeType === Node.DOCUMENT_NODE) {
|
339
|
+
observeScrollable = true;
|
340
|
+
}
|
341
|
+
if (observeScrollable) {
|
342
|
+
this._scrollablesObserved.push(node);
|
343
|
+
node.addEventListener('scroll', this.#repositionBound);
|
344
|
+
}
|
345
|
+
|
346
|
+
// observe mutations on each DOM scope (excludes sibling scopes... can only do so much)
|
347
|
+
if (node.nodeType === Node.DOCUMENT_NODE || (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && node.host)) {
|
348
|
+
this._ancestorMutationObserver.observe(node, mutationConfig);
|
349
|
+
}
|
350
|
+
|
351
|
+
node = getComposedParent(node);
|
352
|
+
}
|
353
|
+
|
354
|
+
this._openerIntersectionObserver = new IntersectionObserver(entries => {
|
355
|
+
entries.forEach(entry => this._offscreen = !entry.isIntersecting);
|
356
|
+
}, { threshold: 0 }); // 0-1 (0 -> intersection requires any pixel visible, 1 -> intersection requires all pixels visible)
|
357
|
+
if (this._opener) {
|
358
|
+
this._openerIntersectionObserver.observe(this._opener);
|
359
|
+
}
|
360
|
+
|
145
361
|
}
|
146
362
|
|
147
|
-
|
363
|
+
#clearDismissible() {
|
148
364
|
if (!this._dismissibleId) return;
|
149
365
|
clearDismissible(this._dismissibleId);
|
150
366
|
this._dismissibleId = null;
|
151
367
|
}
|
152
368
|
|
153
|
-
|
369
|
+
#constrainSpaceAround(spaceAround, spaceRequired, openerRect) {
|
370
|
+
const constrained = { ...spaceAround };
|
371
|
+
|
372
|
+
if ((this._preferredPosition.span === 'end' && !this._rtl) || (this._preferredPosition.span === 'start' && this._rtl)) {
|
373
|
+
constrained.left = Math.max(0, spaceRequired.width - (openerRect.width + spaceAround.right));
|
374
|
+
} else if ((this._preferredPosition.span === 'end' && this._rtl) || (this._preferredPosition.span === 'start' && !this._rtl)) {
|
375
|
+
constrained.right = Math.max(0, spaceRequired.width - (openerRect.width + spaceAround.left));
|
376
|
+
}
|
377
|
+
|
378
|
+
return constrained;
|
379
|
+
}
|
380
|
+
|
381
|
+
#focusContent(container) {
|
154
382
|
if (this._noAutoFocus || this._applyFocus === false) return;
|
155
383
|
|
156
384
|
const focusable = getFirstFocusableDescendant(container);
|
157
385
|
if (focusable) {
|
158
|
-
//
|
386
|
+
// removing the rAF call can allow infinite focus looping to happen in content using a focus trap
|
159
387
|
requestAnimationFrame(() => focusable.focus());
|
160
388
|
} else {
|
161
|
-
const content = this
|
389
|
+
const content = this.#getContentContainer();
|
162
390
|
content.setAttribute('tabindex', '-1');
|
163
391
|
content.focus();
|
164
392
|
}
|
165
393
|
}
|
166
394
|
|
167
|
-
|
395
|
+
#focusOpener() {
|
168
396
|
if (!document.activeElement) return;
|
169
397
|
if (!isComposedAncestor(this, getComposedActiveElement())) return;
|
170
398
|
|
171
399
|
this?._opener.focus();
|
172
400
|
}
|
173
401
|
|
174
|
-
|
175
|
-
return this.shadowRoot.querySelector('.content');
|
402
|
+
#getContentContainer() {
|
403
|
+
return this.shadowRoot.querySelector('.content-container');
|
404
|
+
}
|
405
|
+
|
406
|
+
#getLocation(spaceAround, spaceAroundScroll, spaceRequired) {
|
407
|
+
|
408
|
+
const preferred = this._preferredPosition;
|
409
|
+
if (!preferred.allowFlip) {
|
410
|
+
return preferred.location;
|
411
|
+
}
|
412
|
+
|
413
|
+
if (preferred.location === 'block-end') {
|
414
|
+
if (spaceAround.below >= spaceRequired.height) return 'block-end';
|
415
|
+
if (spaceAround.above >= spaceRequired.height) return 'block-start';
|
416
|
+
// if auto-fit is enabled, scroll will be enabled for the inner content so it will always fit in the available space so pick the largest space it can be displayed in
|
417
|
+
if (!this.noAutoFit) return spaceAround.above > spaceAround.below ? 'block-start' : 'block-end';
|
418
|
+
if (spaceAroundScroll.below >= spaceRequired.height) return 'block-end';
|
419
|
+
if (spaceAroundScroll.above >= spaceRequired.height) return 'block-start';
|
420
|
+
}
|
421
|
+
|
422
|
+
if (preferred.location === 'block-start') {
|
423
|
+
if (spaceAround.above >= spaceRequired.height) return 'block-start';
|
424
|
+
if (spaceAround.below >= spaceRequired.height) return 'block-end';
|
425
|
+
// if auto-fit is enabled, scroll will be enabled for the inner content so it will always fit in the available space so pick the largest space it can be displayed in
|
426
|
+
if (!this.noAutoFit) return spaceAround.above > spaceAround.below ? 'block-start' : 'block-end';
|
427
|
+
if (spaceAroundScroll.above >= spaceRequired.height) return 'block-start';
|
428
|
+
if (spaceAroundScroll.below >= spaceRequired.height) return 'block-end';
|
429
|
+
}
|
430
|
+
|
431
|
+
// todo: add location order for inline-start and inline-end
|
432
|
+
|
433
|
+
// if auto-fit is disabled and it doesn't fit in the scrollable space above or below, always open down because it can add scrollable space
|
434
|
+
return 'block-end';
|
435
|
+
}
|
436
|
+
|
437
|
+
#getPointer() {
|
438
|
+
return this.shadowRoot.querySelector('.pointer');
|
439
|
+
}
|
440
|
+
|
441
|
+
#getPointerPosition(openerRect) {
|
442
|
+
const position = {};
|
443
|
+
const pointer = this.#getPointer();
|
444
|
+
if (!pointer) return position;
|
445
|
+
|
446
|
+
const pointerRect = pointer.getBoundingClientRect();
|
447
|
+
|
448
|
+
if (this._preferredPosition.span !== 'all') {
|
449
|
+
const xAdjustment = Math.min(20 + ((pointerRotatedLength - pointerLength) / 2), (openerRect.width - pointerLength) / 2);
|
450
|
+
if (!this._rtl) {
|
451
|
+
if (this._preferredPosition.span === 'end') {
|
452
|
+
position.left = openerRect.left + xAdjustment;
|
453
|
+
} else {
|
454
|
+
position.right = (openerRect.right * -1) + xAdjustment;
|
455
|
+
}
|
456
|
+
} else {
|
457
|
+
if (this._preferredPosition.span === 'end') {
|
458
|
+
position.right = window.innerWidth - openerRect.right + xAdjustment;
|
459
|
+
} else {
|
460
|
+
position.left = (window.innerWidth - openerRect.left - xAdjustment) * -1;
|
461
|
+
}
|
462
|
+
}
|
463
|
+
} else {
|
464
|
+
if (!this._rtl) {
|
465
|
+
position.left = openerRect.left + ((openerRect.width - pointerRect.width) / 2);
|
466
|
+
} else {
|
467
|
+
position.right = window.innerWidth - openerRect.left - ((openerRect.width + pointerRect.width) / 2);
|
468
|
+
}
|
469
|
+
}
|
470
|
+
|
471
|
+
if (this._location === 'block-start') {
|
472
|
+
position.bottom = window.innerHeight - openerRect.top + 8;
|
473
|
+
} else {
|
474
|
+
position.top = openerRect.top + openerRect.height + this._offset - 7;
|
475
|
+
}
|
476
|
+
|
477
|
+
return position;
|
478
|
+
}
|
479
|
+
|
480
|
+
#getPosition(spaceAround, openerRect, contentRect) {
|
481
|
+
const position = {};
|
482
|
+
|
483
|
+
if (this._location === 'block-end' || this._location === 'block-start') {
|
484
|
+
|
485
|
+
const xAdjustment = this.#getPositionXAdjustment(spaceAround, openerRect, contentRect);
|
486
|
+
if (xAdjustment !== null) {
|
487
|
+
if (!this._rtl) {
|
488
|
+
position.left = openerRect.left + xAdjustment;
|
489
|
+
} else {
|
490
|
+
position.right = window.innerWidth - openerRect.left - openerRect.width + xAdjustment;
|
491
|
+
}
|
492
|
+
}
|
493
|
+
|
494
|
+
if (this._location === 'block-start') {
|
495
|
+
position.bottom = window.innerHeight - openerRect.top + this._offset;
|
496
|
+
} else {
|
497
|
+
position.top = openerRect.top + openerRect.height + this._offset;
|
498
|
+
}
|
499
|
+
|
500
|
+
}
|
501
|
+
|
502
|
+
// todo: add position styles for inline-start and inline-end
|
503
|
+
|
504
|
+
return position;
|
505
|
+
}
|
506
|
+
|
507
|
+
#getPositionXAdjustment(spaceAround, openerRect, contentRect) {
|
508
|
+
|
509
|
+
if (this._location === 'block-end' || this._location === 'block-start') {
|
510
|
+
|
511
|
+
const centerDelta = contentRect.width - openerRect.width;
|
512
|
+
const contentXAdjustment = centerDelta / 2;
|
513
|
+
|
514
|
+
if (this._preferredPosition.span === 'all' && centerDelta <= 0) {
|
515
|
+
// center with target (opener wider than content)
|
516
|
+
return contentXAdjustment * -1;
|
517
|
+
}
|
518
|
+
if (this._preferredPosition.span === 'all' && spaceAround.left > contentXAdjustment && spaceAround.right > contentXAdjustment) {
|
519
|
+
// center with target (content wider than opener and enough space around)
|
520
|
+
return contentXAdjustment * -1;
|
521
|
+
}
|
522
|
+
|
523
|
+
if (!this._rtl) {
|
524
|
+
if (spaceAround.left < contentXAdjustment) {
|
525
|
+
// slide content right (not enough space to center)
|
526
|
+
return spaceAround.left * -1;
|
527
|
+
} else if (spaceAround.right < contentXAdjustment) {
|
528
|
+
// slide content left (not enough space to center)
|
529
|
+
return (centerDelta * -1) + spaceAround.right;
|
530
|
+
}
|
531
|
+
} else {
|
532
|
+
if (spaceAround.left < contentXAdjustment) {
|
533
|
+
// slide content right (not enough space to center)
|
534
|
+
return (centerDelta * -1) + spaceAround.left;
|
535
|
+
} else if (spaceAround.right < contentXAdjustment) {
|
536
|
+
// slide content left (not enough space to center)
|
537
|
+
return spaceAround.right * -1;
|
538
|
+
}
|
539
|
+
}
|
540
|
+
|
541
|
+
if (this._preferredPosition.span !== 'all') {
|
542
|
+
// shift it (not enough space to align as requested)
|
543
|
+
const shift = Math.min((openerRect.width / 2) - (20 + pointerLength / 2), 0); // 20 ~= 1rem
|
544
|
+
if (this._preferredPosition.span === 'end') {
|
545
|
+
return shift;
|
546
|
+
} else {
|
547
|
+
return openerRect.width - contentRect.width - shift;
|
548
|
+
}
|
549
|
+
}
|
550
|
+
|
551
|
+
}
|
552
|
+
|
553
|
+
// todo: add position styles for inline-start and inline-end
|
554
|
+
|
555
|
+
return null;
|
556
|
+
}
|
557
|
+
|
558
|
+
#getStyleMaps() {
|
559
|
+
const widthStyle = {
|
560
|
+
maxWidth: this._maxWidth ? `${this._maxWidth}px` : undefined,
|
561
|
+
minWidth: this._minWidth ? `${this._minWidth}px` : undefined,
|
562
|
+
width: this._width ? `${this._width + 3}px` : undefined // add 3 to content to account for possible rounding and also scrollWidth does not include border
|
563
|
+
};
|
564
|
+
|
565
|
+
const contentStyle = {
|
566
|
+
maxHeight: this._contentHeight ? `${this._contentHeight}px` : undefined,
|
567
|
+
};
|
568
|
+
|
569
|
+
return {
|
570
|
+
'width' : widthStyle,
|
571
|
+
'content' : contentStyle
|
572
|
+
};
|
176
573
|
}
|
177
574
|
|
178
|
-
|
575
|
+
#handleAncestorMutation(mutations) {
|
576
|
+
if (!this._opener) return;
|
577
|
+
// ignore mutations that are within this popover
|
578
|
+
const reposition = !!mutations.find(mutation => !isComposedAncestor(this._opener, mutation.target));
|
579
|
+
if (reposition) this.#reposition();
|
580
|
+
}
|
179
581
|
|
582
|
+
#handleAutoCloseClick(e) {
|
180
583
|
if (!this._opened || this._noAutoClose) return;
|
181
584
|
|
182
585
|
const rootTarget = e.composedPath()[0];
|
183
|
-
if (isComposedAncestor(this
|
586
|
+
if (isComposedAncestor(this.#getContentContainer(), rootTarget)
|
184
587
|
|| (this._opener !== document.body && isComposedAncestor(this._opener, rootTarget))) {
|
185
588
|
return;
|
186
589
|
}
|
@@ -188,7 +591,7 @@ export const PopoverMixin = superclass => class extends superclass {
|
|
188
591
|
this.close();
|
189
592
|
}
|
190
593
|
|
191
|
-
|
594
|
+
#handleAutoCloseFocus() {
|
192
595
|
|
193
596
|
// todo: try to use relatedTarget instead - this logic is largely copied as-is from dropdown simply to mitigate risk of this fragile code
|
194
597
|
setTimeout(() => {
|
@@ -212,27 +615,130 @@ export const PopoverMixin = superclass => class extends superclass {
|
|
212
615
|
|
213
616
|
}
|
214
617
|
|
215
|
-
|
216
|
-
this
|
618
|
+
#handleFocusTrapEnter() {
|
619
|
+
this.#focusContent(this.#getContentContainer());
|
217
620
|
|
218
621
|
/** Dispatched when user focus enters the popover (trap-focus option only) */
|
219
622
|
this.dispatchEvent(new CustomEvent('d2l-popover-focus-enter', { detail: { applyFocus: this._applyFocus } }));
|
220
623
|
}
|
221
624
|
|
222
|
-
|
223
|
-
this.
|
224
|
-
|
225
|
-
|
625
|
+
#handleResize() {
|
626
|
+
this.resize();
|
627
|
+
}
|
628
|
+
|
629
|
+
async #position(contentRect, options) {
|
630
|
+
if (!this._opener) return;
|
631
|
+
|
632
|
+
options = Object.assign({ updateLocation: true, updateHeight: true }, options);
|
633
|
+
|
634
|
+
const content = this.#getContentContainer();
|
635
|
+
|
636
|
+
if (!this._noAutoFit && options.updateHeight) {
|
637
|
+
this._contentHeight = null;
|
638
|
+
}
|
639
|
+
|
640
|
+
// don't let popover content horizontally overflow viewport
|
641
|
+
this._width = null;
|
642
|
+
|
643
|
+
await this.updateComplete;
|
644
|
+
|
645
|
+
const adjustPosition = async() => {
|
646
|
+
|
647
|
+
const scrollHeight = document.documentElement.scrollHeight;
|
648
|
+
const openerRect = this._opener.getBoundingClientRect();
|
649
|
+
contentRect = contentRect ?? content.getBoundingClientRect();
|
650
|
+
|
651
|
+
const height = this._minHeight ?? Math.min(this._maxHeight ?? Number.MAX_VALUE, contentRect.height);
|
652
|
+
|
653
|
+
const spaceRequired = {
|
654
|
+
height: height + 10,
|
655
|
+
width: contentRect.width
|
656
|
+
};
|
657
|
+
|
658
|
+
// space in viewport
|
659
|
+
const spaceAround = this.#constrainSpaceAround({
|
660
|
+
// allow for opener offset + outer margin
|
661
|
+
above: openerRect.top - this._offset - this._margin,
|
662
|
+
// allow for opener offset + outer margin
|
663
|
+
below: window.innerHeight - openerRect.bottom - this._offset - this._margin,
|
664
|
+
// allow for outer margin
|
665
|
+
left: openerRect.left - 20,
|
666
|
+
// allow for outer margin
|
667
|
+
right: document.documentElement.clientWidth - openerRect.right - 15
|
668
|
+
}, spaceRequired, openerRect);
|
669
|
+
|
670
|
+
// space in document
|
671
|
+
const spaceAroundScroll = this.#constrainSpaceAround({
|
672
|
+
above: openerRect.top + document.documentElement.scrollTop,
|
673
|
+
below: scrollHeight - openerRect.bottom - document.documentElement.scrollTop
|
674
|
+
}, spaceRequired, openerRect);
|
675
|
+
|
676
|
+
if (options.updateLocation) {
|
677
|
+
this._location = this.#getLocation(spaceAround, spaceAroundScroll, spaceRequired);
|
678
|
+
}
|
679
|
+
|
680
|
+
this._position = this.#getPosition(spaceAround, openerRect, contentRect);
|
681
|
+
if (!this._noPointer) this._pointerPosition = this.#getPointerPosition(openerRect);
|
682
|
+
|
683
|
+
if (options.updateHeight) {
|
684
|
+
|
685
|
+
// calculate height available to the popover contents for overflow because that is the only area capable of scrolling
|
686
|
+
const availableHeight = (this._location === 'block-start') ? spaceAround.above : spaceAround.below;
|
687
|
+
|
688
|
+
if (!this._noAutoFit && availableHeight && availableHeight > 0) {
|
689
|
+
// only apply maximum if it's less than space available and the header/footer alone won't exceed it (content must be visible)
|
690
|
+
this._contentHeight = this._maxHeight !== null && availableHeight > this._maxHeight
|
691
|
+
? this._maxHeight - 2 : availableHeight;
|
692
|
+
|
693
|
+
// ensure the content height has updated when the __toggleScrollStyles event handler runs
|
694
|
+
await this.updateComplete;
|
695
|
+
}
|
696
|
+
|
697
|
+
// todo: handle inline-start and inline-end locations
|
698
|
+
|
699
|
+
}
|
700
|
+
|
701
|
+
/** Dispatched when the popover position finishes adjusting */
|
702
|
+
this.dispatchEvent(new CustomEvent('d2l-popover-position', { bubbles: true, composed: true }));
|
703
|
+
|
704
|
+
};
|
705
|
+
|
706
|
+
const scrollWidth = content.scrollWidth;
|
707
|
+
const availableWidth = window.innerWidth - 40;
|
708
|
+
|
709
|
+
this._width = (availableWidth > scrollWidth ? scrollWidth : availableWidth);
|
710
|
+
|
711
|
+
await this.updateComplete;
|
712
|
+
|
713
|
+
await adjustPosition();
|
714
|
+
|
226
715
|
}
|
227
716
|
|
228
|
-
|
229
|
-
|
717
|
+
#removeAutoCloseHandlers() {
|
718
|
+
this.removeEventListener('blur', this.#handleAutoCloseFocusBound, { capture: true });
|
719
|
+
document.body?.removeEventListener('focus', this.#handleAutoCloseFocusBound, { capture: true }); // DE41322: document.body can be null in some scenarios
|
720
|
+
document.removeEventListener('click', this.#handleAutoCloseClickBound, { capture: true });
|
721
|
+
}
|
230
722
|
|
231
|
-
|
232
|
-
|
233
|
-
|
723
|
+
#removeRepositionHandlers() {
|
724
|
+
this._openerIntersectionObserver?.unobserve(this._opener);
|
725
|
+
this._scrollablesObserved?.forEach(node => {
|
726
|
+
node.removeEventListener('scroll', this.#repositionBound);
|
727
|
+
});
|
728
|
+
this._scrollablesObserved = null;
|
729
|
+
this._ancestorMutationObserver?.disconnect();
|
730
|
+
window.removeEventListener('resize', this.#handleResizeBound);
|
731
|
+
}
|
234
732
|
|
235
|
-
|
733
|
+
#reposition() {
|
734
|
+
// throttle repositioning (https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event#scroll_event_throttling)
|
735
|
+
if (!this._repositioning) {
|
736
|
+
requestAnimationFrame(() => {
|
737
|
+
this.#position(undefined, { updateLocation: false, updateHeight: false });
|
738
|
+
this._repositioning = false;
|
739
|
+
});
|
740
|
+
}
|
741
|
+
this._repositioning = true;
|
236
742
|
}
|
237
743
|
|
238
744
|
};
|