From f6411ad7e44d18e258527b68cd509917e05cd3f9 Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 11:40:07 -0400 Subject: [PATCH 01/24] Refactor the data state properties into a mixin for consistency/re-use. --- components/backdrop/backdrop-loading.js | 40 +++++------------------ components/table/table-wrapper.js | 42 ++----------------------- mixins/data-state/data-state-mixin.js | 41 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 72 deletions(-) create mode 100644 mixins/data-state/data-state-mixin.js diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index c783bfc18c5..baec9adab1b 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -3,9 +3,8 @@ import '../loading-spinner/loading-spinner.js'; import './backdrop-dirty-overlay.js'; import { css, html, LitElement, nothing } from 'lit'; import { getComposedChildren, getComposedParent } from '../../helpers/dom.js'; +import { DataStateMixin } from '../../mixins/data-state/data-state-mixin.js'; import { LocalizeCoreElement } from '../../helpers/localize-core-element.js'; - -import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js'; import { styleMap } from 'lit/directives/style-map.js'; const BACKDROP_DELAY_MS = 800; @@ -20,40 +19,14 @@ const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; /** * A component for displaying a semi-transparent backdrop and a loading spinner over the containing element */ -class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitElement)) { +class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { static properties = { - /** - * The state of data in the element being overlaid. Set to 'clean' when the data represents the user's latest selections, 'dirty' when the data does not represent the user's latest selections, and 'loading' if the data is being actively refreshed - * @type {'clean'|'dirty'|'loading'} - */ - dataState: { - reflect: true, - type: String - }, /** * Used to identify content that the backdrop should make inert * @type {string} */ for: { type: String, required: true }, - /** - * The text displayed on the dirty state overlay when the 'dirty' dataState is set. - * @type {string} - */ - dirtyText: { - reflect: true, - attribute: 'dirty-text', - type: String - }, - /** - * The text displayed on the button of the dirty state overlay when the 'dirty' dataState is set. - * @type {string} - */ - dirtyButtonText: { - reflect: true, - attribute: 'dirty-button-text', - type: String - }, _state: { type: String, reflect: true }, _spinnerTop: { state: true }, _ariaContent: { state: true } @@ -99,7 +72,7 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme opacity: 1; transition: opacity ${FADE_DURATION_MS}ms ease-in ${SPINNER_DELAY_MS}ms; } - :host([_state="shown"][dataState="dirty"]) d2l-loading-spinner, + :host([_state="shown"][data-state="dirty"]) d2l-loading-spinner, :host([_state="hiding"]) d2l-loading-spinner { opacity: 0; transition: opacity ${FADE_DURATION_MS}ms ease-out; @@ -118,7 +91,7 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme opacity: 1; transition: opacity ${FADE_DURATION_MS}ms ease-in; } - :host([_state="shown"][dataState="loading"]) d2l-backdrop-dirty-overlay, + :host([_state="shown"][data-state="loading"]) d2l-backdrop-dirty-overlay, :host([_state="hiding"]) d2l-backdrop-dirty-overlay { opacity: 0; transition: opacity ${FADE_DURATION_MS}ms ease-out; @@ -131,7 +104,6 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme constructor() { super(); - this.dataState = 'clean'; this._state = 'hidden'; this._spinnerTop = 0; this._dirtyDialogTop = 0; @@ -162,7 +134,9 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme `; } + updated(changedProperties) { + super.updated(changedProperties); if (changedProperties.get('_state') && changedProperties.get('_state') === 'hidden') { this.#centerLoadingSpinnerAndDialog(); @@ -180,7 +154,9 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme } } } + willUpdate(changedProperties) { + super.willUpdate(changedProperties); if (changedProperties.has('dataState') && changedProperties.get('dataState') !== undefined) { this.#clearLiveArea(); diff --git a/components/table/table-wrapper.js b/components/table/table-wrapper.js index a2ce884e2c2..3e842ae6940 100644 --- a/components/table/table-wrapper.js +++ b/components/table/table-wrapper.js @@ -3,11 +3,11 @@ import '../scroll-wrapper/scroll-wrapper.js'; import '../backdrop/backdrop-loading.js'; import { css, html, LitElement, nothing } from 'lit'; import { cssSizes } from '../inputs/input-checkbox-styles.js'; +import { DataStateMixin } from '../../mixins/data-state/data-state-mixin.js'; import { getComposedParent } from '../../helpers/dom.js'; import { getFlag } from '../../helpers/flags.js'; import { isPopoverSupported } from '../popover/popover-mixin.js'; import { PageableMixin } from '../paging/pageable-mixin.js'; -import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js'; import { SelectionMixin } from '../selection/selection-mixin.js'; const enableStickyScrollyFix = getFlag('table-sticky-scrolly-fix', true); @@ -266,7 +266,7 @@ const SELECTORS = { * @slot controls - Slot for `d2l-table-controls` to be rendered above the table * @slot pager - Slot for `d2l-pager-load-more` to be rendered below the table */ -export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionMixin(LitElement))) { +export class TableWrapper extends DataStateMixin(PageableMixin(SelectionMixin(LitElement))) { static properties = { /** @@ -310,40 +310,6 @@ export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionM attribute: '_no-scroll-width', reflect: true, type: Boolean, - }, - /** - * The state of data in the table. Set to 'clean' when the data represents the user's latest selections, 'dirty' when the data does not represent the user's latest selections, and 'loading' if the data is being actively refreshed - * @type {'clean'|'dirty'|'loading'} - */ - dataState: { - reflect: true, - type: String - }, - /** - * The text displayed on the dirty state overlay when the 'dirty' dataState is set. - * @type {string} - */ - dirtyText: { - reflect: true, - attribute: 'dirty-text', - required: { - dependentProps: ['dataState'], - validator: (_value, elem, hasValue) => hasValue || elem.dataState !== 'dirty' - }, - type: String - }, - /** - * The text displayed on the button dirty state overlay when the 'dirty' dataState is set. - * @type {string} - */ - dirtyButtonText: { - reflect: true, - attribute: 'dirty-button-text', - required: { - dependentProps: ['dataState'], - validator: (_value, elem, hasValue) => hasValue || elem.dataState !== 'dirty' - }, - type: String } }; @@ -412,10 +378,6 @@ export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionM this._tableIntersectionObserver = null; this._tableMutationObserver = null; this._tableScrollers = {}; - this.dataState = 'clean'; - - this.dirtyText = null; - this.dirtyButtonText = null; } connectedCallback() { diff --git a/mixins/data-state/data-state-mixin.js b/mixins/data-state/data-state-mixin.js new file mode 100644 index 00000000000..56a4586da2b --- /dev/null +++ b/mixins/data-state/data-state-mixin.js @@ -0,0 +1,41 @@ +import { PropertyRequiredMixin } from '../property-required/property-required-mixin.js'; + +export const DataStateMixin = superclass => class extends PropertyRequiredMixin(superclass) { + + static properties = { + + /** + * The state of data in the data component. Set to 'clean' when the data represents the user's latest selections, 'dirty' when the data does not represent the user's latest selections, and 'loading' if the data is being actively refreshed + * @type {'clean'|'dirty'|'loading'} + */ + dataState: { type: String, attribute: 'data-state', reflect: true }, + /** + * The text displayed on the button dirty state overlay when the 'dirty' dataState is set. + * @type {string} + */ + dirtyButtonText: { + type: String, attribute: 'dirty-button-text', reflect: true, required: { + dependentProps: ['dataState'], + validator: (_value, elem, hasValue) => hasValue || elem.dataState !== 'dirty' + } + }, + /** + * The text displayed on the dirty state overlay when the 'dirty' dataState is set. + * @type {string} + */ + dirtyText: { + type: String, attribute: 'dirty-text', reflect: true, required: { + dependentProps: ['dataState'], + validator: (_value, elem, hasValue) => hasValue || elem.dataState !== 'dirty' + } + } + }; + + constructor() { + super(); + this.dataState = 'clean'; + this.dirtyButtonText = null; + this.dirtyText = null; + } + +}; From 25e0771ce232f40f86d9d20a6772c8a5a940d56c Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 11:51:11 -0400 Subject: [PATCH 02/24] Reorder properties. --- components/backdrop/backdrop-loading.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index baec9adab1b..aa23037e0be 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -27,9 +27,9 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { * @type {string} */ for: { type: String, required: true }, - _state: { type: String, reflect: true }, + _ariaContent: { state: true }, _spinnerTop: { state: true }, - _ariaContent: { state: true } + _state: { type: String, reflect: true } }; static styles = [css` @@ -104,10 +104,10 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { constructor() { super(); - this._state = 'hidden'; - this._spinnerTop = 0; - this._dirtyDialogTop = 0; this._ariaContent = ''; + this._dirtyDialogTop = 0; + this._spinnerTop = 0; + this._state = 'hidden'; } render() { From 06b1bb0f10e3e25868ebfa38847ff3c46724c3e5 Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 11:57:43 -0400 Subject: [PATCH 03/24] Rename for clarity. --- components/backdrop/backdrop-loading.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index aa23037e0be..3c6266b52fc 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -33,16 +33,16 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { }; static styles = [css` - #visible { + .backdrop-container { display: none; } :host([_state="showing"]), :host([_state="shown"]), :host([_state="hiding"]), - :host([_state="showing"]) #visible, - :host([_state="shown"]) #visible, - :host([_state="hiding"]) #visible { + :host([_state="showing"]) .backdrop-container, + :host([_state="shown"]) .backdrop-container, + :host([_state="hiding"]) .backdrop-container { display: flex; inset: 0; justify-content: center; @@ -115,12 +115,12 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { return html` ${backdropVisible ? - html`
+ html`
` : nothing } -
+
${backdropVisible ? html` Date: Fri, 17 Jul 2026 12:07:54 -0400 Subject: [PATCH 04/24] Rename for clarity and add missing private reactive state property. --- components/backdrop/backdrop-loading.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index 3c6266b52fc..eee9b8512bb 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -28,6 +28,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { */ for: { type: String, required: true }, _ariaContent: { state: true }, + _dirtyOverlayTop: { state: true }, _spinnerTop: { state: true }, _state: { type: String, reflect: true } }; @@ -105,7 +106,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { constructor() { super(); this._ariaContent = ''; - this._dirtyDialogTop = 0; + this._dirtyOverlayTop = 0; this._spinnerTop = 0; this._state = 'hidden'; } @@ -123,7 +124,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) {
${backdropVisible ? html` Date: Fri, 17 Jul 2026 12:10:21 -0400 Subject: [PATCH 05/24] Renaming. --- components/backdrop/backdrop-loading.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index eee9b8512bb..69a6916aa92 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -29,7 +29,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { for: { type: String, required: true }, _ariaContent: { state: true }, _dirtyOverlayTop: { state: true }, - _spinnerTop: { state: true }, + _loadingSpinnerTop: { state: true }, _state: { type: String, reflect: true } }; @@ -107,7 +107,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { super(); this._ariaContent = ''; this._dirtyOverlayTop = 0; - this._spinnerTop = 0; + this._loadingSpinnerTop = 0; this._state = 'hidden'; } @@ -118,7 +118,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { ${backdropVisible ? html`
- +
` : nothing }
@@ -204,7 +204,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { // Adjust for the size of the spinner const spinnerSizeOffset = LOADING_SPINNER_SIZE / 2; - this._spinnerTop = centeringOffset + topOffset - spinnerSizeOffset; + this._loadingSpinnerTop = centeringOffset + topOffset - spinnerSizeOffset; // Adjust for the size of the dirty dialog const dirtyOverlay = this.shadowRoot.querySelector('d2l-backdrop-dirty-overlay'); From abc2b17cb344f8de667715dd7d635335329bb1ed Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 12:26:51 -0400 Subject: [PATCH 06/24] Rename to remove reference to dialog. --- components/backdrop/backdrop-loading.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index 69a6916aa92..c37b7dc6d7e 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -140,7 +140,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { super.updated(changedProperties); if (changedProperties.get('_state') && changedProperties.get('_state') === 'hidden') { - this.#centerLoadingSpinnerAndDialog(); + this.#centerLoadingSpinnerAndDirtyOverlay(); } if (changedProperties.has('_state')) { @@ -184,35 +184,35 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { } } - async #centerLoadingSpinnerAndDialog() { + async #centerLoadingSpinnerAndDirtyOverlay() { if (this._state === 'hidden') { return; } const loadingSpinner = this.shadowRoot.querySelector('d2l-loading-spinner'); if (!loadingSpinner) { return; } - const boundingRect = this.shadowRoot.querySelector('.backdrop-container').getBoundingClientRect(); + const backdropContainerRect = this.shadowRoot.querySelector('.backdrop-container').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 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, -boundingRect.top); // measures the distance below the top of the viewport, which is negative if the element starts above the viewport + 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 dirty dialog + // Adjust for the size of the dirty overlay 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; + const dirtyOverlaySizeOffset = dirtyOverlay.getBoundingClientRect().height / 2; - this._dirtyOverlayTop = centeringOffset + topOffset - dirtyDialogSizeOffset; + this._dirtyOverlayTop = centeringOffset + topOffset - dirtyOverlaySizeOffset; } } From 7a2bf2da013680f88c190387e1fe03fd681e3767 Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 12:33:53 -0400 Subject: [PATCH 07/24] Reformatting to reduce indentation. --- components/backdrop/backdrop-loading.js | 32 ++++++++++++------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index c37b7dc6d7e..5af43332674 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -11,7 +11,6 @@ const BACKDROP_DELAY_MS = 800; const FADE_DURATION_MS = 500; const SPINNER_DELAY_MS = FADE_DURATION_MS; const LOADING_ANNOUNCEMENT_DELAY = 1000; - const LOADING_SPINNER_SIZE = 50; const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; @@ -114,24 +113,23 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { render() { const backdropVisible = this._state !== 'hidden'; + const backdropContainer = backdropVisible ? html`
+
+ +
` : nothing; + + const backdropDirtyOverlay = backdropVisible ? html` + ` : nothing; + return html` - ${backdropVisible ? - html`
-
- -
` : nothing - } + ${backdropContainer}
- ${backdropVisible ? - html`` : nothing } - - ${this._ariaContent} - + ${backdropDirtyOverlay} + ${this._ariaContent}
`; } From 7863f33f1044f969bec4d7b340ec62859c6bb56a Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 12:35:12 -0400 Subject: [PATCH 08/24] Clean up dangling bracket. --- components/backdrop/backdrop-loading.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index 5af43332674..34dc1898876 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -136,8 +136,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { updated(changedProperties) { super.updated(changedProperties); - if (changedProperties.get('_state') && changedProperties.get('_state') === 'hidden') - { + if (changedProperties.get('_state') && changedProperties.get('_state') === 'hidden') { this.#centerLoadingSpinnerAndDirtyOverlay(); } From 718d8c2eb5cd2bb90f5df9ad25641f381ba528e5 Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 12:41:32 -0400 Subject: [PATCH 09/24] Rename private timeout id to be private and also explicit. --- components/backdrop/backdrop-loading.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index 34dc1898876..b12720356f5 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -181,6 +181,8 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { } } + #announcementTimeoutId; + async #centerLoadingSpinnerAndDirtyOverlay() { if (this._state === 'hidden') { return; } @@ -216,11 +218,11 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { #clearLiveArea() { this._ariaContent = ''; - if (this.announcementTimeout) { - clearTimeout(this.announcementTimeout); + if (this.#announcementTimeoutId) { + clearTimeout(this.#announcementTimeoutId); } - this.announcementTimeout = null; + this.#announcementTimeoutId = null; } #fade() { @@ -266,7 +268,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { } #setLiveArea(content, { delay } = {}) { - this.announcementTimeout = setTimeout(() => this._ariaContent = content, delay || 0); + this.#announcementTimeoutId = setTimeout(() => this._ariaContent = content, delay || 0); } #show() { From ae826c1ee8928ff3ffdb78942b27c1f8111c8120 Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 12:46:04 -0400 Subject: [PATCH 10/24] Renaming for clarity. --- components/backdrop/backdrop-loading.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index b12720356f5..80972d830bf 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -156,16 +156,16 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { willUpdate(changedProperties) { super.willUpdate(changedProperties); if (changedProperties.has('dataState') && changedProperties.get('dataState') !== undefined) { - this.#clearLiveArea(); + this.#clearLiveRegionContent(); const oldState = changedProperties.get('dataState'); const newState = this.dataState; // Calculate announcements if (newState === 'loading') { - this.#setLiveArea(this.localize('components.backdrop-loading.loadingAnnouncement'), { delay: LOADING_ANNOUNCEMENT_DELAY }); + this.#setLiveRegionContent(this.localize('components.backdrop-loading.loadingAnnouncement'), { delay: LOADING_ANNOUNCEMENT_DELAY }); } else if (oldState === 'loading' && newState === 'clean') { - this.#setLiveArea(this.localize('components.backdrop-loading.loadingCompleteAnnouncement')); + this.#setLiveRegionContent(this.localize('components.backdrop-loading.loadingCompleteAnnouncement')); } // Update backdrop @@ -215,7 +215,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { } } - #clearLiveArea() { + #clearLiveRegionContent() { this._ariaContent = ''; if (this.#announcementTimeoutId) { @@ -267,7 +267,7 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { if (containingBlock.dataset.initiallyInert !== '1') containingBlock.removeAttribute('inert'); } - #setLiveArea(content, { delay } = {}) { + #setLiveRegionContent(content, { delay } = {}) { this.#announcementTimeoutId = setTimeout(() => this._ariaContent = content, delay || 0); } From 05afac6f9d78adc0b2a71df721d881d6598aa1bf Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 12:48:56 -0400 Subject: [PATCH 11/24] Rename class. --- components/backdrop/backdrop-loading.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index 80972d830bf..0121b967c23 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -18,7 +18,7 @@ const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; /** * A component for displaying a semi-transparent backdrop and a loading spinner over the containing element */ -class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { +class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { static properties = { /** @@ -283,4 +283,4 @@ class LoadingBackdrop extends DataStateMixin(LocalizeCoreElement(LitElement)) { } -customElements.define('d2l-backdrop-loading', LoadingBackdrop); +customElements.define('d2l-backdrop-loading', BackdropLoading); From 428682314f3d16f62de76e8fc1b2f7e31f8a0170 Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 12:52:04 -0400 Subject: [PATCH 12/24] Rename to be more general. --- components/backdrop/backdrop-loading.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index 0121b967c23..5f7725f5e74 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -137,7 +137,7 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { updated(changedProperties) { super.updated(changedProperties); if (changedProperties.get('_state') && changedProperties.get('_state') === 'hidden') { - this.#centerLoadingSpinnerAndDirtyOverlay(); + this.#updatePosition(); } if (changedProperties.has('_state')) { @@ -183,7 +183,7 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { #announcementTimeoutId; - async #centerLoadingSpinnerAndDirtyOverlay() { + async #updatePosition() { if (this._state === 'hidden') { return; } const loadingSpinner = this.shadowRoot.querySelector('d2l-loading-spinner'); From 40962091d5ca2b886a4730dd22b7eb145192ff8e Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 12:53:42 -0400 Subject: [PATCH 13/24] Reorder method for linting. --- components/backdrop/backdrop-loading.js | 64 ++++++++++++------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index 5f7725f5e74..a2147ca74ef 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -183,38 +183,6 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { #announcementTimeoutId; - 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 dirty overlay - const dirtyOverlay = this.shadowRoot.querySelector('d2l-backdrop-dirty-overlay'); - if (dirtyOverlay) { - await this.shadowRoot.querySelector('d2l-empty-state-action-button')?.getUpdateComplete(); - const dirtyOverlaySizeOffset = dirtyOverlay.getBoundingClientRect().height / 2; - - this._dirtyOverlayTop = centeringOffset + topOffset - dirtyOverlaySizeOffset; - } - } - #clearLiveRegionContent() { this._ariaContent = ''; @@ -281,6 +249,38 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { 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 dirty overlay + const dirtyOverlay = this.shadowRoot.querySelector('d2l-backdrop-dirty-overlay'); + if (dirtyOverlay) { + await this.shadowRoot.querySelector('d2l-empty-state-action-button')?.getUpdateComplete(); + const dirtyOverlaySizeOffset = dirtyOverlay.getBoundingClientRect().height / 2; + + this._dirtyOverlayTop = centeringOffset + topOffset - dirtyOverlaySizeOffset; + } + } + } customElements.define('d2l-backdrop-loading', BackdropLoading); From 069106f0735ec10a0c67b0bad39ba7935443eaa3 Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 12:58:20 -0400 Subject: [PATCH 14/24] Minor reformatting. --- components/backdrop/backdrop-loading.js | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index a2147ca74ef..68c8f3b9a3c 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -136,6 +136,7 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { updated(changedProperties) { super.updated(changedProperties); + if (changedProperties.get('_state') && changedProperties.get('_state') === 'hidden') { this.#updatePosition(); } @@ -155,20 +156,19 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { willUpdate(changedProperties) { super.willUpdate(changedProperties); + if (changedProperties.has('dataState') && changedProperties.get('dataState') !== undefined) { this.#clearLiveRegionContent(); const oldState = changedProperties.get('dataState'); const newState = this.dataState; - // Calculate announcements if (newState === 'loading') { this.#setLiveRegionContent(this.localize('components.backdrop-loading.loadingAnnouncement'), { delay: LOADING_ANNOUNCEMENT_DELAY }); } else if (oldState === 'loading' && newState === 'clean') { this.#setLiveRegionContent(this.localize('components.backdrop-loading.loadingCompleteAnnouncement')); } - // Update backdrop if (oldState === 'clean') { this.#show(); } else if (newState === 'clean') { @@ -186,10 +186,7 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { #clearLiveRegionContent() { this._ariaContent = ''; - if (this.#announcementTimeoutId) { - clearTimeout(this.#announcementTimeoutId); - } - + if (this.#announcementTimeoutId) clearTimeout(this.#announcementTimeoutId); this.#announcementTimeoutId = null; } @@ -210,14 +207,10 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { #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]; } @@ -231,7 +224,6 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { this._state = 'hidden'; const containingBlock = this.#getBackdropTarget(); - if (containingBlock.dataset.initiallyInert !== '1') containingBlock.removeAttribute('inert'); } @@ -243,9 +235,7 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { this._state = reduceMotion ? 'shown' : 'showing'; const containingBlock = this.#getBackdropTarget(); - if (containingBlock.getAttribute('inert') !== null) containingBlock.dataset.initiallyInert = '1'; - containingBlock.setAttribute('inert', 'inert'); } From 862cb338b05014d6a9b5bef2d250618a6d868ec0 Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 13:05:06 -0400 Subject: [PATCH 15/24] Provide an enum for the different data states (clean, dirty, loading). --- mixins/data-state/data-state-mixin.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/mixins/data-state/data-state-mixin.js b/mixins/data-state/data-state-mixin.js index 56a4586da2b..6bba880d607 100644 --- a/mixins/data-state/data-state-mixin.js +++ b/mixins/data-state/data-state-mixin.js @@ -1,5 +1,11 @@ import { PropertyRequiredMixin } from '../property-required/property-required-mixin.js'; +export const dataStates = Object.freeze({ + clean: 'clean', + dirty: 'dirty', + loading: 'loading' +}); + export const DataStateMixin = superclass => class extends PropertyRequiredMixin(superclass) { static properties = { @@ -7,6 +13,7 @@ export const DataStateMixin = superclass => class extends PropertyRequiredMixin( /** * The state of data in the data component. Set to 'clean' when the data represents the user's latest selections, 'dirty' when the data does not represent the user's latest selections, and 'loading' if the data is being actively refreshed * @type {'clean'|'dirty'|'loading'} + * @default "clean" */ dataState: { type: String, attribute: 'data-state', reflect: true }, /** @@ -16,7 +23,7 @@ export const DataStateMixin = superclass => class extends PropertyRequiredMixin( dirtyButtonText: { type: String, attribute: 'dirty-button-text', reflect: true, required: { dependentProps: ['dataState'], - validator: (_value, elem, hasValue) => hasValue || elem.dataState !== 'dirty' + validator: (_value, elem, hasValue) => hasValue || elem.dataState !== dataStates.dirty } }, /** @@ -26,14 +33,14 @@ export const DataStateMixin = superclass => class extends PropertyRequiredMixin( dirtyText: { type: String, attribute: 'dirty-text', reflect: true, required: { dependentProps: ['dataState'], - validator: (_value, elem, hasValue) => hasValue || elem.dataState !== 'dirty' + validator: (_value, elem, hasValue) => hasValue || elem.dataState !== dataStates.dirty } } }; constructor() { super(); - this.dataState = 'clean'; + this.dataState = dataStates.clean; this.dirtyButtonText = null; this.dirtyText = null; } From f45c16d8f1cb660060d0c77cd299d8d1c1450381 Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 13:20:47 -0400 Subject: [PATCH 16/24] Update d2l-backdrop-loading to use the dataStates enum. --- components/backdrop/backdrop-loading.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/backdrop/backdrop-loading.js b/components/backdrop/backdrop-loading.js index 68c8f3b9a3c..35779452697 100644 --- a/components/backdrop/backdrop-loading.js +++ b/components/backdrop/backdrop-loading.js @@ -2,8 +2,8 @@ import '../colors/colors.js'; import '../loading-spinner/loading-spinner.js'; import './backdrop-dirty-overlay.js'; import { css, html, LitElement, nothing } from 'lit'; +import { DataStateMixin, dataStates } from '../../mixins/data-state/data-state-mixin.js'; import { getComposedChildren, getComposedParent } from '../../helpers/dom.js'; -import { DataStateMixin } from '../../mixins/data-state/data-state-mixin.js'; import { LocalizeCoreElement } from '../../helpers/localize-core-element.js'; import { styleMap } from 'lit/directives/style-map.js'; @@ -122,7 +122,7 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { style=${styleMap({ top: `${this._dirtyOverlayTop}px` })} description="${this.dirtyText}" action="${this.dirtyButtonText}" - ?inert=${this.dataState !== 'dirty'}> + ?inert=${this.dataState !== dataStates.dirty}> ` : nothing; return html` @@ -143,7 +143,7 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { if (changedProperties.has('_state')) { if (this._state === 'showing') { - if (this.dataState === 'loading') { + if (this.dataState === dataStates.loading) { setTimeout(() => { if (this._state === 'showing') this._state = 'shown'; }, BACKDROP_DELAY_MS); @@ -163,17 +163,17 @@ class BackdropLoading extends DataStateMixin(LocalizeCoreElement(LitElement)) { const oldState = changedProperties.get('dataState'); const newState = this.dataState; - if (newState === 'loading') { + if (newState === dataStates.loading) { this.#setLiveRegionContent(this.localize('components.backdrop-loading.loadingAnnouncement'), { delay: LOADING_ANNOUNCEMENT_DELAY }); - } else if (oldState === 'loading' && newState === 'clean') { + } else if (oldState === dataStates.loading && newState === dataStates.clean) { this.#setLiveRegionContent(this.localize('components.backdrop-loading.loadingCompleteAnnouncement')); } - if (oldState === 'clean') { + if (oldState === dataStates.clean) { this.#show(); - } else if (newState === 'clean') { + } else if (newState === dataStates.clean) { this.#fade(); - } else if (oldState === 'loading' && newState === 'dirty') { + } else if (oldState === dataStates.loading && newState === dataStates.dirty) { setTimeout(() => { if (this._state === 'showing') this._state = 'shown'; }, BACKDROP_DELAY_MS); From 3041cd872dc77c64b520440676da0a8f2aabb804 Mon Sep 17 00:00:00 2001 From: Dave Batiste Date: Fri, 17 Jul 2026 13:36:19 -0400 Subject: [PATCH 17/24] Update d2l-backdrop-loading tests and demo to use dataStates enum. --- components/backdrop/demo/backdrop-loading.html | 8 +++++--- components/backdrop/test/backdrop-loading.test.js | 13 +++++++------ components/backdrop/test/backdrop-loading.vdiff.js | 11 ++++++++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/components/backdrop/demo/backdrop-loading.html b/components/backdrop/demo/backdrop-loading.html index ccabd50e2fa..c0f951874b7 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 { dataStates } from '../../../mixins/data-state/data-state-mixin.js'; + window.backdropLoadingDataStates = dataStates;