@brightspace-ui/core 3.77.0 → 3.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -273,6 +273,22 @@
273
273
  </template>
274
274
  </d2l-demo-snippet>
275
275
 
276
+ <h2>Popover (mobile tray)</h2>
277
+ <d2l-demo-snippet>
278
+ <template>
279
+ <d2l-button-subtle text="Open"></d2l-button-subtle>
280
+ <d2l-test-popover mobile-tray-location="inline-start" style="max-width: 400px;">
281
+ <d2l-link href="https://pirateipsum.me/" target="_blank">Pirate Ipsum</d2l-link>
282
+ <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>
283
+ <d2l-button-subtle text="Close"></d2l-button-subtle>
284
+ </d2l-test-popover>
285
+ <script>
286
+ window.wireUpPopover(document.currentScript.parentNode);
287
+ </script>
288
+ </template>
289
+ </template>
290
+ </d2l-demo-snippet>
291
+
276
292
  </d2l-demo-page>
277
293
 
278
294
  <script>
@@ -12,6 +12,8 @@ const defaultPreferredPosition = {
12
12
  span: 'all', // start, end, all
13
13
  allowFlip: true
14
14
  };
15
+ const minBackdropHeightMobile = 42;
16
+ const minBackdropWidthMobile = 30;
15
17
  const pointerLength = 16;
16
18
  const pointerRotatedLength = Math.SQRT2 * parseFloat(pointerLength);
17
19
  const isSupported = ('popover' in HTMLElement.prototype);
@@ -30,6 +32,9 @@ export const PopoverMixin = superclass => class extends superclass {
30
32
  _maxWidth: { state: true },
31
33
  _minHeight: { state: true },
32
34
  _minWidth: { state: true },
35
+ _mobile: { type: Boolean, reflect: true, attribute: '_mobile' },
36
+ _mobileBreakpoint: { state: true },
37
+ _mobileTrayLocation: { type: String, reflect: true, attribute: '_mobile-tray-location' },
33
38
  _noAutoClose: { state: true },
34
39
  _noAutoFit: { state: true },
35
40
  _noAutoFocus: { state: true },
@@ -140,6 +145,58 @@ export const PopoverMixin = superclass => class extends superclass {
140
145
  }
141
146
  }
142
147
 
148
+ :host([_mobile][_mobile-tray-location]) .content-width {
149
+ position: fixed;
150
+ z-index: 1000;
151
+ }
152
+
153
+ :host([_mobile][_mobile-tray-location="inline-start"]) .content-width,
154
+ :host([_mobile][_mobile-tray-location="inline-end"]) .content-width {
155
+ inset-block-end: 0;
156
+ inset-block-start: 0;
157
+ }
158
+
159
+ :host([_mobile][_mobile-tray-location="inline-start"]) .content-width {
160
+ border-end-start-radius: 0;
161
+ border-start-start-radius: 0;
162
+ }
163
+
164
+ :host([_mobile][_mobile-tray-location="inline-end"]) .content-width {
165
+ border-end-end-radius: 0;
166
+ border-start-end-radius: 0;
167
+ }
168
+
169
+ :host([_mobile][_mobile-tray-location="block-end"]) .content-width {
170
+ border-end-end-radius: 0;
171
+ border-end-start-radius: 0;
172
+ inset-inline-start: 0;
173
+ }
174
+
175
+ :host([_mobile][_mobile-tray-location="inline-end"][opened]) .content-width {
176
+ inset-inline-end: 0;
177
+ }
178
+
179
+ :host([_mobile][_mobile-tray-location="inline-start"][opened]) .content-width {
180
+ inset-inline-start: 0;
181
+ }
182
+
183
+ :host([_mobile][_mobile-tray-location="block-end"][opened]) .content-width {
184
+ inset-block-end: 0;
185
+ }
186
+
187
+ :host([_mobile][_mobile-tray-location="inline-start"][opened]) .content-container,
188
+ :host([_mobile][_mobile-tray-location="inline-end"][opened]) .content-container {
189
+ height: 100vh;
190
+ }
191
+
192
+ :host([_mobile][_mobile-tray-location]) > .pointer {
193
+ display: none;
194
+ }
195
+
196
+ :host([_mobile][_mobile-tray-location][opened]) {
197
+ animation: none;
198
+ }
199
+
143
200
  :host([_offscreen]) {
144
201
  ${_offscreenStyleDeclarations}
145
202
  }
@@ -149,10 +206,12 @@ export const PopoverMixin = superclass => class extends superclass {
149
206
  constructor() {
150
207
  super();
151
208
  this.configure();
209
+ this._mobile = false;
152
210
  this._useNativePopover = isSupported ? 'manual' : undefined;
153
211
  this.#handleAncestorMutationBound = this.#handleAncestorMutation.bind(this);
154
212
  this.#handleAutoCloseClickBound = this.#handleAutoCloseClick.bind(this);
155
213
  this.#handleAutoCloseFocusBound = this.#handleAutoCloseFocus.bind(this);
214
+ this.#handleMobileResizeBound = this.#handleMobileResize.bind(this);
156
215
  this.#handleResizeBound = this.#handleResize.bind(this);
157
216
  this.#repositionBound = this.#reposition.bind(this);
158
217
  }
@@ -161,6 +220,7 @@ export const PopoverMixin = superclass => class extends superclass {
161
220
  super.connectedCallback();
162
221
  if (this._opened) {
163
222
  this.#addAutoCloseHandlers();
223
+ this.#addMediaQueryHandlers();
164
224
  this.#addRepositionHandlers();
165
225
  }
166
226
  }
@@ -168,6 +228,7 @@ export const PopoverMixin = superclass => class extends superclass {
168
228
  disconnectedCallback() {
169
229
  super.disconnectedCallback();
170
230
  this.#removeAutoCloseHandlers();
231
+ this.#removeMediaQueryHandlers();
171
232
  this.#removeRepositionHandlers();
172
233
  this.#clearDismissible();
173
234
  }
@@ -181,6 +242,7 @@ export const PopoverMixin = superclass => class extends superclass {
181
242
 
182
243
  this._previousFocusableAncestor = null;
183
244
  this.#removeAutoCloseHandlers();
245
+ this.#removeMediaQueryHandlers();
184
246
  this.#removeRepositionHandlers();
185
247
  this.#clearDismissible();
186
248
  await this.updateComplete; // wait before applying focus to opener
@@ -195,6 +257,8 @@ export const PopoverMixin = superclass => class extends superclass {
195
257
  this._maxWidth = properties?.maxWidth;
196
258
  this._minHeight = properties?.minHeight;
197
259
  this._minWidth = properties?.minWidth;
260
+ this._mobileBreakpoint = properties?.mobileBreakpoint ?? 616;
261
+ this._mobileTrayLocation = properties?.mobileTrayLocation;
198
262
  this._noAutoClose = properties?.noAutoClose ?? false;
199
263
  this._noAutoFit = properties?.noAutoFit ?? false;
200
264
  this._noAutoFocus = properties?.noAutoFocus ?? false;
@@ -217,6 +281,8 @@ export const PopoverMixin = superclass => class extends superclass {
217
281
  async open(applyFocus = true) {
218
282
  if (this._opened) return;
219
283
 
284
+ this.#addMediaQueryHandlers();
285
+
220
286
  this._rtl = document.documentElement.getAttribute('dir') === 'rtl';
221
287
  this._applyFocus = applyFocus !== undefined ? applyFocus : true;
222
288
  this._opened = true;
@@ -243,7 +309,16 @@ export const PopoverMixin = superclass => class extends superclass {
243
309
 
244
310
  renderPopover(content) {
245
311
 
246
- const stylesMap = this.#getStyleMaps();
312
+ const mobileTrayLocation = this._mobile ? this._mobileTrayLocation : null;
313
+
314
+ let stylesMap;
315
+ if (mobileTrayLocation === 'block-end') {
316
+ stylesMap = this.#getMobileTrayBlockStyleMaps();
317
+ } else if (mobileTrayLocation === 'inline-start' || mobileTrayLocation === 'inline-end') {
318
+ stylesMap = this.#getMobileTrayInlineStyleMaps();
319
+ } else {
320
+ stylesMap = this.#getStyleMaps();
321
+ }
247
322
  const widthStyle = stylesMap['width'];
248
323
  const contentStyle = stylesMap['content'];
249
324
 
@@ -301,9 +376,11 @@ export const PopoverMixin = superclass => class extends superclass {
301
376
  else return this.open(!this._noAutoFocus && applyFocus);
302
377
  }
303
378
 
379
+ #mediaQueryList;
304
380
  #handleAncestorMutationBound;
305
381
  #handleAutoCloseClickBound;
306
382
  #handleAutoCloseFocusBound;
383
+ #handleMobileResizeBound;
307
384
  #handleResizeBound;
308
385
  #repositionBound;
309
386
 
@@ -313,6 +390,12 @@ export const PopoverMixin = superclass => class extends superclass {
313
390
  document.addEventListener('click', this.#handleAutoCloseClickBound, { capture: true });
314
391
  }
315
392
 
393
+ #addMediaQueryHandlers() {
394
+ this.#mediaQueryList = window.matchMedia(`(max-width: ${this._mobileBreakpoint - 1}px)`);
395
+ this._mobile = this.#mediaQueryList.matches;
396
+ this.#mediaQueryList.addEventListener?.('change', this.#handleMobileResizeBound);
397
+ }
398
+
316
399
  #addRepositionHandlers() {
317
400
 
318
401
  const isScrollable = (node, prop) => {
@@ -434,6 +517,123 @@ export const PopoverMixin = superclass => class extends superclass {
434
517
  return 'block-end';
435
518
  }
436
519
 
520
+ #getMobileTrayBlockStyleMaps() {
521
+
522
+ let maxHeightOverride;
523
+ const availableHeight = Math.min(window.innerHeight, window.screen.height);
524
+
525
+ // default maximum height for bottom tray (42px margin)
526
+ const mobileTrayMaxHeightDefault = availableHeight - minBackdropHeightMobile;
527
+ if (this._maxHeight) {
528
+ // if maxHeight provided is smaller, use the maxHeight
529
+ maxHeightOverride = Math.min(mobileTrayMaxHeightDefault, this._maxHeight);
530
+ } else {
531
+ maxHeightOverride = mobileTrayMaxHeightDefault;
532
+ }
533
+ maxHeightOverride = `${maxHeightOverride}px`;
534
+
535
+ const widthOverride = '100vw';
536
+
537
+ const widthStyle = {
538
+ minWidth: widthOverride,
539
+ width: widthOverride,
540
+ maxHeight: maxHeightOverride,
541
+ };
542
+
543
+ const contentWidthStyle = {
544
+ // set width of content in addition to width container so header and footer borders are full width
545
+ width: widthOverride
546
+ };
547
+
548
+ const contentStyle = {
549
+ ...contentWidthStyle,
550
+ maxHeight: maxHeightOverride,
551
+ };
552
+
553
+ return {
554
+ width: widthStyle,
555
+ content: contentStyle,
556
+ };
557
+ }
558
+
559
+ #getMobileTrayInlineStyleMaps() {
560
+
561
+ let maxWidthOverride = this._maxWidth;
562
+ const availableWidth = Math.min(window.innerWidth, window.screen.width);
563
+
564
+ // default maximum width for tray (30px margin)
565
+ const mobileTrayMaxWidthDefault = Math.min(availableWidth - minBackdropWidthMobile, 420);
566
+ if (maxWidthOverride) {
567
+ // if maxWidth provided is smaller, use the maxWidth
568
+ maxWidthOverride = Math.min(mobileTrayMaxWidthDefault, maxWidthOverride);
569
+ } else {
570
+ maxWidthOverride = mobileTrayMaxWidthDefault;
571
+ }
572
+
573
+ let minWidthOverride = this.minWidth;
574
+ // minimum size - 285px
575
+ const mobileTrayMinWidthDefault = 285;
576
+ if (minWidthOverride) {
577
+ // if minWidth provided is smaller, use the minumum width for tray
578
+ minWidthOverride = Math.max(mobileTrayMinWidthDefault, minWidthOverride);
579
+ } else {
580
+ minWidthOverride = mobileTrayMinWidthDefault;
581
+ }
582
+
583
+ // if no width property set, automatically size to maximum width
584
+ let widthOverride = this._width ? this._width : maxWidthOverride;
585
+ // ensure width is between minWidth and maxWidth
586
+ if (widthOverride && maxWidthOverride && widthOverride > (maxWidthOverride - 20)) widthOverride = maxWidthOverride - 20;
587
+ if (widthOverride && minWidthOverride && widthOverride < (minWidthOverride - 20)) widthOverride = minWidthOverride - 20;
588
+ maxWidthOverride = `${maxWidthOverride}px`;
589
+ minWidthOverride = `${minWidthOverride}px`;
590
+
591
+ const contentWidth = `${widthOverride + 18}px`;
592
+ // add 2 to content width since scrollWidth does not include border
593
+ const containerWidth = `${widthOverride + 20}px`;
594
+
595
+ const topOverride = (window.innerHeight > window.screen.height) ? window.pageYOffset : undefined;
596
+
597
+ let inlineEndOverride;
598
+ let inlineStartOverride;
599
+ if (this._mobileTrayLocation === 'inline-end') {
600
+ // On non-responsive pages, the innerWidth may be wider than the screen,
601
+ // override right to stick to right of viewport
602
+ inlineEndOverride = `${Math.max(window.innerWidth - window.screen.width, 0)}px`;
603
+ } else if (this._mobileTrayLocation === 'inline-start') {
604
+ // On non-responsive pages, the innerWidth may be wider than the screen,
605
+ // override left to stick to left of viewport
606
+ inlineStartOverride = `${Math.max(window.innerWidth - window.screen.width, 0)}px`;
607
+ }
608
+
609
+ if (minWidthOverride > maxWidthOverride) {
610
+ minWidthOverride = maxWidthOverride;
611
+ }
612
+ const widthStyle = {
613
+ maxWidth: maxWidthOverride,
614
+ minWidth: minWidthOverride,
615
+ width: containerWidth,
616
+ top: topOverride,
617
+ insetInlineStart: inlineStartOverride,
618
+ insetInlineEnd: inlineEndOverride
619
+ };
620
+
621
+ const contentWidthStyle = {
622
+ minWidth: minWidthOverride,
623
+ // set width of content in addition to width container so header and footer borders are full width
624
+ width: contentWidth,
625
+ };
626
+
627
+ const contentStyle = {
628
+ ...contentWidthStyle,
629
+ };
630
+
631
+ return {
632
+ width : widthStyle,
633
+ content : contentStyle,
634
+ };
635
+ }
636
+
437
637
  #getPointer() {
438
638
  return this.shadowRoot.querySelector('.pointer');
439
639
  }
@@ -567,8 +767,8 @@ export const PopoverMixin = superclass => class extends superclass {
567
767
  };
568
768
 
569
769
  return {
570
- 'width' : widthStyle,
571
- 'content' : contentStyle
770
+ width : widthStyle,
771
+ content : contentStyle
572
772
  };
573
773
  }
574
774
 
@@ -622,6 +822,11 @@ export const PopoverMixin = superclass => class extends superclass {
622
822
  this.dispatchEvent(new CustomEvent('d2l-popover-focus-enter', { detail: { applyFocus: this._applyFocus } }));
623
823
  }
624
824
 
825
+ async #handleMobileResize() {
826
+ this._mobile = this.#mediaQueryList.matches;
827
+ if (this._opened) await this.#position();
828
+ }
829
+
625
830
  #handleResize() {
626
831
  this.resize();
627
832
  }
@@ -720,6 +925,10 @@ export const PopoverMixin = superclass => class extends superclass {
720
925
  document.removeEventListener('click', this.#handleAutoCloseClickBound, { capture: true });
721
926
  }
722
927
 
928
+ #removeMediaQueryHandlers() {
929
+ this.#mediaQueryList?.removeEventListener?.('change', this.#handleMobileResizeBound);
930
+ }
931
+
723
932
  #removeRepositionHandlers() {
724
933
  this._openerIntersectionObserver?.unobserve(this._opener);
725
934
  this._scrollablesObserved?.forEach(node => {
@@ -10619,6 +10619,11 @@
10619
10619
  "description": "Min-width (undefined). Specify a number that would be the px value.",
10620
10620
  "type": "number"
10621
10621
  },
10622
+ {
10623
+ "name": "mobile-tray-location",
10624
+ "description": "Mobile tray location.",
10625
+ "type": "'inline-start'|'inline-end'|'block-end'"
10626
+ },
10622
10627
  {
10623
10628
  "name": "position-location",
10624
10629
  "description": "Position the popover before or after the opener. Default is \"block-end\" (after).",
@@ -10685,6 +10690,12 @@
10685
10690
  "description": "Min-width (undefined). Specify a number that would be the px value.",
10686
10691
  "type": "number"
10687
10692
  },
10693
+ {
10694
+ "name": "mobileTrayLocation",
10695
+ "attribute": "mobile-tray-location",
10696
+ "description": "Mobile tray location.",
10697
+ "type": "'inline-start'|'inline-end'|'block-end'"
10698
+ },
10688
10699
  {
10689
10700
  "name": "positionLocation",
10690
10701
  "attribute": "position-location",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "3.77.0",
3
+ "version": "3.78.0",
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",