From d2a1f584e4fc5b9e546c98dfaee9bb050e32aff3 Mon Sep 17 00:00:00 2001 From: Duncan Uszkay Date: Tue, 14 Apr 2026 11:40:36 -0400 Subject: [PATCH] Add loading backdrop to list --- components/list/demo/demo-list.js | 43 +++++++++++++- components/list/list.js | 67 +++++++++++++++++++++- components/selection/selection-controls.js | 8 ++- 3 files changed, 113 insertions(+), 5 deletions(-) diff --git a/components/list/demo/demo-list.js b/components/list/demo/demo-list.js index 813570c13d2..827522cbd3a 100644 --- a/components/list/demo/demo-list.js +++ b/components/list/demo/demo-list.js @@ -13,6 +13,8 @@ import '../list-controls.js'; import '../list-item-content.js'; import '../list-item.js'; import '../list.js'; +import '../../inputs/input-radio.js'; +import '../../inputs/input-radio-group.js'; import { css, html, LitElement } from 'lit'; import { getUniqueId } from '../../../helpers/uniqueId.js'; import { ifDefined } from 'lit/directives/if-defined.js'; @@ -116,10 +118,12 @@ class DemoList extends LitElement { static get properties() { return { + dataState: { type: String }, addButton: { type: Boolean, attribute: 'add-button' }, grid: { type: Boolean }, extendSeparators: { type: Boolean, attribute: 'extend-separators' }, - _lastItemLoadedIndex: { state: true } + _lastItemLoadedIndex: { state: true }, + _selectAllDisabled: { state: true } }; } @@ -144,6 +148,7 @@ class DemoList extends LitElement { this.items = JSON.parse(JSON.stringify(items)); this._lastItemLoadedIndex = 2; this._pageSize = 2; + this.dataState = 'clean'; } render() { @@ -155,9 +160,18 @@ class DemoList extends LitElement { ?grid="${this.grid}" item-count="${this.items.length}" ?extend-separators="${this.extendSeparators}" + .dataState="${this.dataState}" ?add-button="${this.addButton}" + dirty-text="Your filters have changed" + dirty-button-text="Apply" + @d2l-list-dirty-button-clicked=${this._handleReload} add-button-text="${ifDefined(addButtonText)}"> - + + + + + + @@ -216,6 +230,21 @@ class DemoList extends LitElement { `; } + updated(changedProperties) { + if (changedProperties.has('dataState')) { + switch (this.dataState) { + case 'clean': + this._selectAllDisabled = false; + break; + case 'dirty': + this._selectAllDisabled = true; + break; + case 'loading': + setTimeout(() => { this._selectAllDisabled = this.dataState !== 'clean'; }, 800); + } + } + } + _handleAddItem() { const newKey = getUniqueId(); this.items.push({ @@ -228,6 +257,11 @@ class DemoList extends LitElement { this.requestUpdate(); } + _handleDataStateChange(e) { + this.shadowRoot.querySelector('d2l-list').dataState = e.detail.value; + this.dataState = e.detail.value; + } + _handlePagerLoadMore(e) { // mock delay consumers might have setTimeout(() => { @@ -237,5 +271,10 @@ class DemoList extends LitElement { } + _handleReload() { + this.dataState = 'loading'; + setTimeout(() => { this.dataState = 'clean'; }, 2000); + } + } customElements.define('d2l-demo-list', DemoList); diff --git a/components/list/list.js b/components/list/list.js index 6ffc020f129..f15bcffc549 100644 --- a/components/list/list.js +++ b/components/list/list.js @@ -1,3 +1,4 @@ +import '../backdrop/backdrop-loading.js'; import { css, html, LitElement } from 'lit'; import { getNextFocusable, getPreviousFocusable } from '../../helpers/focus.js'; import { SelectionInfo, SelectionMixin } from '../selection/selection-mixin.js'; @@ -94,13 +95,48 @@ class List extends PageableMixin(SelectionMixin(LitElement)) { * @default "all" */ separators: { type: String, reflect: true }, + /** + * 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 + }, /** * Show selection only on hover, focus or if at least one item is selected. Exclusive for the tile layout * @type {boolean} */ selectionWhenInteracted: { type: Boolean, attribute: 'selection-when-interacted', reflect: true }, _breakpoint: { type: Number, reflect: true }, - _slimColor: { type: Boolean, reflect: true, attribute: '_slim-color' } + _slimColor: { type: Boolean, reflect: true, attribute: '_slim-color' }, + _backdropTriggered: { type: Boolean, reflect: true } }; } @@ -161,6 +197,19 @@ class List extends PageableMixin(SelectionMixin(LitElement)) { flex-basis: 100%; height: 0; } + + :host([_backdropTriggered]) { + position: relative; + } + :host([_backdropTriggered]) slot { + z-index: 2; + } + :host([_backdropTriggered]) d2l-backdrop-loading { + z-index: 1; + } + :host([_backdropTriggered]) #list-slot { + z-index: 0; + } `; } @@ -175,11 +224,15 @@ class List extends PageableMixin(SelectionMixin(LitElement)) { this._listItemChanges = []; this._childHasColor = false; this._childHasExpandCollapseToggle = false; + this.dataState = 'clean'; this._breakpoint = 0; this._slimColor = false; this._width = 0; + this.dirtyText = null; + this.dirtyButtonText = null; + this._listChildrenUpdatedSubscribers = new SubscriberRegistryController(this, 'list-child-status', { onSubscribe: this._updateActiveSubscriber.bind(this), updateSubscribers: this._updateActiveSubscribers.bind(this) @@ -250,12 +303,14 @@ class List extends PageableMixin(SelectionMixin(LitElement)) { return html` -
+
+ ${this._renderPagerContainer()} `; } + willUpdate(changedProperties) { super.willUpdate(changedProperties); if (changedProperties.has('breakpoints') && changedProperties.get('breakpoints') !== undefined) { @@ -276,6 +331,9 @@ class List extends PageableMixin(SelectionMixin(LitElement)) { if (changedProperties.has('dragHandleShowAlways')) { this._updateItemDragHandleShowAlways(); } + if (changedProperties.has('dataState') && this.dataState !== undefined) { + this._backdropTriggered = true; + } } getItems(slot) { @@ -381,6 +439,11 @@ class List extends PageableMixin(SelectionMixin(LitElement)) { return items.length > 0 ? items[0]._getFlattenedListItems().lazyLoadListItems : new Map(); } + _handleDirtyButton() { + /** Dispatched when the action button on the dirty overlay is clicked */ + this.dispatchEvent(new CustomEvent('d2l-list-dirty-button-clicked')); + } + _handleKeyDown(e) { if (!this.grid || this.slot === 'nested' || e.keyCode !== keyCodes.TAB) return; e.preventDefault(); diff --git a/components/selection/selection-controls.js b/components/selection/selection-controls.js index d164e68ea46..d38f4a86515 100644 --- a/components/selection/selection-controls.js +++ b/components/selection/selection-controls.js @@ -39,6 +39,11 @@ export class SelectionControls extends PageableSubscriberMixin(SelectionObserver * @type {boolean} */ selectAllPagesAllowed: { type: Boolean, attribute: 'select-all-pages-allowed' }, + /** + * Whether to disable and visually grey out the select all items checkbox + * @type {boolean} + */ + selectAllPagesDisabled: { type: Boolean, attribute: 'select-all-pages-disabled' }, _hasActions: { state: true }, _noSelectionText: { state: true }, _scrolled: { type: Boolean, reflect: true } @@ -118,6 +123,7 @@ export class SelectionControls extends PageableSubscriberMixin(SelectionObserver constructor() { super(); this.noSelection = false; + this.selectAllPagesDisabled = false; this.noSticky = false; this.selectAllPagesAllowed = false; this._scrolled = false; @@ -183,7 +189,7 @@ export class SelectionControls extends PageableSubscriberMixin(SelectionObserver _renderSelection() { return html` - ${this._provider && !this._noSelectAll ? html`` : nothing} + ${this._provider && !this._noSelectAll ? html`` : nothing} ${this.selectAllPagesAllowed ? html`` : nothing} `;