Skip to content

Commit

Permalink
Initial popover support for tray positioning.
Browse files Browse the repository at this point in the history
  • Loading branch information
dbatiste committed Dec 20, 2024
1 parent 31fe8e6 commit 5e19256
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 1 deletion.
16 changes: 16 additions & 0 deletions components/popover/demo/popover.html
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,22 @@ <h2>Popover (trap-focus)</h2>
</template>
</d2l-demo-snippet>

<h2>Popover (mobile tray)</h2>
<d2l-demo-snippet>
<template>
<d2l-button-subtle text="Open"></d2l-button-subtle>
<d2l-test-popover mobile-tray-location="inline-start" style="max-width: 400px;">
<d2l-link href="https://pirateipsum.me/" target="_blank">Pirate Ipsum</d2l-link>
<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>
<d2l-button-subtle text="Close"></d2l-button-subtle>
</d2l-test-popover>
<script>
window.wireUpPopover(document.currentScript.parentNode);
</script>
</template>
</template>
</d2l-demo-snippet>

</d2l-demo-page>

<script>
Expand Down
211 changes: 210 additions & 1 deletion components/popover/popover-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const defaultPreferredPosition = {
span: 'all', // start, end, all
allowFlip: true
};
const minBackdropHeightMobile = 42;
const minBackdropWidthMobile = 30;
const pointerLength = 16;
const pointerRotatedLength = Math.SQRT2 * parseFloat(pointerLength);
const isSupported = ('popover' in HTMLElement.prototype);
Expand All @@ -30,6 +32,9 @@ export const PopoverMixin = superclass => class extends superclass {
_maxWidth: { state: true },
_minHeight: { state: true },
_minWidth: { state: true },
_mobile: { type: Boolean, reflect: true, attribute: '_mobile' },
_mobileBreakpoint: { state: true },
_mobileTrayLocation: { type: String, reflect: true, attribute: '_mobile-tray-location' },
_noAutoClose: { state: true },
_noAutoFit: { state: true },
_noAutoFocus: { state: true },
Expand Down Expand Up @@ -140,6 +145,58 @@ export const PopoverMixin = superclass => class extends superclass {
}
}
:host([_mobile][_mobile-tray-location]) .content-width {
position: fixed;
z-index: 1000;
}
:host([_mobile][_mobile-tray-location="inline-start"]) .content-width,
:host([_mobile][_mobile-tray-location="inline-end"]) .content-width {
bottom: 0;
top: 0;
}
:host([_mobile][_mobile-tray-location="inline-start"]) .content-width {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
:host([_mobile][_mobile-tray-location="inline-end"]) .content-width {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
:host([_mobile][_mobile-tray-location="block-end"]) .content-width {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
left: 0;
}
:host([_mobile][_mobile-tray-location="inline-end"][opened]) .content-width {
right: 0;
}
:host([_mobile][_mobile-tray-location="inline-start"][opened]) .content-width {
left: 0;
}
:host([_mobile][_mobile-tray-location="block-end"][opened]) .content-width {
bottom: 0;
}
:host([_mobile][_mobile-tray-location="inline-start"][opened]) .content-container,
:host([_mobile][_mobile-tray-location="inline-end"][opened]) .content-container {
height: 100vh;
}
:host([_mobile][_mobile-tray-location]) > .pointer {
display: none;
}
:host([_mobile][_mobile-tray-location][opened]) {
animation: none;
}
:host([_offscreen]) {
${_offscreenStyleDeclarations}
}
Expand All @@ -149,10 +206,12 @@ export const PopoverMixin = superclass => class extends superclass {
constructor() {
super();
this.configure();
this._mobile = false;
this._useNativePopover = isSupported ? 'manual' : undefined;
this.#handleAncestorMutationBound = this.#handleAncestorMutation.bind(this);
this.#handleAutoCloseClickBound = this.#handleAutoCloseClick.bind(this);
this.#handleAutoCloseFocusBound = this.#handleAutoCloseFocus.bind(this);
this.#handleMobileResizeBound = this.#handleMobileResize.bind(this);
this.#handleResizeBound = this.#handleResize.bind(this);
this.#repositionBound = this.#reposition.bind(this);
}
Expand All @@ -161,13 +220,15 @@ export const PopoverMixin = superclass => class extends superclass {
super.connectedCallback();
if (this._opened) {
this.#addAutoCloseHandlers();
this.#addMediaQueryHandlers();
this.#addRepositionHandlers();
}
}

disconnectedCallback() {
super.disconnectedCallback();
this.#removeAutoCloseHandlers();
this.#removeMediaQueryHandlers();
this.#removeRepositionHandlers();
this.#clearDismissible();
}
Expand All @@ -181,6 +242,7 @@ export const PopoverMixin = superclass => class extends superclass {

this._previousFocusableAncestor = null;
this.#removeAutoCloseHandlers();
this.#removeMediaQueryHandlers();
this.#removeRepositionHandlers();
this.#clearDismissible();
await this.updateComplete; // wait before applying focus to opener
Expand All @@ -195,6 +257,8 @@ export const PopoverMixin = superclass => class extends superclass {
this._maxWidth = properties?.maxWidth;
this._minHeight = properties?.minHeight;
this._minWidth = properties?.minWidth;
this._mobileBreakpoint = properties?.mobileBreakpoint ?? 616;
this._mobileTrayLocation = properties?.mobileTrayLocation;
this._noAutoClose = properties?.noAutoClose ?? false;
this._noAutoFit = properties?.noAutoFit ?? false;
this._noAutoFocus = properties?.noAutoFocus ?? false;
Expand All @@ -217,6 +281,8 @@ export const PopoverMixin = superclass => class extends superclass {
async open(applyFocus = true) {
if (this._opened) return;

this.#addMediaQueryHandlers();

this._rtl = document.documentElement.getAttribute('dir') === 'rtl';
this._applyFocus = applyFocus !== undefined ? applyFocus : true;
this._opened = true;
Expand All @@ -243,7 +309,16 @@ export const PopoverMixin = superclass => class extends superclass {

renderPopover(content) {

const stylesMap = this.#getStyleMaps();
const mobileTrayLocation = this._mobile ? this._mobileTrayLocation : null;

let stylesMap;
if (mobileTrayLocation === 'block-end') {
stylesMap = this.#getMobileTrayBlockStyleMaps();
} else if (mobileTrayLocation === 'inline-start' || mobileTrayLocation === 'inline-end') {
stylesMap = this.#getMobileTrayInlineStyleMaps();
} else {
stylesMap = this.#getStyleMaps();
}
const widthStyle = stylesMap['width'];
const contentStyle = stylesMap['content'];

Expand Down Expand Up @@ -301,9 +376,11 @@ export const PopoverMixin = superclass => class extends superclass {
else return this.open(!this._noAutoFocus && applyFocus);
}

#mediaQueryList;
#handleAncestorMutationBound;
#handleAutoCloseClickBound;
#handleAutoCloseFocusBound;
#handleMobileResizeBound;
#handleResizeBound;
#repositionBound;

Expand All @@ -313,6 +390,12 @@ export const PopoverMixin = superclass => class extends superclass {
document.addEventListener('click', this.#handleAutoCloseClickBound, { capture: true });
}

#addMediaQueryHandlers() {
this.#mediaQueryList = window.matchMedia(`(max-width: ${this._mobileBreakpoint - 1}px)`);
this._mobile = this.#mediaQueryList.matches;
this.#mediaQueryList.addEventListener?.('change', this.#handleMobileResizeBound);
}

#addRepositionHandlers() {

const isScrollable = (node, prop) => {
Expand Down Expand Up @@ -434,6 +517,123 @@ export const PopoverMixin = superclass => class extends superclass {
return 'block-end';
}

#getMobileTrayBlockStyleMaps() {

let maxHeightOverride;
const availableHeight = Math.min(window.innerHeight, window.screen.height);

// default maximum height for bottom tray (42px margin)
const mobileTrayMaxHeightDefault = availableHeight - minBackdropHeightMobile;
if (this._maxHeight) {
// if maxHeight provided is smaller, use the maxHeight
maxHeightOverride = Math.min(mobileTrayMaxHeightDefault, this._maxHeight);
} else {
maxHeightOverride = mobileTrayMaxHeightDefault;
}
maxHeightOverride = `${maxHeightOverride}px`;

const widthOverride = '100vw';

const widthStyle = {
minWidth: widthOverride,
width: widthOverride,
maxHeight: maxHeightOverride,
};

const contentWidthStyle = {
// set width of content in addition to width container so header and footer borders are full width
width: widthOverride
};

const contentStyle = {
...contentWidthStyle,
maxHeight: maxHeightOverride,
};

return {
'width' : widthStyle,
'content' : contentStyle,
};
}

#getMobileTrayInlineStyleMaps() {

let maxWidthOverride = this._maxWidth;
const availableWidth = Math.min(window.innerWidth, window.screen.width);

// default maximum width for tray (30px margin)
const mobileTrayMaxWidthDefault = Math.min(availableWidth - minBackdropWidthMobile, 420);
if (maxWidthOverride) {
// if maxWidth provided is smaller, use the maxWidth
maxWidthOverride = Math.min(mobileTrayMaxWidthDefault, maxWidthOverride);
} else {
maxWidthOverride = mobileTrayMaxWidthDefault;
}

let minWidthOverride = this.minWidth;
// minimum size - 285px
const mobileTrayMinWidthDefault = 285;
if (minWidthOverride) {
// if minWidth provided is smaller, use the minumum width for tray
minWidthOverride = Math.max(mobileTrayMinWidthDefault, minWidthOverride);
} else {
minWidthOverride = mobileTrayMinWidthDefault;
}

// if no width property set, automatically size to maximum width
let widthOverride = this._width ? this._width : maxWidthOverride;
// ensure width is between minWidth and maxWidth
if (widthOverride && maxWidthOverride && widthOverride > (maxWidthOverride - 20)) widthOverride = maxWidthOverride - 20;
if (widthOverride && minWidthOverride && widthOverride < (minWidthOverride - 20)) widthOverride = minWidthOverride - 20;
maxWidthOverride = `${maxWidthOverride}px`;
minWidthOverride = `${minWidthOverride}px`;

const contentWidth = `${widthOverride + 18}px`;
// add 2 to content width since scrollWidth does not include border
const containerWidth = `${widthOverride + 20}px`;

const topOverride = (window.innerHeight > window.screen.height) ? window.pageYOffset : undefined;

let rightOverride;
let leftOverride;
if (this._mobileTrayLocation === 'inline-end') {
// On non-responsive pages, the innerWidth may be wider than the screen,
// override right to stick to right of viewport
rightOverride = `${Math.max(window.innerWidth - window.screen.width, 0)}px`;
} else if (this._mobileTrayLocation === 'inline-start') {
// On non-responsive pages, the innerWidth may be wider than the screen,
// override left to stick to left of viewport
leftOverride = `${Math.max(window.innerWidth - window.screen.width, 0)}px`;
}

if (minWidthOverride > maxWidthOverride) {
minWidthOverride = maxWidthOverride;
}
const widthStyle = {
maxWidth: maxWidthOverride,
minWidth: minWidthOverride,
width: containerWidth,
top: topOverride,
right: rightOverride,
left: leftOverride,
};

const contentWidthStyle = {
minWidth: minWidthOverride,
// set width of content in addition to width container so header and footer borders are full width
width: contentWidth,
};

const contentStyle = {
...contentWidthStyle,
};

return {
'width' : widthStyle,
'content' : contentStyle,
};
}

#getPointer() {
return this.shadowRoot.querySelector('.pointer');
}
Expand Down Expand Up @@ -622,6 +822,11 @@ export const PopoverMixin = superclass => class extends superclass {
this.dispatchEvent(new CustomEvent('d2l-popover-focus-enter', { detail: { applyFocus: this._applyFocus } }));
}

async #handleMobileResize() {
this._mobile = this.#mediaQueryList.matches;
if (this._opened) await this.#position();
}

#handleResize() {
this.resize();
}
Expand Down Expand Up @@ -720,6 +925,10 @@ export const PopoverMixin = superclass => class extends superclass {
document.removeEventListener('click', this.#handleAutoCloseClickBound, { capture: true });
}

#removeMediaQueryHandlers() {
this.#mediaQueryList.removeEventListener?.('change', this.#handleMobileResizeBound);
}

#removeRepositionHandlers() {
this._openerIntersectionObserver?.unobserve(this._opener);
this._scrollablesObserved?.forEach(node => {
Expand Down
6 changes: 6 additions & 0 deletions components/popover/test/popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ class Popover extends PopoverMixin(LitElement) {
* @type {number}
*/
minWidth: { type: Number, reflect: true, attribute: 'min-width' },
/**
* Mobile tray location.
* @type {'inline-start'|'inline-end'|'block-end'}
*/
mobileTrayLocation: { type: String, reflect: true, attribute: 'mobile-tray-location' },
/**
* Whether to disable auto-close/light-dismiss
* @type {boolean}
Expand Down Expand Up @@ -148,6 +153,7 @@ class Popover extends PopoverMixin(LitElement) {
noAutoClose: this.noAutoClose,
noAutoFocus: this.noAutoFocus,
noPointer: this.noPointer,
mobileTrayLocation: this.mobileTrayLocation,
position: { location: this.positionLocation, span: this.positionSpan },
trapFocus: this.trapFocus
});
Expand Down

0 comments on commit 5e19256

Please sign in to comment.