Skip to content
Closed
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();
});
});
13 changes: 13 additions & 0 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 @@ -144,6 +146,7 @@ class DemoList extends LitElement {
this.items = JSON.parse(JSON.stringify(items));
this._lastItemLoadedIndex = 2;
this._pageSize = 2;
this.dataState = 'clean';
}

render() {
Expand All @@ -158,6 +161,11 @@ class DemoList extends LitElement {
?add-button="${this.addButton}"
add-button-text="${ifDefined(addButtonText)}">
<d2l-list-controls slot="controls" select-all-pages-allowed>
<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 @@ -228,6 +236,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
16 changes: 15 additions & 1 deletion components/list/list.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -98,6 +99,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 +184,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 @@ -247,11 +257,15 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
render() {
const role = !this.grid ? 'list' : 'application'; // not using grid role due to Safari+VO: https://bugs.webkit.org/show_bug.cgi?id=291591
const ariaLabel = this.slot !== 'nested' ? this.label : undefined;
console.log(this.dataState);
return html`
<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()}
`;
Expand Down
20 changes: 12 additions & 8 deletions components/table/demo/table-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import '../../selection/selection-action.js';
import '../../selection/selection-action-dropdown.js';
import '../../selection/selection-action-menu-item.js';
import '../../selection/selection-input.js';
import '../../inputs/input-radio.js';
import '../../inputs/input-radio-group.js';

import { css, html, nothing } from 'lit';
import { tableStyles, TableWrapper } from '../table-wrapper.js';
Expand Down Expand Up @@ -81,6 +83,7 @@ class TestTable extends DemoPassthroughMixin(TableWrapper, 'd2l-table-wrapper')
this._data = data();
this._sortField = undefined;
this._sortDesc = false;
this.dataState = 'clean';
}

render() {
Expand All @@ -97,11 +100,11 @@ class TestTable extends DemoPassthroughMixin(TableWrapper, 'd2l-table-wrapper')
icon="tier1:${this.stickyHeaders ? 'check' : 'close-default'}"
@d2l-selection-action-click="${this._toggleStickyHeaders}"
></d2l-selection-action>
<d2l-selection-action
text="Loading"
icon="tier1:${this.loading ? 'check' : 'close-default'}"
@d2l-selection-action-click="${this._toggleLoading}"
></d2l-selection-action>
<d2l-input-radio-group style="align-content:center" 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-table-controls>

<table class="d2l-table">
Expand Down Expand Up @@ -167,6 +170,10 @@ class TestTable extends DemoPassthroughMixin(TableWrapper, 'd2l-table-wrapper')
`;
}

_handleDataStateChange(e) {
this.dataState = e.detail.value;
}

async _handlePagerLoadMore(e) {
const pageSize = e.target.pageSize;
await new Promise(resolve => setTimeout(resolve, 1000));
Expand Down Expand Up @@ -256,9 +263,6 @@ class TestTable extends DemoPassthroughMixin(TableWrapper, 'd2l-table-wrapper')
this.requestUpdate();
}

_toggleLoading() {
this.loading = !this.loading;
}
_toggleStickyControls() {
this.stickyControls = !this.stickyControls;
}
Expand Down
Loading