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
102 changes: 81 additions & 21 deletions components/backdrop/backdrop-loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const BACKDROP_DELAY_MS = 800;
const FADE_DURATION_MS = 500;
const SPINNER_DELAY_MS = FADE_DURATION_MS;
const LOADING_ANNOUNCEMENT_DELAY = 1000;
const DIRTY_ANNOUNCEMENT_DELAY = 1000;

const LOADING_SPINNER_SIZE = 50;

Expand All @@ -25,10 +26,13 @@ class LoadingBackdrop extends LocalizeCoreElement(LitElement) {
static get properties() {
return {
/**
* Used to control whether the loading backdrop is shown
* @type {boolean}
* 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'}
*/
shown: { type: Boolean },
dataState: {
reflect: true,
type: String
},
/**
* Used to identify content that the backdrop should make inert
* @type {boolean}
Expand Down Expand Up @@ -81,9 +85,28 @@ class LoadingBackdrop extends LocalizeCoreElement(LitElement) {
opacity: 1;
transition: opacity ${FADE_DURATION_MS}ms ease-in ${SPINNER_DELAY_MS}ms;
}

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

d2l-empty-state-simple {
background-color: var(--d2l-table-controls-background-color, white);
top: 0;
opacity: 0;
height: fit-content;
justify-content: center;
position: relative;
z-index: 1000;
}
:host([_state="shown"]) d2l-empty-state-simple {
opacity: 1;
transition: opacity ${FADE_DURATION_MS}ms ease-in;
}
:host([_state="shown"][dataState="loading"]) d2l-empty-state-simple,
:host([_state="hiding"]) d2l-empty-state-simple {
opacity: 0;
transition: opacity ${FADE_DURATION_MS}ms ease-out;
}

Expand All @@ -95,9 +118,10 @@ class LoadingBackdrop extends LocalizeCoreElement(LitElement) {

constructor() {
super();
this.shown = false;
this.dataState = 'clean';
this._state = 'hidden';
this._spinnerTop = 0;
this._dirtyDialogTop = 0;
this._ariaContent = '';
}

Expand All @@ -107,40 +131,62 @@ class LoadingBackdrop extends LocalizeCoreElement(LitElement) {
html`<div id="visible">
<div class="backdrop" @transitionend="${this.#handleTransitionEnd}" @transitioncancel="${this.#handleTransitionEnd}"></div>
<d2l-loading-spinner style=${styleMap({ top: `${this._spinnerTop}px` })} size="${LOADING_SPINNER_SIZE}"></d2l-loading-spinner>
<d2l-empty-state-simple style=${styleMap({ top: `${this._dirtyDialogTop}px` })} description="${this.localize('components.backdrop-loading.dirtyDialogDescription')}">
<d2l-empty-state-action-button @d2l-empty-state-action=${this.#handleApplyButton} text="${this.localize('components.backdrop-loading.dirtyDialogAction')}"></d2l-empty-state-action-button>
</d2l-empty-state-simple>
</div>`
}
<d2l-offscreen aria-live="polite">${this._ariaContent}</d2l-offscreen>
`;
}
updated(changedProperties) {
if (changedProperties.get('_state') && changedProperties.get('_state') === 'hidden')
{
this.#centerLoadingSpinner();
}

if (changedProperties.has('_state')) {
if (this._state === 'showing') {
setTimeout(() => {
if (this._state === 'showing') this._state = 'shown';
}, BACKDROP_DELAY_MS);
if (this.dataState === 'loading') {
setTimeout(() => {
if (this._state === 'showing') this._state = 'shown';
}, BACKDROP_DELAY_MS);
} else {
this._state = 'shown';
}
}
}

if (changedProperties.has('shown') && (
(reduceMotion && this._state === 'shown') || (!reduceMotion && this._state === 'showing')
)) {
this.#centerLoadingSpinner();
}
}
willUpdate(changedProperties) {
if (changedProperties.has('shown')) {
if (changedProperties.has('dataState') && changedProperties.get('dataState') !== undefined) {
this.#clearLiveArea();
if (this.shown) {

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.#show();
} else if (changedProperties.get('shown') !== undefined) {
} else if (oldState === 'loading' && newState === 'clean') {
this.#setLiveArea(this.localize('components.backdrop-loading.loadingCompleteAnnouncement'));
} else if (newState === 'dirty') {
this.#setLiveArea(this.localize('components.backdrop-loading.dirtyAnnouncement'), { delay: DIRTY_ANNOUNCEMENT_DELAY });
}

// Update backdrop
if (oldState === 'clean') {
this.#show();
} else if (newState === 'clean') {
this.#fade();
} else if (oldState === 'loading' && newState === 'dirty') {
setTimeout(() => {
if (this._state === 'showing') this._state = 'shown';
}, BACKDROP_DELAY_MS);
}
}
}

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

const loadingSpinner = this.shadowRoot.querySelector('d2l-loading-spinner');
Expand All @@ -160,7 +206,13 @@ class LoadingBackdrop extends LocalizeCoreElement(LitElement) {
// Adjust for the size of the spinner
const spinnerSizeOffset = LOADING_SPINNER_SIZE / 2;

// Adjust for the size of the dirty dialog
await this.shadowRoot.querySelector('d2l-empty-state-simple').getUpdateComplete();
await this.shadowRoot.querySelector('d2l-empty-state-action-button')?.getUpdateComplete();
const dirtyDialogSizeOffset = this.shadowRoot.querySelector('d2l-empty-state-simple').getBoundingClientRect().height / 2;

this._spinnerTop = centeringOffset + topOffset - spinnerSizeOffset;
this._dirtyDialogTop = centeringOffset + topOffset - dirtyDialogSizeOffset;
}

#clearLiveArea() {
Expand All @@ -186,6 +238,7 @@ class LoadingBackdrop extends LocalizeCoreElement(LitElement) {
this._state = 'hiding';
}
}

#getBackdropTarget() {
const parent = getComposedParent(this);

Expand All @@ -199,21 +252,29 @@ class LoadingBackdrop extends LocalizeCoreElement(LitElement) {

return targetedChildren.length === 0 ? parent : targetedChildren[0];
}

#handleApplyButton() {
this.dispatchEvent(new CustomEvent('d2l-apply-button-click', { bubbles: true, composed: true }));
}

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

#hide() {
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);
}

#show() {
this._state = reduceMotion ? 'shown' : 'showing';

Expand All @@ -223,7 +284,6 @@ class LoadingBackdrop extends LocalizeCoreElement(LitElement) {

containingBlock.setAttribute('inert', 'inert');
}

}

customElements.define('d2l-backdrop-loading', LoadingBackdrop);
12 changes: 9 additions & 3 deletions components/backdrop/test/backdrop-loading.vdiff.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ const template = html`
`;

describe('backdrop-loading', () => {
it('not shown', async() => {
it('clean', async() => {
const elem = await fixture(template);
await expect(elem).to.be.golden();
});

it('shown', async() => {
it('dirty', async() => {
const elem = await fixture(template);
elem.querySelector('d2l-backdrop-loading').shown = true;
elem.querySelector('d2l-backdrop-loading').dataState = 'dirty';
await expect(elem).to.be.golden();
});

it('loading', async() => {
const elem = await fixture(template);
elem.querySelector('d2l-backdrop-loading').dataState = 'loading';
await expect(elem).to.be.golden();
});
});
34 changes: 32 additions & 2 deletions components/list/demo/demo-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 }
};
}

Expand All @@ -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() {
Expand All @@ -157,7 +162,12 @@ class DemoList extends LitElement {
?extend-separators="${this.extendSeparators}"
?add-button="${this.addButton}"
add-button-text="${ifDefined(addButtonText)}">
<d2l-list-controls slot="controls" select-all-pages-allowed>
<d2l-list-controls slot="controls" select-all-pages-allowed ?disabled=${this._selectAllDisabled}>
<d2l-input-radio-group style="align-content:center;min-width:260px;" label="Date State" horizontal label-hidden name="dataState" @change=${this._handleDataStateChange}>
<d2l-input-radio label="Clean" value="clean" ?checked=${this.dataState === 'clean'}></d2l-input-radio>
<d2l-input-radio label="Dirty" value="dirty" ?checked=${this.dataState === 'dirty'}></d2l-input-radio>
<d2l-input-radio label="Loading" value="loading" ?checked=${this.dataState === 'loading'}></d2l-input-radio>
</d2l-input-radio-group>
<d2l-selection-action icon="tier1:plus-default" text="Add" @d2l-selection-action-click="${this._handleAddItem}"></d2l-selection-action>
<d2l-selection-action-dropdown text="Move To" requires-selection>
<d2l-dropdown-menu>
Expand Down Expand Up @@ -216,6 +226,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({
Expand All @@ -228,6 +253,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(() => {
Expand Down
21 changes: 20 additions & 1 deletion components/list/list.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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';
import { ifDefined } from 'lit/directives/if-defined.js';
import { PageableMixin } from '../paging/pageable-mixin.js';
import { SubscriberRegistryController } from '../../controllers/subscriber/subscriberControllers.js';
import { querySelectorComposed } from '../../helpers/dom.js';
import { query } from 'lit/decorators.js';

const keyCodes = {
TAB: 9
Expand Down Expand Up @@ -98,6 +101,14 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
* Show selection only on hover, focus or if at least one item is selected. Exclusive for the tile layout
* @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
},
selectionWhenInteracted: { type: Boolean, attribute: 'selection-when-interacted', reflect: true },
_breakpoint: { type: Number, reflect: true },
_slimColor: { type: Boolean, reflect: true, attribute: '_slim-color' }
Expand Down Expand Up @@ -175,6 +186,7 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
this._listItemChanges = [];
this._childHasColor = false;
this._childHasExpandCollapseToggle = false;
this.dataState = 'clean';

this._breakpoint = 0;
this._slimColor = false;
Expand Down Expand Up @@ -251,11 +263,15 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
<slot name="controls"></slot>
<slot name="header"></slot>
<div role="${role}" aria-label="${ifDefined(ariaLabel)}" class="d2l-list-content">
<slot @keydown="${this._handleKeyDown}" @slotchange="${this._handleSlotChange}"></slot>
<div style="position:relative">
<slot id="list-slot" @keydown="${this._handleKeyDown}" @slotchange="${this._handleSlotChange}"></slot>
<d2l-backdrop-loading for="list-slot" dataState='${this.dataState}'></d2l-backdrop-loading>
</div>
</div>
${this._renderPagerContainer()}
`;
}

willUpdate(changedProperties) {
super.willUpdate(changedProperties);
if (changedProperties.has('breakpoints') && changedProperties.get('breakpoints') !== undefined) {
Expand Down Expand Up @@ -381,6 +397,9 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
return items.length > 0 ? items[0]._getFlattenedListItems().lazyLoadListItems : new Map();
}

_getLazyLoadItemsFoo() {
}

_handleKeyDown(e) {
if (!this.grid || this.slot === 'nested' || e.keyCode !== keyCodes.TAB) return;
e.preventDefault();
Expand Down
8 changes: 7 additions & 1 deletion components/selection/selection-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export class SelectionControls extends PageableSubscriberMixin(SelectionObserver
* @type {boolean}
*/
selectAllPagesAllowed: { type: Boolean, attribute: 'select-all-pages-allowed' },
/**
* Disables the select all checkbox
* @type {boolean}
*/
disabled: { type: Boolean, attribute: 'disabled' },
_hasActions: { state: true },
_noSelectionText: { state: true },
_scrolled: { type: Boolean, reflect: true }
Expand Down Expand Up @@ -118,6 +123,7 @@ export class SelectionControls extends PageableSubscriberMixin(SelectionObserver
constructor() {
super();
this.noSelection = false;
this.disabled = false;
this.noSticky = false;
this.selectAllPagesAllowed = false;
this._scrolled = false;
Expand Down Expand Up @@ -183,7 +189,7 @@ export class SelectionControls extends PageableSubscriberMixin(SelectionObserver

_renderSelection() {
return html`
${this._provider && !this._noSelectAll ? html`<d2l-selection-select-all></d2l-selection-select-all>` : nothing}
${this._provider && !this._noSelectAll ? html`<d2l-selection-select-all ?disabled=${this.disabled} ></d2l-selection-select-all>` : nothing}
<d2l-selection-summary no-selection-text="${ifDefined(this._noSelectionText)}"></d2l-selection-summary>
${this.selectAllPagesAllowed ? html`<d2l-selection-select-all-pages></d2l-selection-select-all-pages>` : nothing}
`;
Expand Down
Loading