@brightspace-ui/core 3.74.0 → 3.74.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -35,6 +35,7 @@
35
35
  <d2l-object-property-list-item text="Example item"></d2l-object-property-list-item>
36
36
  <d2l-object-property-list-item text="Example item with icon" icon="tier1:grade"></d2l-object-property-list-item>
37
37
  <d2l-object-property-list-item-link text="Example link" href="https://www.d2l.com/"></d2l-object-property-list-item-link>
38
+ <d2l-object-property-list-item-link target="_blank" text="Example new tab link" href="https://www.d2l.com/"></d2l-object-property-list-item-link>
38
39
  <d2l-object-property-list-item-link text="Example link with icon" href="https://www.d2l.com/" icon="tier1:alert"></d2l-object-property-list-item-link>
39
40
  </d2l-object-property-list>
40
41
  </template>
@@ -1,7 +1,7 @@
1
+ import '../link/link.js';
1
2
  import { FocusMixin } from '../../mixins/focus/focus-mixin.js';
2
3
  import { html } from 'lit';
3
4
  import { ifDefined } from 'lit/directives/if-defined.js';
4
- import { linkStyles } from '../link/link.js';
5
5
  import { ObjectPropertyListItem } from './object-property-list-item.js';
6
6
 
7
7
  /**
@@ -29,29 +29,20 @@ class ObjectPropertyListItemLink extends FocusMixin(ObjectPropertyListItem) {
29
29
  };
30
30
  }
31
31
 
32
- static get styles() {
33
- return [
34
- ...super.styles,
35
- linkStyles
36
- ];
37
- }
38
-
39
32
  static get focusElementSelector() {
40
- return '.d2l-link';
33
+ return 'd2l-link';
41
34
  }
42
35
 
43
36
  render() {
44
37
  return html`
45
38
  ${this._renderIcon()}
46
39
  ${!this.skeleton ? html`
47
- <a
40
+ <d2l-link
48
41
  ?download="${this.download}"
49
- class="d2l-link"
50
42
  href="${ifDefined(this.href)}"
51
- target="${ifDefined(this.target)}"
52
- >
43
+ target="${ifDefined(this.target)}">
53
44
  ${this._renderText()}
54
- </a>
45
+ </d2l-link>
55
46
  ` : this._renderText()}
56
47
  ${this._renderSeparator()}
57
48
  `;
@@ -6,10 +6,20 @@
6
6
  <meta charset="UTF-8">
7
7
  <link rel="stylesheet" href="../../demo/styles.css" type="text/css">
8
8
  <script type="module">
9
+ import '../../button/button.js';
9
10
  import '../../button/button-subtle.js';
10
11
  import '../../demo/demo-page.js';
12
+ import '../../dialog/dialog.js';
11
13
  import '../../link/link.js';
12
14
  import '../test/popover.js';
15
+
16
+ window.wireUpPopover = demo => {
17
+ const popover = demo.querySelector('d2l-test-popover');
18
+ const openButton = demo.querySelector('d2l-button-subtle[text="Open"]');
19
+ openButton.addEventListener('click', () => popover.opened = !popover.opened);
20
+ const closeButton = demo.querySelector('d2l-button-subtle[text="Close"]');
21
+ if (closeButton) closeButton.addEventListener('click', () => popover.opened = false);
22
+ };
13
23
  </script>
14
24
  </head>
15
25
 
@@ -20,41 +30,256 @@
20
30
  <h2>Popover</h2>
21
31
  <d2l-demo-snippet>
22
32
  <template>
23
- <d2l-button-subtle id="open1" text="Open"></d2l-button-subtle>
24
- <d2l-test-popover id="popover1" style="max-width: 400px;">
33
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
34
+ <d2l-test-popover>
35
+ <div slot="header">header content</div>
36
+ <d2l-button-subtle text="Close"></d2l-button-subtle>
37
+ <div>Sink me piracy Gold Road quarterdeck wherry long boat line pillage walk the plank Plate Fleet. Haul wind black spot strike colors deadlights lee Barbary Coast yo-ho-ho ballast gally Shiver me timbers. Sea Legs quarterdeck yard scourge of the seven seas coffer plunder lanyard holystone code of conduct belay.</div>
38
+ <div style="border: 1px solid black; width: 600px;">stuff</div>
39
+ <div slot="footer">footer content</div>
40
+ </d2l-test-popover>
41
+ <script>
42
+ window.wireUpPopover(document.currentScript.parentNode);
43
+ </script>
44
+ </template>
45
+ </d2l-demo-snippet>
46
+
47
+ <h2>Popover (content width less than min-width)</h2>
48
+ <d2l-demo-snippet>
49
+ <template>
50
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
51
+ <d2l-test-popover>
52
+ <div>1</div>
53
+ </d2l-test-popover>
54
+ <script>
55
+ window.wireUpPopover(document.currentScript.parentNode);
56
+ </script>
57
+ </template>
58
+ </d2l-demo-snippet>
59
+
60
+ <h2>Popover (content width greater than min-width, less than max-width)</h2>
61
+ <d2l-demo-snippet>
62
+ <template>
63
+ <div style="text-align: center;"><d2l-button-subtle text="Open"></d2l-button-subtle></div>
64
+ <d2l-test-popover>
65
+ <div>Sink me piracy Gold Road.</div>
66
+ </d2l-test-popover>
67
+ <script>
68
+ window.wireUpPopover(document.currentScript.parentNode);
69
+ </script>
70
+ </template>
71
+ </d2l-demo-snippet>
72
+
73
+ <h2>Popover (content width greater than max-width)</h2>
74
+ <d2l-demo-snippet>
75
+ <template>
76
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
77
+ <d2l-test-popover>
25
78
  <div>Sink me piracy Gold Road quarterdeck wherry long boat line pillage walk the plank Plate Fleet. Haul wind black spot strike colors deadlights lee Barbary Coast yo-ho-ho ballast gally Shiver me timbers. Sea Legs quarterdeck yard scourge of the seven seas coffer plunder lanyard holystone code of conduct belay.</div>
26
- <d2l-button-subtle id="close1" text="Close"></d2l-button-subtle>
27
79
  </d2l-test-popover>
28
80
  <script>
29
- const popover1 = document.querySelector('#popover1');
30
- document.querySelector('#open1').addEventListener('click', () => popover1.opened = !popover1.opened);
31
- document.querySelector('#close1').addEventListener('click', () => popover1.opened = false);
81
+ window.wireUpPopover(document.currentScript.parentNode);
82
+ </script>
83
+ </template>
84
+ </d2l-demo-snippet>
85
+
86
+ <h2>Popover (no pointer)</h2>
87
+ <d2l-demo-snippet>
88
+ <template>
89
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
90
+ <d2l-test-popover no-pointer>
91
+ <div>Sink me piracy Gold Road quarterdeck wherry long boat line pillage walk the plank Plate Fleet. Haul wind black spot strike colors deadlights lee Barbary Coast yo-ho-ho ballast gally Shiver me timbers. Sea Legs quarterdeck yard scourge of the seven seas coffer plunder lanyard holystone code of conduct belay.</div>
92
+ </d2l-test-popover>
93
+ <script>
94
+ window.wireUpPopover(document.currentScript.parentNode);
95
+ </script>
96
+ </template>
97
+ </d2l-demo-snippet>
98
+
99
+ <h2>Popover (custom max-width, single-line)</h2>
100
+ <d2l-demo-snippet>
101
+ <template>
102
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
103
+ <d2l-test-popover max-width="1000">
104
+ <div>Sink me piracy Gold Road quarterdeck wherry and some.</div>
105
+ </d2l-test-popover>
106
+ <script>
107
+ window.wireUpPopover(document.currentScript.parentNode);
108
+ </script>
109
+ </template>
110
+ </d2l-demo-snippet>
111
+
112
+ <h2>Popover (custom max-width, multi-line)</h2>
113
+ <d2l-demo-snippet>
114
+ <template>
115
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
116
+ <d2l-test-popover max-width="1000">
117
+ <div>Sink me piracy Gold Road quarterdeck wherry long boat line pillage walk the plank Plate Fleet. Haul wind black spot strike colors deadlights lee Barbary Coast yo-ho-ho ballast gally Shiver me timbers. Sea Legs quarterdeck yard scourge of the seven seas coffer plunder lanyard holystone code of conduct belay.</div>
118
+ </d2l-test-popover>
119
+ <script>
120
+ window.wireUpPopover(document.currentScript.parentNode);
121
+ </script>
122
+ </template>
123
+ </d2l-demo-snippet>
124
+
125
+ <h2>Popover (custom min-width)</h2>
126
+ <d2l-demo-snippet>
127
+ <template>
128
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
129
+ <d2l-test-popover min-width="60">
130
+ <div>1</div>
131
+ </d2l-test-popover>
132
+ <script>
133
+ window.wireUpPopover(document.currentScript.parentNode);
134
+ </script>
135
+ </template>
136
+ </d2l-demo-snippet>
137
+
138
+ <h2>Popover (position location)</h2>
139
+ <d2l-demo-snippet>
140
+ <template>
141
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
142
+ <d2l-test-popover position-location="block-start">
143
+ <div>Sink me piracy Gold Road.</div>
144
+ </d2l-test-popover>
145
+ <script>
146
+ window.wireUpPopover(document.currentScript.parentNode);
147
+ </script>
148
+ </template>
149
+ </d2l-demo-snippet>
150
+
151
+ <h2>Popover (position span)</h2>
152
+ <d2l-demo-snippet>
153
+ <template>
154
+ <div style="text-align: center;"><d2l-button-subtle text="Open"></d2l-button-subtle></div>
155
+ <d2l-test-popover position-span="end">
156
+ <div>Sink me piracy Gold Road.</div>
157
+ </d2l-test-popover>
158
+ <script>
159
+ window.wireUpPopover(document.currentScript.parentNode);
160
+ </script>
161
+ </template>
162
+ </d2l-demo-snippet>
163
+
164
+ <h2>Popover (in a scrollable container)</h2>
165
+ <d2l-demo-snippet>
166
+ <template>
167
+ <div style="height: 250px; overflow: scroll;">
168
+ <p>Gabion warp American Main gunwalls cutlass gally cable gibbet jib keel. Trysail chantey swing the lead hempen halter hang the jib chase Jack Tar furl galleon scurvy. Brig splice the main brace provost pink rutters tender heave to Shiver me timbers belaying pin Brethren of the Coast.</p>
169
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
170
+ <d2l-test-popover>
171
+ <div>Sink me piracy Gold Road quarterdeck wherry long boat line pillage walk the plank Plate Fleet. Haul wind black spot strike colors deadlights lee Barbary Coast yo-ho-ho ballast gally Shiver me timbers. Sea Legs quarterdeck yard scourge of the seven seas coffer plunder lanyard holystone code of conduct belay.</div>
172
+ </d2l-test-popover>
173
+ <p>Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.</p>
174
+ <p>Trysail Sail ho Corsair red ensign hulk smartly boom jib rum gangway. Case shot Shiver me timbers gangplank crack Jennys tea cup ballast Blimey lee snow crow's nest rutters. Fluke jib scourge of the seven seas boatswain schooner gaff booty Jack Tar transom spirits.</p>
175
+ <p>Hardtack hang the jib haul wind booty pillage spike hearties Pirate Round tack yard. Piracy fire ship trysail stern scurvy blow the man down skysail salmagundi lee grog blossom. Hands gabion ho schooner lad ballast keel mutiny square-rigged haul wind.</p>
176
+ </div>
177
+ <script>
178
+ window.wireUpPopover(document.currentScript.parentNode);
32
179
  </script>
33
180
  </template>
34
181
  </d2l-demo-snippet>
35
182
 
183
+ <h2>Popover (with DOM mutation)</h2>
184
+ <d2l-demo-snippet>
185
+ <template>
186
+ <div>
187
+ <div id="mutations-above"></div>
188
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
189
+ <d2l-test-popover>
190
+ <d2l-button-subtle id="mutations-add-above">Add to Above</d2l-button-subtle>
191
+ </d2l-test-popover>
192
+ </div>
193
+ <script>
194
+ window.wireUpPopover(document.currentScript.parentNode);
195
+
196
+ document.querySelector('#mutations-add-above').addEventListener('click', e => {
197
+ const mutationsContainer = e.target.parentNode.parentNode.parentNode.querySelector('#mutations-above');
198
+ const newContent = document.createElement('p');
199
+ newContent.innerText = 'Blimey brigantine gangplank booty rope\'s end lugger heave down run a rig Yellow Jack dead men tell no tales. Pirate Round scuppers spanker hogshead Davy Jone\'s Locker heave down wench fluke marooned boom. Lanyard salmagundi careen doubloon swing the lead shrouds crow\'s nest parrel gun pressgang.';
200
+ mutationsContainer.appendChild(newContent);
201
+ });
202
+ </script>
203
+ </template>
204
+ </d2l-demo-snippet>
205
+
206
+ <h2>Popover (in a dialog)</h2>
207
+ <d2l-demo-snippet>
208
+ <template>
209
+ <d2l-button id="openDialog1">Show Dialog</d2l-button>
210
+ <d2l-dialog id="dialog1" title-text="Dialog Title">
211
+ <div>
212
+ <p>Bilge tack furl dance the hempen jig fathom weigh anchor mizzen Blimey Jack Ketch flogging. Lee galleon avast schooner long clothes scuppers pinnace bucko deadlights gibbet. Nipper brigantine Buccaneer Gold Road matey gangway booty tender killick Brethren of the Coast.</p>
213
+ <d2l-button-subtle id="openPopover1" text="Open"></d2l-button-subtle>
214
+ <d2l-test-popover id="popover1">
215
+ <div>Sink me piracy Gold Road quarterdeck wherry long boat line pillage walk the plank Plate Fleet. Haul wind black spot strike colors deadlights lee Barbary Coast yo-ho-ho ballast gally Shiver me timbers. Sea Legs quarterdeck yard scourge of the seven seas coffer plunder lanyard holystone code of conduct belay.</div>
216
+ </d2l-test-popover>
217
+ <p>Piracy bowsprit Arr shrouds salmagundi scuttle heave down doubloon trysail Jack Ketch. Killick boom Jolly Roger Pieces of Eight crack Jennys tea cup Cat o'nine tails league Privateer topgallant lanyard. Cat o'nine tails coxswain scurvy spirits keelhaul quarterdeck matey nipper scallywag Jolly Roger.</p>
218
+ <p>Clap of thunder aye Corsair Barbary Coast prow shrouds schooner keel topmast code of conduct. Matey case shot spirits Davy Jones' Locker draft schooner Brethren of the Coast barkadeer jury mast measured fer yer chains. Bilge rat run a rig gaff warp loot clipper belaying pin main sheet lanyard avast.</p>
219
+ <p>Pieces of Eight lookout Letter of Marque mutiny tender spanker Jack Ketch long clothes crow's nest line. Lass draught six pounders spirits skysail jib American Main chase hulk coxswain. Run a shot across the bow galleon Cat o'nine tails brigantine reef Admiral of the Black wherry quarterdeck keelhaul coffer.</p>
220
+ </div>
221
+ <d2l-button slot="footer" primary data-dialog-action="ok">Click Me!</d2l-button>
222
+ <d2l-button slot="footer" data-dialog-action>Cancel</d2l-button>
223
+ </d2l-dialog>
224
+ <script>
225
+ document.querySelector('#openDialog1').addEventListener('click', () => {
226
+ document.querySelector('#dialog1').opened = true;
227
+ });
228
+ document.querySelector('#openPopover1').addEventListener('click', () => {
229
+ const popover = document.querySelector('#popover1');
230
+ popover.opened = !popover.opened;
231
+ });
232
+ </script>
233
+ </template>
234
+ </d2l-demo-snippet>
235
+
236
+ <h2>Popover (in another popover)</h2>
237
+ <d2l-demo-snippet>
238
+ <template>
239
+ <d2l-button-subtle id="outerOpener" text="Open"></d2l-button-subtle>
240
+ <d2l-test-popover id="outerPopover">
241
+ <d2l-button-subtle id="innerOpener" text="Open Nested"></d2l-button-subtle>
242
+ <d2l-test-popover id="innerPopover">
243
+ <div>Piracy bowsprit Arr shrouds salmagundi scuttle heave down doubloon trysail Jack Ketch. Killick boom Jolly Roger Pieces of Eight crack Jennys tea cup Cat o'nine tails league Privateer topgallant lanyard. Cat o'nine tails coxswain scurvy spirits keelhaul quarterdeck matey nipper scallywag Jolly Roger.</div>
244
+ </d2l-test-popover>
245
+ </d2l-test-popover>
246
+ <script>
247
+ document.querySelector('#outerOpener').addEventListener('click', () => {
248
+ const popover = document.querySelector('#outerPopover');
249
+ popover.opened = !popover.opened;
250
+ });
251
+ document.querySelector('#innerOpener').addEventListener('click', () => {
252
+ const popover = document.querySelector('#innerPopover');
253
+ popover.opened = !popover.opened;
254
+ });
255
+ </script>
256
+ </template>
257
+ </d2l-demo-snippet>
258
+
259
+
36
260
  <h2>Popover (trap-focus)</h2>
37
261
  <d2l-demo-snippet>
38
262
  <template>
39
- <d2l-button-subtle id="open2" text="Open"></d2l-button-subtle>
40
- <d2l-test-popover trap-focus id="popover2" style="max-width: 400px;">
263
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
264
+ <d2l-test-popover trap-focus style="max-width: 400px;">
41
265
  <d2l-link href="https://pirateipsum.me/" target="_blank">Pirate Ipsum</d2l-link>
42
266
  <div>Sink me piracy Gold Road quarterdeck wherry long boat line pillage walk the plank Plate Fleet. Haul wind black spot strike colors deadlights lee Barbary Coast yo-ho-ho ballast gally Shiver me timbers. Sea Legs quarterdeck yard scourge of the seven seas coffer plunder lanyard holystone code of conduct belay.</div>
43
- <d2l-button-subtle id="close2" text="Close"></d2l-button-subtle>
267
+ <d2l-button-subtle text="Close"></d2l-button-subtle>
44
268
  </d2l-test-popover>
45
269
  <script>
46
- const popover2 = document.querySelector('#popover2');
47
- document.querySelector('#open2').addEventListener('click', () => popover2.opened = !popover2.opened);
48
- document.querySelector('#close2').addEventListener('click', () => popover2.opened = false);
49
- popover2.addEventListener('d2l-popover-focus-enter', e => console.log(e.type, e));
270
+ window.wireUpPopover(document.currentScript.parentNode);
50
271
  </script>
51
272
  </template>
273
+ </template>
52
274
  </d2l-demo-snippet>
53
275
 
54
- <script>
55
- document.addEventListener('d2l-popover-open', e => console.log(e.type, e));
56
- document.addEventListener('d2l-popover-close', e => console.log(e.type, e));
57
- </script>
276
+ </d2l-demo-page>
277
+
278
+ <script>
279
+ document.addEventListener('d2l-popover-open', e => console.log(e.type, e));
280
+ document.addEventListener('d2l-popover-close', e => console.log(e.type, e));
281
+ document.addEventListener('d2l-popover-focus-enter', e => console.log(e.type, e), true);
282
+ </script>
58
283
  </body>
59
284
 
60
285
  </html>
@@ -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: auto;
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._handleAutoCloseClick = this._handleAutoCloseClick.bind(this);
83
- this._handleAutoCloseFocus = this._handleAutoCloseFocus.bind(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) this._addAutoCloseHandlers();
162
+ if (this._opened) {
163
+ this.#addAutoCloseHandlers();
164
+ this.#addRepositionHandlers();
165
+ }
89
166
  }
90
167
 
91
168
  disconnectedCallback() {
92
169
  super.disconnectedCallback();
93
- this._removeAutoCloseHandlers();
94
- this._clearDismissible();
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._removeAutoCloseHandlers();
106
- this._clearDismissible();
183
+ this.#removeAutoCloseHandlers();
184
+ this.#removeRepositionHandlers();
185
+ this.#clearDismissible();
107
186
  await this.updateComplete; // wait before applying focus to opener
108
- this._focusOpener();
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._addAutoCloseHandlers();
230
+ this.#addAutoCloseHandlers();
231
+
232
+ await this.#position();
233
+
131
234
  this._dismissibleId = setDismissible(() => this.close());
132
- this._focusContent(this);
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
- _addAutoCloseHandlers() {
142
- this.addEventListener('blur', this._handleAutoCloseFocus, { capture: true });
143
- document.body.addEventListener('focus', this._handleAutoCloseFocus, { capture: true });
144
- document.addEventListener('click', this._handleAutoCloseClick, { capture: true });
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
- _clearDismissible() {
363
+ #clearDismissible() {
148
364
  if (!this._dismissibleId) return;
149
365
  clearDismissible(this._dismissibleId);
150
366
  this._dismissibleId = null;
151
367
  }
152
368
 
153
- _focusContent(container) {
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
- // Removing the rAF call can allow infinite focus looping to happen in content using a focus trap
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._getContentContainer();
389
+ const content = this.#getContentContainer();
162
390
  content.setAttribute('tabindex', '-1');
163
391
  content.focus();
164
392
  }
165
393
  }
166
394
 
167
- _focusOpener() {
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
- _getContentContainer() {
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
- _handleAutoCloseClick(e) {
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._getContentContainer(), rootTarget)
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
- _handleAutoCloseFocus() {
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
- _handleFocusTrapEnter() {
216
- this._focusContent(this._getContentContainer());
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
- _removeAutoCloseHandlers() {
223
- this.removeEventListener('blur', this._handleAutoCloseFocus, { capture: true });
224
- document.body?.removeEventListener('focus', this._handleAutoCloseFocus, { capture: true }); // DE41322: document.body can be null in some scenarios
225
- document.removeEventListener('click', this._handleAutoCloseClick, { capture: true });
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
- _renderPopover() {
229
- const content = html`<div class="content"><slot></slot></div>`;
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
- if (this._trapFocus) return html`<d2l-focus-trap @d2l-focus-trap-enter="${this._handleFocusTrapEnter}" ?trap="${this._opened}">
232
- ${content}
233
- </d2l-focus-trap>`;
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
- return content;
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
  };
@@ -10599,6 +10599,36 @@
10599
10599
  "name": "d2l-test-popover",
10600
10600
  "path": "./components/popover/test/popover.js",
10601
10601
  "attributes": [
10602
+ {
10603
+ "name": "max-height",
10604
+ "description": "Max-height. Note that the default behaviour is to be as tall as necessary within the viewport, so this property is usually not needed.",
10605
+ "type": "number"
10606
+ },
10607
+ {
10608
+ "name": "max-width",
10609
+ "description": "Max-width (undefined). Specify a number that would be the px value.",
10610
+ "type": "number"
10611
+ },
10612
+ {
10613
+ "name": "min-height",
10614
+ "description": "Min-height used 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.",
10615
+ "type": "number"
10616
+ },
10617
+ {
10618
+ "name": "min-width",
10619
+ "description": "Min-width (undefined). Specify a number that would be the px value.",
10620
+ "type": "number"
10621
+ },
10622
+ {
10623
+ "name": "position-location",
10624
+ "description": "Position the popover before or after the opener. Default is \"block-end\" (after).",
10625
+ "type": "'block-start'|'block-end'"
10626
+ },
10627
+ {
10628
+ "name": "position-span",
10629
+ "description": "Position the popover to span from the opener edge to this grid line. Default is \"all\" (centered).",
10630
+ "type": "'start'|'end'|'all'"
10631
+ },
10602
10632
  {
10603
10633
  "name": "no-auto-close",
10604
10634
  "description": "Whether to disable auto-close/light-dismiss",
@@ -10611,6 +10641,12 @@
10611
10641
  "type": "boolean",
10612
10642
  "default": "false"
10613
10643
  },
10644
+ {
10645
+ "name": "no-pointer",
10646
+ "description": "Render without a pointer",
10647
+ "type": "boolean",
10648
+ "default": "false"
10649
+ },
10614
10650
  {
10615
10651
  "name": "opened",
10616
10652
  "description": "Whether the popover is open or not",
@@ -10625,6 +10661,42 @@
10625
10661
  }
10626
10662
  ],
10627
10663
  "properties": [
10664
+ {
10665
+ "name": "maxHeight",
10666
+ "attribute": "max-height",
10667
+ "description": "Max-height. Note that the default behaviour is to be as tall as necessary within the viewport, so this property is usually not needed.",
10668
+ "type": "number"
10669
+ },
10670
+ {
10671
+ "name": "maxWidth",
10672
+ "attribute": "max-width",
10673
+ "description": "Max-width (undefined). Specify a number that would be the px value.",
10674
+ "type": "number"
10675
+ },
10676
+ {
10677
+ "name": "minHeight",
10678
+ "attribute": "min-height",
10679
+ "description": "Min-height used 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.",
10680
+ "type": "number"
10681
+ },
10682
+ {
10683
+ "name": "minWidth",
10684
+ "attribute": "min-width",
10685
+ "description": "Min-width (undefined). Specify a number that would be the px value.",
10686
+ "type": "number"
10687
+ },
10688
+ {
10689
+ "name": "positionLocation",
10690
+ "attribute": "position-location",
10691
+ "description": "Position the popover before or after the opener. Default is \"block-end\" (after).",
10692
+ "type": "'block-start'|'block-end'"
10693
+ },
10694
+ {
10695
+ "name": "positionSpan",
10696
+ "attribute": "position-span",
10697
+ "description": "Position the popover to span from the opener edge to this grid line. Default is \"all\" (centered).",
10698
+ "type": "'start'|'end'|'all'"
10699
+ },
10628
10700
  {
10629
10701
  "name": "noAutoClose",
10630
10702
  "attribute": "no-auto-close",
@@ -10639,6 +10711,13 @@
10639
10711
  "type": "boolean",
10640
10712
  "default": "false"
10641
10713
  },
10714
+ {
10715
+ "name": "noPointer",
10716
+ "attribute": "no-pointer",
10717
+ "description": "Render without a pointer",
10718
+ "type": "boolean",
10719
+ "default": "false"
10720
+ },
10642
10721
  {
10643
10722
  "name": "opened",
10644
10723
  "attribute": "opened",
@@ -10664,6 +10743,10 @@
10664
10743
  {
10665
10744
  "name": "d2l-popover-focus-enter",
10666
10745
  "description": "Dispatched when user focus enters the popover (trap-focus option only)"
10746
+ },
10747
+ {
10748
+ "name": "d2l-popover-position",
10749
+ "description": "Dispatched when the popover position finishes adjusting"
10667
10750
  }
10668
10751
  ]
10669
10752
  },
@@ -98,7 +98,10 @@ export function loadMathJax(mathJaxConfig) {
98
98
  options: {
99
99
  menuOptions: {
100
100
  settings: { zoom: 'None' }
101
- }
101
+ },
102
+ skipHtmlTags: [
103
+ 'd2l-html-block' // Prevents MathJax from reaching into the html-block to try to parse what's inside; we leave that to the custom renderer
104
+ ]
102
105
  },
103
106
  loader: {
104
107
  load: ['ui/menu']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "3.74.0",
3
+ "version": "3.74.2",
4
4
  "description": "A collection of accessible, free, open-source web components for building Brightspace applications",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/BrightspaceUI/core.git",