Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 75 additions & 44 deletions components/backdrop/backdrop-loading.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import '../colors/colors.js';
import '../loading-spinner/loading-spinner.js';
import { css, html, LitElement } from 'lit';
import { css, html, LitElement, nothing } from 'lit';
import { getOffsetParent } from '../../helpers/dom.js';
import { styleMap } from 'lit/directives/style-map.js';

const BACKDROP_DELAY_MS = 800;
const FADE_IN_DURATION_MS = 500;
const FADE_OUT_DURATION_MS = 500;
const SPINNER_DELAY_MS = BACKDROP_DELAY_MS + FADE_IN_DURATION_MS;
const FADE_DURATION_MS = 500;
const SPINNER_DELAY_MS = FADE_DURATION_MS;

const LOADING_SPINNER_MINIMUM_BUFFER = 100;

const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;

Expand All @@ -23,57 +25,52 @@ class LoadingBackdrop extends LitElement {
*/
shown: { type: Boolean },
_state: { type: String, reflect: true },
_spinnerTop: { type: Number, reflect: true }
};
}

static get styles() {
return css`
:host, .backdrop, d2l-loading-spinner {
height: 0%;
position: absolute;
width: 0%;
}

.backdrop, d2l-loading-spinner {
opacity: 0;
}

:host {
display: none;
height: 100%;
justify-content: center;
position: absolute;
top: 0;
width: 100%;
z-index: 999;
}
:host([_state="showing"]),
:host([_state="shown"]),
:host([_state="hiding"]) {
display: flex;
}

.backdrop {
background-color: var(--d2l-color-regolith);
}

d2l-loading-spinner {
top: 100px;
}

:host([_state="showing"]),
:host([_state="hiding"]),
d2l-loading-spinner[_state="showing"],
d2l-loading-spinner[_state="hiding"],
.backdrop[_state="showing"],
.backdrop[_state="hiding"] {
height: 100%;
opacity: 0;
position: absolute;
top: 0;
transition: opacity ${FADE_DURATION_MS}ms ease-in;
width: 100%;
}

d2l-loading-spinner[_state="showing"] {
opacity: 1;
transition: opacity ${FADE_IN_DURATION_MS}ms ease-in ${SPINNER_DELAY_MS}ms;
:host([_state="shown"]) .backdrop {
opacity: 0.7;
}

.backdrop[_state="showing"] {
opacity: 0.7;
transition: opacity ${FADE_IN_DURATION_MS}ms ease-in ${BACKDROP_DELAY_MS}ms;
d2l-loading-spinner {
opacity: 0;
position: absolute;
transition: opacity ${FADE_DURATION_MS}ms ease-in ${SPINNER_DELAY_MS}ms;
}
:host([_state="shown"]) d2l-loading-spinner {
opacity: 1;
}

d2l-loading-spinner[_state="hiding"],
.backdrop[_state="hiding"] {
transition: opacity ${FADE_OUT_DURATION_MS}ms ease-out;
:host([_state="hiding"]) .d2l-backdrop,
:host([_state="hiding"]) d2l-loading-spinner {
transition: opacity ${FADE_DURATION_MS}ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
Expand All @@ -85,16 +82,29 @@ class LoadingBackdrop extends LitElement {
constructor() {
super();
this.shown = false;
this._state = null;
this._state = 'hidden';
this._spinnerTop = LOADING_SPINNER_MINIMUM_BUFFER;
}

render() {
if (this._state === 'hidden') return nothing;
return html`
<div class="backdrop" _state=${this._state} @transitionend=${this.#handleTransitionEnd} @transitioncancel=${this.#hide}></div>
<d2l-loading-spinner _state=${this._state} size="${this._state === null ? '0' : '50'}"></d2l-loading-spinner>
<div class="backdrop" @transitionend="${this.#handleTransitionEnd}" @transitioncancel="${this.#hide}"></div>
<d2l-loading-spinner style=${styleMap({ top: `${this._spinnerTop}px` })}></d2l-loading-spinner>
`;
}
updated(changedProperties) {
if (changedProperties.has('_state')) {
if (this._state === 'showing') {
setTimeout(() => this._state = 'shown', BACKDROP_DELAY_MS);
}
}

if (this.#mustRepositionSpinner) {
this.#centerLoadingSpinner();
this.#mustRepositionSpinner = false;
}
}
Comment on lines +103 to +107
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using a private property here to indicate that the spinner should be repositioned after the next render- the flow looks like this:

  1. The user marks the element as 'shown'
  2. We call show(), which sets the state properties to start the fade-in, and records that we'll need to re-position after this render
  3. We render the page as a result of the properties changing (must occur first to get page dimensions before we can center)
  4. When we move onto updated we see that the spinner needs to be re-positioned, so we do so and remove the mark
  5. Future renderings, such as those that result from transitions starting/completing, don't trigger further repositioning of the spinner, even if the viewport has since moved. This is by design.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could do without it and check for changedProperties having _state being set to either shown (if reduced motion) or showing (if not reduced motion)?

willUpdate(changedProperties) {
if (changedProperties.has('shown')) {
if (this.shown) {
Expand All @@ -104,6 +114,28 @@ class LoadingBackdrop extends LitElement {
}
}
}
#mustRepositionSpinner;

#centerLoadingSpinner() {
if (this._state === 'hidden') { return; }

const loadingSpinner = this.shadowRoot.querySelector('d2l-loading-spinner');
if (!loadingSpinner) { return; }

const boundingRect = this.getBoundingClientRect();

// Calculate the centerpoint of the visible portion of the element
const upperVisibleBound = Math.max(0, boundingRect.top);
const lowerVisibleBound = Math.min(window.innerHeight, boundingRect.bottom);
const visibleHeight = lowerVisibleBound - upperVisibleBound;
const centeringOffset = visibleHeight / 2;

// Calculate if an offset is required to move to the top of the viewport before centering
const topOffset = Math.max(0, -boundingRect.top); // measures the distance below the top of the viewport, which is negative if the element starts above the viewport
const newPosition = centeringOffset + topOffset;

this._spinnerTop = Math.max(LOADING_SPINNER_MINIMUM_BUFFER, newPosition);
}

#fade() {
if (reduceMotion) {
Expand All @@ -112,23 +144,22 @@ class LoadingBackdrop extends LitElement {
this._state = 'hiding';
}
}

#handleTransitionEnd() {
if (this._state === 'hiding') {
this.#hide();
}
}

#hide() {
this._state = null;
this._state = 'hidden';

const containingBlock = getOffsetParent(this);

if (containingBlock.dataset.initiallyInert !== '1') containingBlock.removeAttribute('inert');
}

#show() {
this._state = 'showing';
this.#mustRepositionSpinner = true;

this._state = reduceMotion ? 'shown' : 'showing';

const containingBlock = getOffsetParent(this);

Expand Down
100 changes: 100 additions & 0 deletions components/backdrop/demo/backdrop-loading.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,106 @@
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
Comment on lines +41 to +49
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the demo long enough to easily test this property

</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Art</td>
<td class="grade">98%</td>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading