@brightspace-ui/core 3.219.6 → 3.219.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1299 +0,0 @@
1
- import '../backdrop/backdrop.js';
2
- import '../button/button.js';
3
- import '../focus-trap/focus-trap.js';
4
- import { clearDismissible, setDismissible } from '../../helpers/dismissible.js';
5
- import { findComposedAncestor, getBoundingAncestor, getComposedParent, isComposedAncestor, isVisible } from '../../helpers/dom.js';
6
- import { getComposedActiveElement, getFirstFocusableDescendant, getPreviousFocusableAncestor } from '../../helpers/focus.js';
7
- import { classMap } from 'lit/directives/class-map.js';
8
- import { html } from 'lit';
9
- import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
10
- import ResizeObserver from 'resize-observer-polyfill/dist/ResizeObserver.es.js';
11
- import { RtlMixin } from '../../mixins/rtl/rtl-mixin.js';
12
- import { styleMap } from 'lit/directives/style-map.js';
13
- import { tryGetIfrauBackdropService } from '../../helpers/ifrauBackdropService.js';
14
- import { visualReady } from '../../helpers/visualReady.js';
15
-
16
- const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
17
- const minBackdropHeightMobile = 42;
18
- const minBackdropWidthMobile = 30;
19
- const outerMarginTopBottom = 18;
20
- const defaultVerticalOffset = 16;
21
- const pointerLength = 16;
22
- const pointerRotatedLength = Math.SQRT2 * parseFloat(pointerLength);
23
-
24
- export const DropdownContentMixin = superclass => class extends LocalizeCoreElement(RtlMixin(superclass)) {
25
-
26
- static get properties() {
27
- return {
28
- /**
29
- * Optionally align dropdown to either start or end. If not set, the dropdown will attempt to be centred.
30
- * @type {'start'|'end'}
31
- */
32
- align: {
33
- type: String,
34
- reflect: true
35
- },
36
- /**
37
- * Optionally provide boundaries to where the dropdown will appear. Valid properties are "above", "below", "left", and "right".
38
- * @type {object}
39
- */
40
- boundary: {
41
- type: Object,
42
- },
43
- /**
44
- * Override default max-width (undefined). Specify a number that would be the px value.
45
- * @type {number}
46
- */
47
- maxWidth: {
48
- type: Number,
49
- reflect: true,
50
- attribute: 'max-width'
51
- },
52
- /**
53
- * Override default min-width (undefined). Specify a number that would be the px value.
54
- * @type {number}
55
- */
56
- minWidth: {
57
- type: Number,
58
- reflect: true,
59
- attribute: 'min-width'
60
- },
61
- /**
62
- * Override max-height. Note that the default behaviour is to be as tall as necessary within the viewport, so this property is usually not needed.
63
- * @type {number}
64
- */
65
- maxHeight: {
66
- type: Number,
67
- attribute: 'max-height'
68
- },
69
- /**
70
- * Override the breakpoint at which mobile styling is used. Defaults to 616px.
71
- * @type {number}
72
- */
73
- mobileBreakpointOverride: {
74
- type: Number,
75
- attribute: 'mobile-breakpoint'
76
- },
77
- /**
78
- * Override default height used for required space when `no-auto-fit` is true. Specify a number that would be the px value. Note that the default behaviour is to be as tall as necessary within the viewport, so this property is usually not needed.
79
- * @type {number}
80
- */
81
- minHeight: {
82
- type: Number,
83
- reflect: true,
84
- attribute: 'min-height'
85
- },
86
- /**
87
- * Opt-out of showing a close button in the footer of tray-style mobile dropdowns.
88
- * @type {boolean}
89
- */
90
- noMobileCloseButton: {
91
- type: Boolean,
92
- reflect: true,
93
- attribute: 'no-mobile-close-button'
94
- },
95
- /**
96
- * Mobile dropdown style.
97
- * @type {'left'|'right'|'bottom'}
98
- */
99
- mobileTray: {
100
- type: String,
101
- reflect: true,
102
- attribute: 'mobile-tray'
103
- },
104
- /**
105
- * Opt out of automatically closing on focus or click outside of the dropdown content
106
- * @type {boolean}
107
- */
108
- noAutoClose: {
109
- type: Boolean,
110
- reflect: true,
111
- attribute: 'no-auto-close'
112
- },
113
- /**
114
- * Opt out of auto-sizing
115
- * @type {boolean}
116
- */
117
- noAutoFit: {
118
- type: Boolean,
119
- reflect: true,
120
- attribute: 'no-auto-fit'
121
- },
122
- /**
123
- * Opt out of focus being automatically moved to the first focusable element in the dropdown when opened
124
- * @type {boolean}
125
- */
126
- noAutoFocus: {
127
- type: Boolean,
128
- reflect: true,
129
- attribute: 'no-auto-focus'
130
- },
131
- /**
132
- * Render with no padding
133
- * @type {boolean}
134
- */
135
- noPadding: {
136
- type: Boolean,
137
- reflect: true,
138
- attribute: 'no-padding'
139
- },
140
- /**
141
- * Render the footer with no padding (if it has content)
142
- * @type {boolean}
143
- */
144
- noPaddingFooter: {
145
- type: Boolean,
146
- reflect: true,
147
- attribute: 'no-padding-footer'
148
- },
149
- /**
150
- * Render the header with no padding (if it has content)
151
- * @type {boolean}
152
- */
153
- noPaddingHeader: {
154
- type: Boolean,
155
- reflect: true,
156
- attribute: 'no-padding-header'
157
- },
158
- /**
159
- * Render without a pointer
160
- * @type {boolean}
161
- */
162
- noPointer: {
163
- type: Boolean,
164
- reflect: true,
165
- attribute: 'no-pointer'
166
- },
167
- /**
168
- * Private, set by the opener depending on whether it's intersecting
169
- * @ignore
170
- */
171
- offscreen: {
172
- type: Boolean,
173
- reflect: true
174
- },
175
- /**
176
- * Whether the dropdown is open or not
177
- * @type {boolean}
178
- */
179
- opened: {
180
- type: Boolean,
181
- reflect: true
182
- },
183
- /**
184
- * Private.
185
- * @ignore
186
- */
187
- openedAbove: {
188
- type: Boolean,
189
- reflect: true,
190
- attribute: 'opened-above'
191
- },
192
- /**
193
- * Optionally render a d2l-focus-trap around the dropdown content
194
- * @type {boolean}
195
- */
196
- trapFocus: {
197
- type: Boolean,
198
- reflect: true,
199
- attribute: 'trap-focus'
200
- },
201
- /**
202
- * Provide custom offset, positive or negative
203
- * @type {string}
204
- */
205
- verticalOffset: {
206
- type: String,
207
- attribute: 'vertical-offset'
208
- },
209
- _bottomOverflow: {
210
- type: Boolean
211
- },
212
- _closing: {
213
- type: Boolean
214
- },
215
- _dropdownContent: {
216
- type: Boolean,
217
- attribute: 'dropdown-content',
218
- reflect: true
219
- },
220
- _useMobileStyling: {
221
- type: Boolean,
222
- attribute: 'data-mobile',
223
- reflect: true
224
- },
225
- _hasHeader: {
226
- type: Boolean
227
- },
228
- _hasFooter: {
229
- type: Boolean
230
- },
231
- _contentHeight: {
232
- type: Number
233
- },
234
- _pointerPosition: {
235
- state: true
236
- },
237
- _position: {
238
- state: true
239
- },
240
- _showBackdrop: {
241
- type: Boolean
242
- },
243
- _topOverflow: {
244
- type: Boolean
245
- },
246
- _width: {
247
- type: Number
248
- }
249
- };
250
- }
251
-
252
- constructor() {
253
- super();
254
-
255
- this.noAutoClose = false;
256
- this.noAutoFit = false;
257
- this.noAutoFocus = false;
258
- this.noMobileCloseButton = false;
259
- this.noPadding = false;
260
- this.noPaddingFooter = false;
261
- this.noPaddingHeader = false;
262
- this.noPointer = false;
263
- this.mobileBreakpointOverride = 616;
264
- this.trapFocus = false;
265
- this._useMobileStyling = false;
266
-
267
- this.__opened = false;
268
- this.__content = null;
269
- this.__previousFocusableAncestor = null;
270
- this.__applyFocus = true;
271
- this.__dismissibleId = null;
272
-
273
- this._dropdownContent = true;
274
- this._bottomOverflow = false;
275
- this._topOverflow = false;
276
- this._closing = false;
277
- this._hasHeader = false;
278
- this._hasFooter = false;
279
- this._showBackdrop = false;
280
- this._verticalOffset = defaultVerticalOffset;
281
-
282
- this.__reposition = this.__reposition.bind(this);
283
- this.__onAncestorMutation = this.__onAncestorMutation.bind(this);
284
- this.__onResize = this.__onResize.bind(this);
285
- this.__onAutoCloseFocus = this.__onAutoCloseFocus.bind(this);
286
- this.__onAutoCloseClick = this.__onAutoCloseClick.bind(this);
287
- this.__toggleScrollStyles = this.__toggleScrollStyles.bind(this);
288
- this._handleMobileResize = this._handleMobileResize.bind(this);
289
- this.__disconnectResizeObserver = this.__disconnectResizeObserver.bind(this);
290
- }
291
-
292
- get opened() {
293
- return this.__opened;
294
- }
295
-
296
- set opened(val) {
297
- const oldVal = this.__opened;
298
- if (oldVal !== val) {
299
- this.__opened = val;
300
- this.requestUpdate('opened', oldVal);
301
- this.__openedChanged(val);
302
- }
303
- }
304
-
305
- connectedCallback() {
306
- super.connectedCallback();
307
-
308
- window.addEventListener('resize', this.__onResize);
309
- this.addEventListener('blur', this.__onAutoCloseFocus, true);
310
- this.addEventListener('touchstart', this.__onTouchStart);
311
- document.body.addEventListener('focus', this.__onAutoCloseFocus, true);
312
- document.addEventListener('click', this.__onAutoCloseClick, true);
313
- this.mediaQueryList = window.matchMedia(`(max-width: ${this.mobileBreakpointOverride - 1}px)`);
314
- this._useMobileStyling = this.mediaQueryList.matches;
315
- if (this.mediaQueryList.addEventListener) this.mediaQueryList.addEventListener('change', this._handleMobileResize);
316
- if (this.opened) this.__addRepositionHandlers();
317
- }
318
-
319
- disconnectedCallback() {
320
- super.disconnectedCallback();
321
- if (this.mediaQueryList.removeEventListener) this.mediaQueryList.removeEventListener('change', this._handleMobileResize);
322
- this.removeEventListener('blur', this.__onAutoCloseFocus);
323
- this.removeEventListener('touchstart', this.__onTouchStart);
324
- window.removeEventListener('resize', this.__onResize);
325
- document.body?.removeEventListener('focus', this.__onAutoCloseFocus, true); // DE41322: document.body can be null in some scenarios
326
- document.removeEventListener('click', this.__onAutoCloseClick, true);
327
- clearDismissible(this.__dismissibleId);
328
- this.__dismissibleId = null;
329
-
330
- if (this.__resizeObserver) this.__resizeObserver.disconnect();
331
- this.__removeRepositionHandlers();
332
- }
333
-
334
- firstUpdated(changedProperties) {
335
- super.firstUpdated(changedProperties);
336
-
337
- this.__content = this.getContentContainer();
338
- this.addEventListener('d2l-dropdown-close', this.__onClose);
339
- this.addEventListener('d2l-dropdown-position', this.__toggleScrollStyles);
340
- }
341
-
342
- async getUpdateComplete() {
343
- await super.getUpdateComplete();
344
- await visualReady;
345
- }
346
-
347
- updated(changedProperties) {
348
- changedProperties.forEach((_, propName) => {
349
- if (propName === 'verticalOffset') {
350
- let newVerticalOffset = parseInt(this.verticalOffset);
351
- if (isNaN(newVerticalOffset)) {
352
- newVerticalOffset = defaultVerticalOffset;
353
- }
354
- this._verticalOffset = newVerticalOffset;
355
- }
356
- });
357
- }
358
-
359
- close() {
360
- const hide = () => {
361
- this._closing = false;
362
- this._showBackdrop = false;
363
- this.opened = false;
364
- };
365
-
366
- if (!reduceMotion && this._useMobileStyling && this.mobileTray && isVisible(this)) {
367
- if (this.shadowRoot) this.shadowRoot.querySelector('.d2l-dropdown-content-width')
368
- .addEventListener('animationend', hide, { once: true });
369
- this._closing = true;
370
- this._showBackdrop = false;
371
- } else {
372
- hide();
373
- }
374
- }
375
-
376
- /**
377
- * forceRender is no longer necessary, this is left as a stub so that
378
- * places calling it will not break. It will be removed once the Polymer
379
- * dropdown is swapped over to use this and all instances of
380
- * forceRender are removed.
381
- */
382
- forceRender() {}
383
-
384
- getContentContainer() {
385
- return this.shadowRoot && this.shadowRoot.querySelector('.d2l-dropdown-content-container');
386
- }
387
-
388
- /**
389
- * Private.
390
- */
391
- height() {
392
- return this.__content && this.__content.offsetHeight;
393
- }
394
-
395
- async open(applyFocus) {
396
- this.__applyFocus = applyFocus !== undefined ? applyFocus : true;
397
- this.opened = true;
398
- await this.updateComplete;
399
- this._showBackdrop = this._useMobileStyling && this.mobileTray;
400
- }
401
-
402
- /**
403
- * Waits for the next resize when elem has a height > 0px,
404
- * then calls the __position function.
405
- */
406
- requestRepositionNextResize(elem) {
407
- if (!elem) return;
408
- if (this.__resizeObserver) this.__resizeObserver.disconnect();
409
- this.__resizeObserver = new ResizeObserver(this.__disconnectResizeObserver);
410
- this.__resizeObserver.observe(elem);
411
- }
412
-
413
- async resize() {
414
- if (!this.opened) {
415
- return;
416
- }
417
- this._showBackdrop = this._useMobileStyling && this.mobileTray;
418
- await this.__position();
419
- }
420
-
421
- /**
422
- * Private.
423
- */
424
- scrollTo(scrollTop) {
425
- const content = this.__content;
426
- if (content) {
427
- if (typeof scrollTop === 'number') {
428
- content.scrollTop = scrollTop;
429
- }
430
- return content.scrollTop;
431
- }
432
- }
433
-
434
- toggleOpen(applyFocus) {
435
- if (this.opened) {
436
- this.close();
437
- } else {
438
- this.open(!this.noAutoFocus && applyFocus);
439
- }
440
- }
441
-
442
- __addRepositionHandlers() {
443
-
444
- const isScrollable = (node, prop) => {
445
- const value = window.getComputedStyle(node, null).getPropertyValue(prop);
446
- return (value === 'scroll' || value === 'auto');
447
- };
448
-
449
- this.__removeRepositionHandlers();
450
-
451
- this._ancestorMutationObserver ??= new MutationObserver(this.__onAncestorMutation);
452
- const mutationConfig = { attributes: true, childList: true, subtree: true };
453
-
454
- let node = this;
455
- this._scrollablesObserved = [];
456
- while (node) {
457
-
458
- // observe scrollables
459
- let observeScrollable = false;
460
- if (node.nodeType === Node.ELEMENT_NODE) {
461
- observeScrollable = isScrollable(node, 'overflow-y') || isScrollable(node, 'overflow-x');
462
- } else if (node.nodeType === Node.DOCUMENT_NODE) {
463
- observeScrollable = true;
464
- }
465
- if (observeScrollable) {
466
- this._scrollablesObserved.push(node);
467
- node.addEventListener('scroll', this.__reposition);
468
- }
469
-
470
- // observe mutations on each DOM scope (excludes sibling scopes... can only do so much)
471
- if (node.nodeType === Node.DOCUMENT_NODE || (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && node.host)) {
472
- this._ancestorMutationObserver.observe(node, mutationConfig);
473
- }
474
-
475
- node = getComposedParent(node);
476
- }
477
-
478
- }
479
-
480
- __disconnectResizeObserver(entries) {
481
- for (let i = 0; i < entries.length; i++) {
482
- const entry = entries[i];
483
- if (this.__resizeObserver && entry.contentRect.height !== 0) {
484
- this.__resizeObserver.disconnect();
485
- // wrap in rAF for Firefox
486
- requestAnimationFrame(() => {
487
- if (this.opened) this.__position();
488
- });
489
- break;
490
- }
491
- }
492
- }
493
-
494
- __getContentBottom() {
495
- return this.shadowRoot && this.shadowRoot.querySelector('.d2l-dropdown-content-bottom');
496
- }
497
-
498
- __getContentTop() {
499
- return this.shadowRoot && this.shadowRoot.querySelector('.d2l-dropdown-content-top');
500
- }
501
-
502
- __getOpener() {
503
- const opener = findComposedAncestor(this, (elem) => {
504
- if (elem.dropdownOpener) {
505
- return true;
506
- }
507
- });
508
- return opener;
509
- }
510
-
511
- __getPointer() {
512
- return this.shadowRoot && this.shadowRoot.querySelector('.d2l-dropdown-content-pointer');
513
- }
514
-
515
- __getPositionContainer() {
516
- return this.shadowRoot && this.shadowRoot.querySelector('.d2l-dropdown-content-position');
517
- }
518
-
519
- __getWidthContainer() {
520
- return this.shadowRoot && this.shadowRoot.querySelector('.d2l-dropdown-content-width');
521
- }
522
-
523
- __handleFooterSlotChange(e) {
524
- this._hasFooter = e.target.assignedNodes().length !== 0;
525
- }
526
-
527
- __handleHeaderSlotChange(e) {
528
- this._hasHeader = e.target.assignedNodes().length !== 0;
529
- }
530
-
531
- __onAncestorMutation(mutations) {
532
- const opener = this.__getOpener();
533
- // ignore mutations that are within this dropdown
534
- const reposition = !!mutations.find(mutation => !isComposedAncestor(opener, mutation.target));
535
- if (reposition) this.__reposition();
536
- }
537
-
538
- __onAutoCloseClick(e) {
539
- if (!this.opened || this.noAutoClose) {
540
- return;
541
- }
542
- const rootTarget = e.composedPath()[0];
543
- const clickInside = isComposedAncestor(this.getContentContainer(), rootTarget) ||
544
- isComposedAncestor(this.__getContentTop(), rootTarget) ||
545
- isComposedAncestor(this.__getContentBottom(), rootTarget);
546
- if (clickInside) {
547
- return;
548
- }
549
- const opener = this.__getOpener();
550
- if (isComposedAncestor(opener.getOpenerElement(), rootTarget)) {
551
- return;
552
- }
553
-
554
- this.close();
555
- }
556
-
557
- __onAutoCloseFocus() {
558
-
559
- /* timeout needed to work around lack of support for relatedTarget */
560
- setTimeout(() => {
561
- if (!this.opened
562
- || this.noAutoClose
563
- || !document.activeElement
564
- || document.activeElement === this.__previousFocusableAncestor
565
- || document.activeElement === document.body) {
566
- return;
567
- }
568
-
569
- const activeElement = getComposedActiveElement();
570
-
571
- if (isComposedAncestor(this, activeElement)
572
- || isComposedAncestor(this.__getOpener(), activeElement)
573
- || activeElement === this.__previousFocusableAncestor) {
574
- return;
575
- }
576
- this.close();
577
- }, 0);
578
- }
579
-
580
- __onClose(e) {
581
-
582
- if (e.target !== this || !document.activeElement) {
583
- return;
584
- }
585
-
586
- const activeElement = getComposedActiveElement();
587
-
588
- if (!isComposedAncestor(this, activeElement)) {
589
- return;
590
- }
591
-
592
- const opener = this.__getOpener();
593
- opener.getOpenerElement().focus();
594
-
595
- }
596
-
597
- __onResize() {
598
- this.resize();
599
- }
600
-
601
- __onTouchStart(e) {
602
- // elements external to the dropdown content such as primary-secondary template should not be reacting
603
- // to touchstart events originating inside the dropdown content
604
- e.stopPropagation();
605
- }
606
-
607
- async __openedChanged(newValue) {
608
-
609
- // DE44538: wait for dropdown content to fully render,
610
- // otherwise this.getContentContainer() can return null.
611
- await this.__waitForContentContainer();
612
-
613
- this.__previousFocusableAncestor =
614
- newValue === true
615
- ? getPreviousFocusableAncestor(this, false, false)
616
- : null;
617
-
618
- const doOpen = async() => {
619
-
620
- const content = this.getContentContainer();
621
-
622
- if (!this.noAutoFit) {
623
- content.scrollTop = 0;
624
- }
625
-
626
- await this.__position();
627
- this._showBackdrop = this._useMobileStyling && this.mobileTray;
628
- if (!this.noAutoFocus && this.__applyFocus) {
629
- const focusable = getFirstFocusableDescendant(this);
630
- if (focusable) {
631
- // Removing the rAF call can allow infinite focus looping to happen in content using a focus trap
632
- requestAnimationFrame(() => focusable.focus());
633
- } else {
634
- content.setAttribute('tabindex', '-1');
635
- content.focus();
636
- }
637
- }
638
-
639
- setTimeout(() =>
640
- this.dispatchEvent(new CustomEvent('d2l-dropdown-open', { bubbles: true, composed: true })), 0
641
- );
642
-
643
- this.__dismissibleId = setDismissible(() => {
644
- this.close();
645
- });
646
- };
647
-
648
- const ifrauBackdropService = await tryGetIfrauBackdropService();
649
-
650
- if (newValue) {
651
-
652
- if (ifrauBackdropService && this.mobileTray && this._useMobileStyling) {
653
- this._ifrauContextInfo = await ifrauBackdropService.showBackdrop();
654
- }
655
-
656
- await doOpen();
657
-
658
- this.__addRepositionHandlers();
659
-
660
- } else {
661
-
662
- this.__removeRepositionHandlers();
663
-
664
- if (this.__dismissibleId) {
665
- clearDismissible(this.__dismissibleId);
666
- this.__dismissibleId = null;
667
- }
668
- if (ifrauBackdropService && this.mobileTray && this._useMobileStyling) {
669
- ifrauBackdropService.hideBackdrop();
670
- this._ifrauContextInfo = null;
671
- }
672
- this._showBackdrop = false;
673
- await this.updateComplete;
674
-
675
- /** Dispatched when the dropdown is closed */
676
- this.dispatchEvent(new CustomEvent('d2l-dropdown-close', { bubbles: true, composed: true }));
677
-
678
- }
679
- }
680
-
681
- async __position(contentRect, options) {
682
-
683
- options = Object.assign({ updateAboveBelow: true, updateHeight: true }, options);
684
-
685
- const opener = this.__getOpener();
686
- if (!opener) {
687
- return;
688
- }
689
- const target = opener.getOpenerElement();
690
- if (!target) {
691
- return;
692
- }
693
-
694
- const content = this.getContentContainer();
695
- const header = this.__getContentTop(); // todo: rename
696
- const footer = this.__getContentBottom(); // todo: rename
697
-
698
- if (!this.noAutoFit && options.updateHeight) {
699
- this._contentHeight = null;
700
- }
701
-
702
- /* don't let dropdown content horizontally overflow viewport */
703
- this._width = null;
704
-
705
- const boundingContainer = getBoundingAncestor(target.parentNode);
706
- const scrollHeight = boundingContainer.scrollHeight;
707
-
708
- await this.updateComplete;
709
-
710
- const adjustPosition = async() => {
711
-
712
- const targetRect = target.getBoundingClientRect();
713
- contentRect = contentRect ? contentRect : content.getBoundingClientRect();
714
- const headerFooterHeight = header.getBoundingClientRect().height + footer.getBoundingClientRect().height;
715
-
716
- const height = this.minHeight ? this.minHeight : Math.min(this.maxHeight ? this.maxHeight : Number.MAX_VALUE, contentRect.height + headerFooterHeight);
717
-
718
- const spaceRequired = {
719
- height: height + 10,
720
- width: contentRect.width
721
- };
722
-
723
- const spaceAround = this._constrainSpaceAround({
724
- // allow for target offset + outer margin
725
- above: targetRect.top - this._verticalOffset - outerMarginTopBottom,
726
- // allow for target offset + outer margin
727
- below: window.innerHeight - targetRect.bottom - this._verticalOffset - outerMarginTopBottom,
728
- // allow for outer margin
729
- left: targetRect.left - 20,
730
- // allow for outer margin
731
- right: document.documentElement.clientWidth - targetRect.right - 15
732
- }, spaceRequired, targetRect);
733
-
734
- const spaceAroundScroll = this._constrainSpaceAround({
735
- above: targetRect.top + document.documentElement.scrollTop,
736
- below: scrollHeight - targetRect.bottom - document.documentElement.scrollTop
737
- }, spaceRequired, targetRect);
738
-
739
- if (options.updateAboveBelow) {
740
- this.openedAbove = this._getOpenedAbove(spaceAround, spaceAroundScroll, spaceRequired);
741
- }
742
-
743
- this._position = this._getPosition(spaceAround, targetRect, contentRect);
744
- this._pointerPosition = this._getPointerPosition(targetRect);
745
-
746
- if (options.updateHeight) {
747
- // calculate height available to the dropdown contents for overflow because that is the only area capable of scrolling
748
- const availableHeight = this.openedAbove ? spaceAround.above : spaceAround.below;
749
- if (!this.noAutoFit && availableHeight && availableHeight > 0) {
750
- // only apply maximum if it's less than space available and the header/footer alone won't exceed it (content must be visible)
751
- this._contentHeight = this.maxHeight !== null
752
- && availableHeight > this.maxHeight
753
- && headerFooterHeight < this.maxHeight
754
- ? this.maxHeight - headerFooterHeight - 2
755
- : availableHeight - headerFooterHeight;
756
-
757
- // ensure the content height has updated when the __toggleScrollStyles event handler runs
758
- await this.updateComplete;
759
- }
760
- }
761
-
762
- /** Dispatched when the dropdown position finishes adjusting */
763
- this.dispatchEvent(new CustomEvent('d2l-dropdown-position', { bubbles: true, composed: true }));
764
- };
765
-
766
- const scrollWidth = Math.max(header.scrollWidth, content.scrollWidth, footer.scrollWidth);
767
- const availableWidth = window.innerWidth - 40;
768
- this._width = (availableWidth > scrollWidth ? scrollWidth : availableWidth) ;
769
-
770
- await this.updateComplete;
771
-
772
- await adjustPosition();
773
- }
774
-
775
- __removeRepositionHandlers() {
776
- this._scrollablesObserved?.forEach(node => {
777
- node.removeEventListener('scroll', this.__reposition);
778
- });
779
- this._scrollablesObserved = null;
780
-
781
- this._ancestorMutationObserver?.disconnect();
782
- }
783
-
784
- __reposition() {
785
- // throttle repositioning (https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event#scroll_event_throttling)
786
- if (!this.__repositioning) {
787
- requestAnimationFrame(() => {
788
- this.__position(undefined, { updateAboveBelow: false, updateHeight: false });
789
- this.__repositioning = false;
790
- });
791
- }
792
- this.__repositioning = true;
793
- }
794
-
795
- __toggleScrollStyles() {
796
- /* scrollHeight incorrect in IE by 4px second time opened */
797
- this._bottomOverflow = this.__content.scrollHeight - (this.__content.scrollTop + this.__content.clientHeight) >= 5;
798
- this._topOverflow = this.__content.scrollTop !== 0;
799
- }
800
-
801
- async __waitForContentContainer() {
802
- if (this.getContentContainer() !== null) return;
803
- await new Promise(resolve => requestAnimationFrame(resolve));
804
- return this.__waitForContentContainer();
805
- }
806
-
807
- _constrainSpaceAround(spaceAround, spaceRequired, targetRect) {
808
- const constrained = { ...spaceAround };
809
- if (this.boundary) {
810
- constrained.above = this.boundary.above >= 0 ? Math.min(spaceAround.above, this.boundary.above) : spaceAround.above;
811
- constrained.below = this.boundary.below >= 0 ? Math.min(spaceAround.below, this.boundary.below) : spaceAround.below;
812
- constrained.left = this.boundary.left >= 0 ? Math.min(spaceAround.left, this.boundary.left) : spaceAround.left;
813
- constrained.right = this.boundary.right >= 0 ? Math.min(spaceAround.right, this.boundary.right) : spaceAround.right;
814
- }
815
- const isRTL = this.getAttribute('dir') === 'rtl';
816
- if ((this.align === 'start' && !isRTL) || (this.align === 'end' && isRTL)) {
817
- constrained.left = Math.max(0, spaceRequired.width - (targetRect.width + spaceAround.right));
818
- } else if ((this.align === 'start' && isRTL) || (this.align === 'end' && !isRTL)) {
819
- constrained.right = Math.max(0, spaceRequired.width - (targetRect.width + spaceAround.left));
820
- }
821
- return constrained;
822
- }
823
-
824
- _getBottomTrayStyling() {
825
-
826
- let maxHeightOverride;
827
- let availableHeight = Math.min(window.innerHeight, window.screen.height);
828
- if (this._ifrauContextInfo) availableHeight = this._ifrauContextInfo.availableHeight;
829
- // default maximum height for bottom tray (42px margin)
830
- const mobileTrayMaxHeightDefault = availableHeight - minBackdropHeightMobile;
831
- if (this.maxHeight) {
832
- // if maxWidth provided is smaller, use the maxWidth
833
- maxHeightOverride = Math.min(mobileTrayMaxHeightDefault, this.maxHeight);
834
- } else {
835
- maxHeightOverride = mobileTrayMaxHeightDefault;
836
- }
837
- maxHeightOverride = `${maxHeightOverride}px`;
838
-
839
- let bottomOverride;
840
- if (this._ifrauContextInfo) {
841
- // Bottom override is measured as
842
- // the distance from the bottom of the screen
843
- const screenHeight =
844
- window.innerHeight
845
- - this._ifrauContextInfo.availableHeight
846
- + Math.min(this._ifrauContextInfo.top, 0);
847
- bottomOverride = `${screenHeight}px`;
848
- }
849
-
850
- const widthOverride = '100vw';
851
-
852
- const widthStyle = {
853
- minWidth: widthOverride,
854
- width: widthOverride,
855
- maxHeight: maxHeightOverride,
856
- bottom: bottomOverride
857
- };
858
-
859
- const contentWidthStyle = {
860
- /* set width of content in addition to width container so header and footer borders are full width */
861
- width: widthOverride
862
- };
863
-
864
- const headerStyle = {
865
- ...contentWidthStyle,
866
- minHeight: this._hasHeader ? 'auto' : '5px'
867
- };
868
-
869
- const footerStyle = {
870
- ...contentWidthStyle,
871
- minHeight: this._hasFooter || !this.noMobileCloseButton ? 'auto' : '5px'
872
- };
873
-
874
- const contentStyle = {
875
- ...contentWidthStyle,
876
- maxHeight: maxHeightOverride,
877
- };
878
-
879
- const closeButtonStyles = {
880
- display: !this.noMobileCloseButton ? 'inline-block' : 'none',
881
- width: this._getTrayFooterWidth(),
882
- padding: this._hasFooter && !this.noPaddingFooter ? '12px 0 0 0' : '12px',
883
- margin: this._getTrayFooterMargin()
884
- };
885
-
886
- return {
887
- 'width' : widthStyle,
888
- 'header' : headerStyle,
889
- 'footer' : footerStyle,
890
- 'content' : contentStyle,
891
- 'close' : closeButtonStyles
892
- };
893
- }
894
-
895
- _getDropdownStyling() {
896
- const widthStyle = {
897
- maxWidth: this.maxWidth ? `${this.maxWidth}px` : '',
898
- minWidth: this.minWidth ? `${this.minWidth}px` : '',
899
- /* add 2 to content width since scrollWidth does not include border */
900
- width: this._width ? `${this._width + 20}px` : ''
901
- };
902
-
903
- const contentWidthStyle = {
904
- minWidth: this.minWidth ? `${this.minWidth}px` : '',
905
- /* set width of content in addition to width container so header and footer borders are full width */
906
- width: this._width ? `${this._width + 18}px` : '',
907
- };
908
-
909
- const contentStyle = {
910
- ...contentWidthStyle,
911
- maxHeight: this._contentHeight ? `${this._contentHeight}px` : '',
912
- };
913
-
914
- const closeButtonStyle = {
915
- display: 'none',
916
- };
917
-
918
- return {
919
- 'width' : widthStyle,
920
- 'content' : contentStyle,
921
- 'close' : closeButtonStyle,
922
- 'header' : contentWidthStyle,
923
- 'footer' : contentWidthStyle
924
- };
925
- }
926
-
927
- _getLeftRightTrayStyling() {
928
-
929
- let maxWidthOverride = this.maxWidth;
930
- let availableWidth = Math.min(window.innerWidth, window.screen.width);
931
- if (this._ifrauContextInfo) availableWidth = this._ifrauContextInfo.availableWidth;
932
- // default maximum width for tray (30px margin)
933
- const mobileTrayMaxWidthDefault = Math.min(availableWidth - minBackdropWidthMobile, 420);
934
- if (maxWidthOverride) {
935
- // if maxWidth provided is smaller, use the maxWidth
936
- maxWidthOverride = Math.min(mobileTrayMaxWidthDefault, maxWidthOverride);
937
- } else {
938
- maxWidthOverride = mobileTrayMaxWidthDefault;
939
- }
940
-
941
- let minWidthOverride = this.minWidth;
942
- // minimum size - 285px
943
- const mobileTrayMinWidthDefault = 285;
944
- if (minWidthOverride) {
945
- // if minWidth provided is smaller, use the minumum width for tray
946
- minWidthOverride = Math.max(mobileTrayMinWidthDefault, minWidthOverride);
947
- } else {
948
- minWidthOverride = mobileTrayMinWidthDefault;
949
- }
950
-
951
- // if no width property set, automatically size to maximum width
952
- let widthOverride = this._width ? this._width : maxWidthOverride;
953
- // ensure width is between minWidth and maxWidth
954
- if (widthOverride && maxWidthOverride && widthOverride > (maxWidthOverride - 20)) widthOverride = maxWidthOverride - 20;
955
- if (widthOverride && minWidthOverride && widthOverride < (minWidthOverride - 20)) widthOverride = minWidthOverride - 20;
956
-
957
- maxWidthOverride = `${maxWidthOverride}px`;
958
- minWidthOverride = `${minWidthOverride}px`;
959
- const contentWidth = `${widthOverride + 18}px`;
960
- /* add 2 to content width since scrollWidth does not include border */
961
- const containerWidth = `${widthOverride + 20}px`;
962
-
963
- let maxHeightOverride = '';
964
- if (this._ifrauContextInfo) maxHeightOverride = `${this._ifrauContextInfo.availableHeight}px`;
965
-
966
- let topOverride;
967
- if (this._ifrauContextInfo) {
968
- // if inside iframe, use ifrauContext top as top of screen
969
- topOverride = `${this._ifrauContextInfo.top < 0 ? -this._ifrauContextInfo.top : 0}px`;
970
- } else if (window.innerHeight > window.screen.height) {
971
- // non-responsive page, manually override top to scroll distance
972
- topOverride = window.pageYOffset;
973
- }
974
-
975
- let rightOverride;
976
- let leftOverride;
977
- if (this.mobileTray === 'right') {
978
- // On non-responsive pages, the innerWidth may be wider than the screen,
979
- // override right to stick to right of viewport
980
- rightOverride = `${Math.max(window.innerWidth - window.screen.width, 0)}px`;
981
- }
982
- if (this.mobileTray === 'left') {
983
- // On non-responsive pages, the innerWidth may be wider than the screen,
984
- // override left to stick to left of viewport
985
- leftOverride = `${Math.max(window.innerWidth - window.screen.width, 0)}px`;
986
- }
987
-
988
- if (minWidthOverride > maxWidthOverride) {
989
- minWidthOverride = maxWidthOverride;
990
- }
991
- const widthStyle = {
992
- maxWidth: maxWidthOverride,
993
- minWidth: minWidthOverride,
994
- width: containerWidth,
995
- maxHeight: maxHeightOverride,
996
- top: topOverride,
997
- right: rightOverride,
998
- left: leftOverride,
999
- };
1000
-
1001
- const contentWidthStyle = {
1002
- minWidth: minWidthOverride,
1003
- /* set width of content in addition to width container so header and footer borders are full width */
1004
- width: contentWidth,
1005
- };
1006
-
1007
- const headerStyle = {
1008
- ...contentWidthStyle,
1009
- minHeight: this._hasHeader ? 'auto' : '5px'
1010
- };
1011
-
1012
- const footerStyle = {
1013
- ...contentWidthStyle,
1014
- minHeight: this._hasFooter || !this.noMobileCloseButton ? 'auto' : '5px'
1015
- };
1016
-
1017
- const contentStyle = {
1018
- ...contentWidthStyle,
1019
- maxHeight: maxHeightOverride,
1020
- };
1021
-
1022
- const closeButtonStyles = {
1023
- display: !this.noMobileCloseButton ? 'inline-block' : 'none',
1024
- width: this._getTrayFooterWidth(),
1025
- padding: this._hasFooter && !this.noPaddingFooter ? '12px 0 0 0' : '12px',
1026
- margin: this._getTrayFooterMargin()
1027
- };
1028
-
1029
- return {
1030
- 'width' : widthStyle,
1031
- 'header' : headerStyle,
1032
- 'footer' : footerStyle,
1033
- 'content' : contentStyle,
1034
- 'close' : closeButtonStyles
1035
- };
1036
- }
1037
-
1038
- _getOpenedAbove(spaceAround, spaceAroundScroll, spaceRequired) {
1039
- if (spaceAround.below >= spaceRequired.height) {
1040
- return false;
1041
- }
1042
- if (spaceAround.above >= spaceRequired.height) {
1043
- return true;
1044
- }
1045
- if (!this.noAutoFit) {
1046
- // if auto-fit is enabled, scroll will be enabled for the
1047
- // inner content so it will always fit in the available space
1048
- // so pick the largest space it can be displayed in
1049
- return spaceAround.above > spaceAround.below;
1050
- }
1051
- if (spaceAroundScroll.below >= spaceRequired.height) {
1052
- return false;
1053
- }
1054
- if (spaceAroundScroll.above >= spaceRequired.height) {
1055
- return true;
1056
- }
1057
- // if auto-fit is disabled and it doesn't fit in the scrollable space
1058
- // above or below, always open down because it can add scrollable space
1059
- return false;
1060
- }
1061
-
1062
- _getPointerPosition(targetRect) {
1063
- const position = {};
1064
-
1065
- const pointer = this.__getPointer();
1066
- if (!pointer) return position;
1067
-
1068
- const pointerRect = pointer.getBoundingClientRect();
1069
- const isRTL = this.getAttribute('dir') === 'rtl';
1070
- if (this.align === 'start' || this.align === 'end') {
1071
- const pointerXAdjustment = Math.min(20 + ((pointerRotatedLength - pointerLength) / 2), (targetRect.width - pointerLength) / 2);
1072
- if ((this.align === 'start' && !isRTL) || (this.align === 'end' && isRTL)) {
1073
- position.left = targetRect.left + pointerXAdjustment;
1074
- } else {
1075
- position.right = window.innerWidth - targetRect.right + pointerXAdjustment;
1076
- }
1077
- } else {
1078
- if (!isRTL) {
1079
- position.left = targetRect.left + ((targetRect.width - pointerRect.width) / 2);
1080
- } else {
1081
- position.right = window.innerWidth - targetRect.left - ((targetRect.width + pointerRect.width) / 2);
1082
- }
1083
- }
1084
- if (this.openedAbove) {
1085
- position.bottom = window.innerHeight - targetRect.top + this._verticalOffset - 8;
1086
- } else {
1087
- position.top = targetRect.top + targetRect.height + this._verticalOffset - 7;
1088
- }
1089
-
1090
- return position;
1091
- }
1092
-
1093
- _getPosition(spaceAround, targetRect, contentRect) {
1094
- const position = {};
1095
- const isRTL = this.getAttribute('dir') === 'rtl';
1096
- const positionXAdjustment = this._getPositionXAdjustment(spaceAround, targetRect, contentRect);
1097
-
1098
- if (positionXAdjustment !== null) {
1099
- if (!isRTL) {
1100
- position.left = targetRect.left + positionXAdjustment;
1101
- } else {
1102
- position.right = window.innerWidth - targetRect.left - targetRect.width + positionXAdjustment;
1103
- }
1104
- }
1105
- if (this.openedAbove) {
1106
- position.bottom = window.innerHeight - targetRect.top + this._verticalOffset;
1107
- } else {
1108
- position.top = targetRect.top + targetRect.height + this._verticalOffset;
1109
- }
1110
-
1111
- return position;
1112
- }
1113
-
1114
- _getPositionXAdjustment(spaceAround, targetRect, contentRect) {
1115
- const centerDelta = contentRect.width - targetRect.width;
1116
- const contentXAdjustment = centerDelta / 2;
1117
- if (!this.align && centerDelta <= 0) {
1118
- return contentXAdjustment * -1;
1119
- }
1120
- if (!this.align && spaceAround.left > contentXAdjustment && spaceAround.right > contentXAdjustment) {
1121
- // center with target
1122
- return contentXAdjustment * -1;
1123
- }
1124
- const isRTL = this.getAttribute('dir') === 'rtl';
1125
- if (!isRTL) {
1126
- if (spaceAround.left < contentXAdjustment) {
1127
- // slide content right (not enough space to center)
1128
- return spaceAround.left * -1;
1129
- } else if (spaceAround.right < contentXAdjustment) {
1130
- // slide content left (not enough space to center)
1131
- return (centerDelta * -1) + spaceAround.right;
1132
- }
1133
- } else {
1134
- if (spaceAround.left < contentXAdjustment) {
1135
- // slide content right (not enough space to center)
1136
- return (centerDelta * -1) + spaceAround.left;
1137
- } else if (spaceAround.right < contentXAdjustment) {
1138
- // slide content left (not enough space to center)
1139
- return spaceAround.right * -1;
1140
- }
1141
- }
1142
- if (this.align === 'start' || this.align === 'end') {
1143
- const shift = Math.min((targetRect.width / 2) - (20 + pointerLength / 2), 0); // 20 ~= 1rem
1144
- if (this.align === 'start') {
1145
- return shift;
1146
- } else {
1147
- return targetRect.width - contentRect.width - shift;
1148
- }
1149
- }
1150
- return null;
1151
- }
1152
-
1153
- _getTrayFooterMargin() {
1154
- let footerMargin;
1155
- if (this._hasFooter) {
1156
- footerMargin = '0';
1157
- } else if (this.getAttribute('dir') === 'rtl') {
1158
- footerMargin = '-20px -20px -20px 0px';
1159
- } else {
1160
- footerMargin = '-20px 0 -20px -20px';
1161
- }
1162
- return footerMargin;
1163
- }
1164
-
1165
- _getTrayFooterWidth() {
1166
- let footerWidth;
1167
- if (this.noPaddingFooter) {
1168
- footerWidth = 'calc(100% - 24px)';
1169
- } else if (this._hasFooter) {
1170
- footerWidth = '100%';
1171
- } else {
1172
- footerWidth = 'calc(100% + 16px)';
1173
- }
1174
- return footerWidth;
1175
- }
1176
-
1177
- _handleFocusTrapEnter() {
1178
- if (this.__applyFocus && !this.noAutoFocus) {
1179
- const content = this.__getWidthContainer();
1180
- const focusable = getFirstFocusableDescendant(content);
1181
- if (focusable) {
1182
- // Removing the rAF call can allow infinite focus looping to happen in content using a focus trap
1183
- requestAnimationFrame(() => focusable.focus());
1184
- } else {
1185
- content.setAttribute('tabindex', '-1');
1186
- content.focus();
1187
- }
1188
- }
1189
- /** Dispatched when user focus enters the dropdown content (trap-focus option only) */
1190
- this.dispatchEvent(new CustomEvent('d2l-dropdown-focus-enter', { detail:{ applyFocus: this.__applyFocus } }));
1191
- }
1192
-
1193
- async _handleMobileResize() {
1194
- this._useMobileStyling = this.mediaQueryList.matches;
1195
- if (this.opened) this._showBackdrop = this._useMobileStyling && this.mobileTray;
1196
- if (this.opened) await this.__position();
1197
- }
1198
-
1199
- _renderContent() {
1200
-
1201
- const mobileTrayRightLeft = this._useMobileStyling && (this.mobileTray === 'right' || this.mobileTray === 'left');
1202
- const mobileTrayBottom = this._useMobileStyling && (this.mobileTray === 'bottom');
1203
-
1204
- let stylesMap;
1205
- if (mobileTrayBottom) {
1206
- stylesMap = this._getBottomTrayStyling();
1207
- } else if (mobileTrayRightLeft) {
1208
- stylesMap = this._getLeftRightTrayStyling();
1209
- } else {
1210
- stylesMap = this._getDropdownStyling();
1211
- }
1212
- const widthStyle = stylesMap['width'];
1213
- const headerStyle = stylesMap['header'];
1214
- const footerStyle = stylesMap['footer'];
1215
- const contentStyle = stylesMap['content'];
1216
- const closeButtonStyles = stylesMap['close'];
1217
-
1218
- const topClasses = {
1219
- 'd2l-dropdown-content-top': true,
1220
- 'd2l-dropdown-content-top-scroll': this._topOverflow,
1221
- 'd2l-dropdown-content-header': this._hasHeader
1222
- };
1223
- const bottomClasses = {
1224
- 'd2l-dropdown-content-bottom': true,
1225
- 'd2l-dropdown-content-bottom-scroll': this._bottomOverflow,
1226
- 'd2l-dropdown-content-footer': this._hasFooter || (this._useMobileStyling && this.mobileTray && !this.noMobileCloseButton)
1227
- };
1228
-
1229
- let dropdownContentSlots = html`
1230
- <div
1231
- id="d2l-dropdown-wrapper"
1232
- class="d2l-dropdown-content-width vdiff-target"
1233
- style=${styleMap(widthStyle)}
1234
- ?data-closing="${this._closing}">
1235
- <div class=${classMap(topClasses)} style=${styleMap(headerStyle)}>
1236
- <slot name="header" @slotchange="${this.__handleHeaderSlotChange}"></slot>
1237
- </div>
1238
- <div
1239
- class="d2l-dropdown-content-container"
1240
- style=${styleMap(contentStyle)}
1241
- @scroll=${this.__toggleScrollStyles}>
1242
- <slot class="d2l-dropdown-content-slot"></slot>
1243
- </div>
1244
- <div class=${classMap(bottomClasses)} style=${styleMap(footerStyle)}>
1245
- <slot name="footer" @slotchange="${this.__handleFooterSlotChange}"></slot>
1246
- <d2l-button
1247
- class="dropdown-close-btn"
1248
- style=${styleMap(closeButtonStyles)}
1249
- @click=${this.close}>
1250
- ${this.localize('components.dropdown.close')}
1251
- </d2l-button>
1252
- </div>
1253
- </div>
1254
- `;
1255
-
1256
- if (this.trapFocus) {
1257
- dropdownContentSlots = html`
1258
- <d2l-focus-trap @d2l-focus-trap-enter="${this._handleFocusTrapEnter}" ?trap="${this.opened}">
1259
- ${dropdownContentSlots}
1260
- </d2l-focus-trap>`;
1261
- }
1262
-
1263
- const positionStyle = {};
1264
- if (this._position) {
1265
- for (const prop in this._position) {
1266
- positionStyle[prop] = `${this._position[prop]}px`;
1267
- }
1268
- }
1269
-
1270
- const dropdown = html`
1271
- <div class="d2l-dropdown-content-position" style=${styleMap(positionStyle)}>
1272
- ${dropdownContentSlots}
1273
- </div>
1274
- `;
1275
-
1276
- const pointerPositionStyle = {};
1277
- if (this._pointerPosition) {
1278
- for (const prop in this._pointerPosition) {
1279
- pointerPositionStyle[prop] = `${this._pointerPosition[prop]}px`;
1280
- }
1281
- }
1282
-
1283
- const pointer = html`
1284
- <div class="d2l-dropdown-content-pointer" style="${styleMap(pointerPositionStyle)}">
1285
- <div></div>
1286
- </div>
1287
- `;
1288
-
1289
- return (this.mobileTray) ? html`
1290
- ${dropdown}
1291
- <d2l-backdrop
1292
- for-target="d2l-dropdown-wrapper"
1293
- ?shown="${this._showBackdrop}" >
1294
- </d2l-backdrop>
1295
- ${pointer}`
1296
- : html`${dropdown}${pointer}`;
1297
- }
1298
-
1299
- };