From 41555c8b61cc671bd87285fada83dc56fbec3b95 Mon Sep 17 00:00:00 2001 From: Duncan Uszkay Date: Wed, 4 Feb 2026 11:52:48 -0500 Subject: [PATCH 1/2] feat: Table visual loading state --- components/table/table-loading-backdrop.js | 128 +++++++++++++++++++++ components/table/table-wrapper.js | 12 +- 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 components/table/table-loading-backdrop.js diff --git a/components/table/table-loading-backdrop.js b/components/table/table-loading-backdrop.js new file mode 100644 index 00000000000..dd5277bb846 --- /dev/null +++ b/components/table/table-loading-backdrop.js @@ -0,0 +1,128 @@ +import '../colors/colors.js'; +import { css, html, LitElement } from 'lit'; + +const BACKDROP_DELAY_MS = 800; +const BACKDROP_FADE_IN_DURATION_MS = 500; +const BACKDROP_FADE_OUT_DURATION_MS = 500; +const SPINNER_DELAY_MS = BACKDROP_DELAY_MS + BACKDROP_FADE_IN_DURATION_MS; +const SPINNER_FADE_IN_DURATION_MS = 500; +const SPINNER_FADE_OUT_DURATION_MS = 500; + +/** + * A component for displaying a semi-transparent backdrop and a loading spinner when a table enters a loading state. + */ +class TableLoadingBackdrop extends LitElement { + + static get properties() { + return { + shown: { type: Boolean }, + _state: { type: String, reflect: true }, + }; + } + + static get styles() { + return css` + :host, .backdrop, d2l-loading-spinner { + width: 0%; + height: 0%; + position: absolute; + } + + .backdrop, d2l-loading-spinner { + opacity: 0; + } + + :host { + z-index: 997 + } + + .backdrop { + z-index: 998; + background-color: var(--d2l-color-regolith); + } + + d2l-loading-spinner { + z-index: 999; + top: 100px; + } + + :host([_state="showing"]), + :host([_state="hiding"]), + d2l-loading-spinner[_state="showing"], + d2l-loading-spinner[_state="hiding"], + .backdrop[_state="showing"], + .backdrop[_state="hiding"] { + width: 100%; + height: 100%; + } + + d2l-loading-spinner[_state="showing"] { + opacity: 1; + transition: opacity ${SPINNER_FADE_IN_DURATION_MS}ms ease-in-out ${SPINNER_DELAY_MS}ms; + } + + .backdrop[_state="showing"] { + opacity: 0.7; + transition: opacity ${BACKDROP_FADE_IN_DURATION_MS}ms ease-in-out ${BACKDROP_DELAY_MS}ms; + } + + d2l-loading-spinner[_state="hiding"] { + transition: opacity ${SPINNER_FADE_OUT_DURATION_MS}ms ease-in-out; + } + + .backdrop[_state="hiding"] { + transition: opacity ${BACKDROP_FADE_OUT_DURATION_MS}ms ease-in-out; + } + + @media (prefers-reduced-motion: reduce) { + * { transition: none} + } + `; + } + + constructor() { + super(); + this.shown = false; + this._state = null; + } + + render() { + return html` + +
+ `; + } + + willUpdate(changedProperties) { + if (changedProperties.get('shown') !== undefined) { + if (this.shown) { + this._state = 'showing'; + } else { + this._state = 'hiding'; + + this._hideAfterFading(); + } + } + } + + _hideAfterFading() { + const backdrop = this.shadowRoot.querySelector('.backdrop'); + const loadingSpinner = this.shadowRoot.querySelector('d2l-loading-spinner'); + + Promise.all([ + new Promise(resolve => { + backdrop.addEventListener('transitionend', resolve, { once: true }); + backdrop.addEventListener('transitioncancel', resolve, { once: true }); + }), + new Promise(resolve => { + loadingSpinner.addEventListener('transitionend', resolve, { once: true }); + loadingSpinner.addEventListener('transitioncancel', resolve, { once: true }); + }) + ]).then(() => { + this._state = null; + }); + } + +} + +customElements.define('d2l-table-loading-backdrop', TableLoadingBackdrop); diff --git a/components/table/table-wrapper.js b/components/table/table-wrapper.js index b26422ca184..2cca3fcdb53 100644 --- a/components/table/table-wrapper.js +++ b/components/table/table-wrapper.js @@ -1,5 +1,6 @@ import '../colors/colors.js'; import '../scroll-wrapper/scroll-wrapper.js'; +import './table-loading-backdrop.js'; import { css, html, LitElement, nothing } from 'lit'; import { cssSizes } from '../inputs/input-checkbox.js'; import { getComposedParent } from '../../helpers/dom.js'; @@ -254,6 +255,7 @@ export const tableStyles = css` [data-popover-count] { z-index: 6 !important; /* if opened above, we want to stack on top of sticky table-controls */ } + `; const SELECTORS = { @@ -313,6 +315,10 @@ export class TableWrapper extends PageableMixin(SelectionMixin(LitElement)) { reflect: true, type: Boolean, }, + loading: { + reflect: true, + type: Boolean + }, }; } @@ -382,6 +388,7 @@ export class TableWrapper extends PageableMixin(SelectionMixin(LitElement)) { this._tableIntersectionObserver = null; this._tableMutationObserver = null; this._tableScrollers = {}; + this._loading = false; } connectedCallback() { @@ -410,7 +417,10 @@ export class TableWrapper extends PageableMixin(SelectionMixin(LitElement)) { } render() { - const slot = html``; + const slot = html` + + + `; const useScrollWrapper = this.stickyHeadersScrollWrapper || !this.stickyHeaders; return html` From ab4b394b133df323323f1d7405022096079f45c1 Mon Sep 17 00:00:00 2001 From: Duncan Uszkay Date: Thu, 5 Feb 2026 16:36:20 -0500 Subject: [PATCH 2/2] Generalize into Loading Backdrop --- components/backdrop/README.md | 78 ++++++++++++++++++- .../loading-backdrop.js} | 33 ++++---- components/table/table-wrapper.js | 4 +- 3 files changed, 94 insertions(+), 21 deletions(-) rename components/{table/table-loading-backdrop.js => backdrop/loading-backdrop.js} (79%) diff --git a/components/backdrop/README.md b/components/backdrop/README.md index b284044a505..015c3396994 100644 --- a/components/backdrop/README.md +++ b/components/backdrop/README.md @@ -1,9 +1,11 @@ # Backdrops -Use a backdrop to de-emphasize background elements and draw the user's attention to a dialog or other modal content. +Use a backdrop to de-emphasize the content of an element or page. ## Backdrop [d2l-backdrop] +Use a backdrop to de-emphasize background elements and draw the user's attention to a dialog or other modal content. + ```html + +
Refresh Content
+
+ + + + + + + + + + + + + + + + +
CourseGradeHours Spent in Content
Math85%100
Art98%10
+ +
+``` + +### Properties: + +| Property | Type | Description | +|--|--|--| +| `shown` | Boolean | Used to control whether the loading backdrop is shown | + diff --git a/components/table/table-loading-backdrop.js b/components/backdrop/loading-backdrop.js similarity index 79% rename from components/table/table-loading-backdrop.js rename to components/backdrop/loading-backdrop.js index dd5277bb846..a7374d7a092 100644 --- a/components/table/table-loading-backdrop.js +++ b/components/backdrop/loading-backdrop.js @@ -3,18 +3,22 @@ import { css, html, LitElement } from 'lit'; const BACKDROP_DELAY_MS = 800; const BACKDROP_FADE_IN_DURATION_MS = 500; -const BACKDROP_FADE_OUT_DURATION_MS = 500; +const BACKDROP_FADE_OUT_DURATION_MS = 200; const SPINNER_DELAY_MS = BACKDROP_DELAY_MS + BACKDROP_FADE_IN_DURATION_MS; const SPINNER_FADE_IN_DURATION_MS = 500; -const SPINNER_FADE_OUT_DURATION_MS = 500; +const SPINNER_FADE_OUT_DURATION_MS = 200; /** - * A component for displaying a semi-transparent backdrop and a loading spinner when a table enters a loading state. + * A component for displaying a semi-transparent backdrop and a loading spinner over the containing element */ -class TableLoadingBackdrop extends LitElement { +class LoadingBackdrop extends LitElement { static get properties() { return { + /** + * Used to control whether the loading backdrop is shown + * @type {boolean} + */ shown: { type: Boolean }, _state: { type: String, reflect: true }, }; @@ -33,16 +37,15 @@ class TableLoadingBackdrop extends LitElement { } :host { - z-index: 997 + z-index: 999; + top: 0px; } .backdrop { - z-index: 998; background-color: var(--d2l-color-regolith); } d2l-loading-spinner { - z-index: 999; top: 100px; } @@ -58,20 +61,20 @@ class TableLoadingBackdrop extends LitElement { d2l-loading-spinner[_state="showing"] { opacity: 1; - transition: opacity ${SPINNER_FADE_IN_DURATION_MS}ms ease-in-out ${SPINNER_DELAY_MS}ms; + transition: opacity ${SPINNER_FADE_IN_DURATION_MS}ms ease-in ${SPINNER_DELAY_MS}ms; } .backdrop[_state="showing"] { opacity: 0.7; - transition: opacity ${BACKDROP_FADE_IN_DURATION_MS}ms ease-in-out ${BACKDROP_DELAY_MS}ms; + transition: opacity ${BACKDROP_FADE_IN_DURATION_MS}ms ease-in ${BACKDROP_DELAY_MS}ms; } d2l-loading-spinner[_state="hiding"] { - transition: opacity ${SPINNER_FADE_OUT_DURATION_MS}ms ease-in-out; + transition: opacity ${SPINNER_FADE_OUT_DURATION_MS}ms ease-out; } .backdrop[_state="hiding"] { - transition: opacity ${BACKDROP_FADE_OUT_DURATION_MS}ms ease-in-out; + transition: opacity ${BACKDROP_FADE_OUT_DURATION_MS}ms ease-out; } @media (prefers-reduced-motion: reduce) { @@ -88,16 +91,16 @@ class TableLoadingBackdrop extends LitElement { render() { return html` -
+ `; } willUpdate(changedProperties) { - if (changedProperties.get('shown') !== undefined) { + if (changedProperties.has('shown')) { if (this.shown) { this._state = 'showing'; - } else { + } else if (changedProperties.get('shown') !== undefined) { this._state = 'hiding'; this._hideAfterFading(); @@ -125,4 +128,4 @@ class TableLoadingBackdrop extends LitElement { } -customElements.define('d2l-table-loading-backdrop', TableLoadingBackdrop); +customElements.define('d2l-loading-backdrop', LoadingBackdrop); diff --git a/components/table/table-wrapper.js b/components/table/table-wrapper.js index 2cca3fcdb53..852f6315449 100644 --- a/components/table/table-wrapper.js +++ b/components/table/table-wrapper.js @@ -1,6 +1,6 @@ import '../colors/colors.js'; import '../scroll-wrapper/scroll-wrapper.js'; -import './table-loading-backdrop.js'; +import '../backdrop/loading-backdrop.js'; import { css, html, LitElement, nothing } from 'lit'; import { cssSizes } from '../inputs/input-checkbox.js'; import { getComposedParent } from '../../helpers/dom.js'; @@ -418,8 +418,8 @@ export class TableWrapper extends PageableMixin(SelectionMixin(LitElement)) { render() { const slot = html` - + `; const useScrollWrapper = this.stickyHeadersScrollWrapper || !this.stickyHeaders; return html`