diff --git a/.vdiff.json b/.vdiff.json
index 4adefefd9d1..739d6b4f9d9 100644
--- a/.vdiff.json
+++ b/.vdiff.json
@@ -7,7 +7,7 @@
],
"system": {
"platform": "linux",
- "release": "6.17.0-1018-azure",
+ "release": "6.17.0-1020-azure",
"arch": "x64"
}
}
diff --git a/components/backdrop/backdrop-dirty-overlay.js b/components/backdrop/backdrop-dirty-overlay.js
deleted file mode 100644
index 9cc8ca4cd47..00000000000
--- a/components/backdrop/backdrop-dirty-overlay.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import '../button/button-subtle.js';
-import { css, html, LitElement } from 'lit';
-import { bodyCompactStyles } from '../typography/styles.js';
-import { getFocusRingStyles } from '../../helpers/focus.js';
-
-const overlayStyles = css`
- .action-slot::slotted(*) {
- display: inline;
- }
- :host {
- align-items: center;
- border: 1px solid var(--d2l-color-mica);
- border-radius: 0.3rem;
- column-gap: 0.75rem;
- display: block;
- display: flex;
- flex-wrap: wrap;
- padding: 1.2rem 1.5rem;
- }
-
- .d2l-backdrop-dirty-overlay {
- --d2l-focus-ring-offset: 3px;
- border-radius: 0.3rem;
- margin: 0;
- }
-
- .d2l-backdrop-dirty-overlay-action {
- vertical-align: top;
- }
-`;
-
-/**
- * The `d2l-backdrop-dirty-overlay` component is used to render a dialog with text and an action over another element that needs to be refreshed after user input.
- */
-class BackdropDirtyOverlay extends LitElement {
-
- static properties = {
- /**
- * The text displayed on the dirty state overlay.
- * @type {string}
- */
- description: { type: String, required: true },
-
- /**
- * The text displayed on the button of the dirty state overlay when the 'dirty' dataState is set.
- * @type {string}
- */
- action: { type: String, required: true }
- };
-
- static styles = [bodyCompactStyles, overlayStyles, getFocusRingStyles('.d2l-backdrop-dirty-overlay')];
-
- render() {
- return html`
-
- ${backdropVisible ?
- html`
` : nothing }
-
- ${this._ariaContent}
-
+ ${backdropContainer}
+
+ ${backdropStaleOverlay}
+ ${this._ariaContent}
`;
}
+
updated(changedProperties) {
- if (changedProperties.get('_state') && changedProperties.get('_state') === 'hidden')
- {
- this.#centerLoadingSpinnerAndDialog();
+ super.updated(changedProperties);
+
+ if (changedProperties.get('_state') && changedProperties.get('_state') === 'hidden') {
+ this.#updatePosition();
}
if (changedProperties.has('_state')) {
if (this._state === 'showing') {
- if (this.dataState === 'loading') {
+ if (this.freshness === freshness.loading) {
setTimeout(() => {
if (this._state === 'showing') this._state = 'shown';
}, BACKDROP_DELAY_MS);
@@ -180,26 +154,27 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme
}
}
}
+
willUpdate(changedProperties) {
- if (changedProperties.has('dataState') && changedProperties.get('dataState') !== undefined) {
- this.#clearLiveArea();
+ super.willUpdate(changedProperties);
+
+ if (changedProperties.has('freshness') && changedProperties.get('freshness') !== undefined) {
+ this.#clearLiveRegionContent();
- const oldState = changedProperties.get('dataState');
- const newState = this.dataState;
+ const oldState = changedProperties.get('freshness');
+ const newState = this.freshness;
- // Calculate announcements
- if (newState === 'loading') {
- this.#setLiveArea(this.localize('components.backdrop-loading.loadingAnnouncement'), { delay: LOADING_ANNOUNCEMENT_DELAY });
- } else if (oldState === 'loading' && newState === 'clean') {
- this.#setLiveArea(this.localize('components.backdrop-loading.loadingCompleteAnnouncement'));
+ if (newState === freshness.loading) {
+ this.#setLiveRegionContent(this.localize('components.backdrop-loading.loadingAnnouncement'), { delay: LOADING_ANNOUNCEMENT_DELAY });
+ } else if (oldState === freshness.loading && newState === freshness.fresh) {
+ this.#setLiveRegionContent(this.localize('components.backdrop-loading.loadingCompleteAnnouncement'));
}
- // Update backdrop
- if (oldState === 'clean') {
+ if (oldState === freshness.fresh) {
this.#show();
- } else if (newState === 'clean') {
+ } else if (newState === freshness.fresh) {
this.#fade();
- } else if (oldState === 'loading' && newState === 'dirty') {
+ } else if (oldState === freshness.loading && newState === freshness.stale) {
setTimeout(() => {
if (this._state === 'showing') this._state = 'shown';
}, BACKDROP_DELAY_MS);
@@ -207,52 +182,19 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme
}
}
- async #centerLoadingSpinnerAndDialog() {
- if (this._state === 'hidden') { return; }
-
- const loadingSpinner = this.shadowRoot.querySelector('d2l-loading-spinner');
- if (!loadingSpinner) { return; }
-
- const boundingRect = this.shadowRoot.querySelector('#visible').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 / 4);
-
- // 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
-
- // Adjust for the size of the spinner
- const spinnerSizeOffset = LOADING_SPINNER_SIZE / 2;
-
- this._spinnerTop = centeringOffset + topOffset - spinnerSizeOffset;
+ #announcementTimeoutId;
- // Adjust for the size of the dirty dialog
- const dirtyOverlay = this.shadowRoot.querySelector('d2l-backdrop-dirty-overlay');
- if (dirtyOverlay) {
- await this.shadowRoot.querySelector('d2l-empty-state-action-button')?.getUpdateComplete();
- const dirtyDialogSizeOffset = dirtyOverlay.getBoundingClientRect().height / 2;
-
- this._dirtyDialogTop = centeringOffset + topOffset - dirtyDialogSizeOffset;
- }
- }
-
- #clearLiveArea() {
+ #clearLiveRegionContent() {
this._ariaContent = '';
- if (this.announcementTimeout) {
- clearTimeout(this.announcementTimeout);
- }
-
- this.announcementTimeout = null;
+ if (this.#announcementTimeoutId) clearTimeout(this.#announcementTimeoutId);
+ this.#announcementTimeoutId = null;
}
#fade() {
let hideImmediately = reduceMotion || this._state === 'showing';
if (this._state === 'shown') {
- const currentOpacity = getComputedStyle(this.shadowRoot.querySelector('#d2l-live-region')).opacity;
+ const currentOpacity = getComputedStyle(this.shadowRoot.querySelector('.backdrop-stale-container')).opacity;
hideImmediately ||= (currentOpacity === '0');
}
@@ -266,14 +208,10 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme
#getBackdropTarget() {
const parent = getComposedParent(this);
- const targetedChildren = getComposedChildren(
- parent,
- (elem) => elem.id === this.for,
- false
- );
-
- if (targetedChildren.length === 0) { throw new Error(`Backdrop cannot find sibling identified by 'for' property with value ${this.for}`);}
-
+ const targetedChildren = getComposedChildren(parent, (elem) => elem.id === this.for, false);
+ if (targetedChildren.length === 0) {
+ throw new Error(`Backdrop cannot find sibling identified by 'for' property with value ${this.for}`);
+ }
return targetedChildren[0];
}
@@ -287,24 +225,53 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme
this._state = 'hidden';
const containingBlock = this.#getBackdropTarget();
-
if (containingBlock.dataset.initiallyInert !== '1') containingBlock.removeAttribute('inert');
}
- #setLiveArea(content, { delay } = {}) {
- this.announcementTimeout = setTimeout(() => this._ariaContent = content, delay || 0);
+ #setLiveRegionContent(content, { delay } = {}) {
+ this.#announcementTimeoutId = setTimeout(() => this._ariaContent = content, delay || 0);
}
#show() {
this._state = reduceMotion ? 'shown' : 'showing';
const containingBlock = this.#getBackdropTarget();
-
if (containingBlock.getAttribute('inert') !== null) containingBlock.dataset.initiallyInert = '1';
-
containingBlock.setAttribute('inert', 'inert');
}
+ async #updatePosition() {
+ if (this._state === 'hidden') { return; }
+
+ const loadingSpinner = this.shadowRoot.querySelector('d2l-loading-spinner');
+ if (!loadingSpinner) { return; }
+
+ const backdropContainerRect = this.shadowRoot.querySelector('.backdrop-container').getBoundingClientRect();
+
+ // Calculate the centerpoint of the visible portion of the element
+ const upperVisibleBound = Math.max(0, backdropContainerRect.top);
+ const lowerVisibleBound = Math.min(window.innerHeight, backdropContainerRect.bottom);
+ const visibleHeight = lowerVisibleBound - upperVisibleBound;
+ const centeringOffset = (visibleHeight / 4);
+
+ // Calculate if an offset is required to move to the top of the viewport before centering
+ const topOffset = Math.max(0, -backdropContainerRect.top); // measures the distance below the top of the viewport, which is negative if the element starts above the viewport
+
+ // Adjust for the size of the spinner
+ const spinnerSizeOffset = LOADING_SPINNER_SIZE / 2;
+
+ this._loadingSpinnerTop = centeringOffset + topOffset - spinnerSizeOffset;
+
+ // Adjust for the size of the stale overlay
+ const staleOverlay = this.shadowRoot.querySelector('d2l-backdrop-stale-overlay');
+ if (staleOverlay) {
+ await this.shadowRoot.querySelector('d2l-empty-state-action-button')?.getUpdateComplete();
+ const staleOverlaySizeOffset = staleOverlay.getBoundingClientRect().height / 2;
+
+ this._staleOverlayTop = centeringOffset + topOffset - staleOverlaySizeOffset;
+ }
+ }
+
}
-customElements.define('d2l-backdrop-loading', LoadingBackdrop);
+customElements.define('d2l-backdrop-loading', BackdropLoading);
diff --git a/components/backdrop/backdrop-stale-overlay.js b/components/backdrop/backdrop-stale-overlay.js
new file mode 100644
index 00000000000..3634fe1b427
--- /dev/null
+++ b/components/backdrop/backdrop-stale-overlay.js
@@ -0,0 +1,54 @@
+import '../button/button-subtle.js';
+import { css, html, LitElement } from 'lit';
+import { bodyCompactStyles } from '../typography/styles.js';
+
+/**
+ * A component to render as an overlay over another element with stale data.
+ */
+class BackdropStaleOverlay extends LitElement {
+
+ static properties = {
+ /**
+ * The action button text
+ * @type {string}
+ */
+ buttonText: { type: String, attribute: 'button-text', required: true },
+ /**
+ * The text displayed on the overlay
+ * @type {string}
+ */
+ text: { type: String, required: true }
+ };
+
+ static styles = [bodyCompactStyles, css`
+ :host {
+ align-items: center;
+ border: 1px solid var(--d2l-color-mica);
+ border-radius: 0.3rem;
+ column-gap: 0.75rem;
+ display: flex;
+ flex-wrap: wrap;
+ padding: 1.2rem 1.5rem;
+ }
+ `];
+
+ render() {
+ return html`
+
${this.text}
+
+
+ `;
+ }
+
+ #handleActionClick(e) {
+ e.stopPropagation();
+
+ /** Dispatched when the action button on the overlay is clicked */
+ this.dispatchEvent(new CustomEvent('d2l-backdrop-stale-overlay-action', { bubbles: true, composed: true }));
+ }
+
+}
+
+customElements.define('d2l-backdrop-stale-overlay', BackdropStaleOverlay);
diff --git a/components/backdrop/demo/backdrop-loading.html b/components/backdrop/demo/backdrop-loading.html
index ccabd50e2fa..464ade4b47a 100644
--- a/components/backdrop/demo/backdrop-loading.html
+++ b/components/backdrop/demo/backdrop-loading.html
@@ -8,6 +8,8 @@
import '../../demo/demo-page.js';
import '../../button/button.js';
import '../backdrop-loading.js';
+ import { freshness } from '../../../mixins/freshness/freshness-mixin.js';
+ window.backdropLoadingFreshness = freshness;