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
78 changes: 74 additions & 4 deletions components/backdrop/README.md
Original file line number Diff line number Diff line change
@@ -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.

<!-- docs: demo code properties name:d2l-backdrop sandboxTitle:'Backdrop' size:small -->
```html
<script type="module">
Expand Down Expand Up @@ -44,8 +46,76 @@ Elements with `aria-hidden` applied (as well as their descendants) are completel

**When hiding a backdrop**: first remove the `shown` attribute on the backdrop, then if appropriate move focus outside the target.

## Accessibility
### Accessibility

The dialog backdrop hides background elements from screen reader users by setting `aria-hidden="true"` on all elements other than the target element specified in `for-target`.

The backdrop hides background elements from screen reader users by setting `aria-hidden="true"` on all elements other than the target element specified in `for-target`.
Before showing the backdrop, focus should be moved inside the target element — see [Focus Management](#focus-management) for more details.

Before showing the backdrop, focus should be moved inside the target element — see [Focus Management](#focus-management) for more details.
## Loading Backdrop

The loading backdrop can be used to de-emphasize an element's contents while they're being refreshed. It also displays a loading spinner to indicate that new contents will be populated without any further user input.

<!-- docs: demo code properties name:d2l-loading-backdrop sandboxTitle:'Loading Backdrop' size:medium -->
```html
<script type="module">
import '@brightspace-ui/core/components/button/button.js';
import '@brightspace-ui/core/components/backdrop/loading-backdrop.js';
import '@brightspace-ui/core/components/loading-spinner/loading-spinner.js';

const loadingBackdrop = document.querySelector('d2l-loading-backdrop');
document.querySelector('#target > d2l-button').addEventListener('click', () => {
loadingBackdrop.shown = !loadingBackdrop.shown;

setTimeout(() => {
document.querySelectorAll('.grade').forEach((grade) => {
grade.innerHTML = `${Math.round(Math.random() * 100).toString()}%`;
})
loadingBackdrop.shown = !loadingBackdrop.shown;
}, 5000)
});

</script>
<style>
table {
width: 100%
background-color: #b4e0bf;
}
th, td {
padding: 30px;
border-bottom: 1px solid #ddd;
text-align: left;
}
#grade-container {
position: relative;
}
</style>
<div id="target"><d2l-button primary>Refresh Content</d2l-button></div>
<div id="grade-container">
<table>
<thead>
<th>Course</th>
<th>Grade</th>
<th>Hours Spent in Content</th>
</thead>
<tr>
<td>Math</td>
<td class="grade">85%</td>
<td>100</td>
</tr>
<tr>
<td>Art</td>
<td class="grade">98%</td>
<td>10</td>
</tr>
</table>
<d2l-loading-backdrop></d2l-loading-backdrop>
</div>
```
<!-- docs: start hidden content -->
### Properties:

| Property | Type | Description |
|--|--|--|
| `shown` | Boolean | Used to control whether the loading backdrop is shown |
<!-- docs: end hidden content -->
131 changes: 131 additions & 0 deletions components/backdrop/loading-backdrop.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I foresee us wanting to use this elsewhere, for example in d2l-list. I'd be inclined to consider making this more general. It might make sense to define this as a different type of backdrop alongside d2l-backdrop. Alternatively enhancing d2l-backdrop for this case, but making this a separate component helps avoid the conditional logic and behaviour since there are a few things d2l-backdrop does that this doesn't.

It could be defined alongside the CollectionMixin since both d2l-table-wrapper and d2l-list extend that, but unfortunately that currently resides outside of components.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense- I'll take a pass to generalize and we can figure out the name/location from there.

Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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 = 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 = 200;

/**
* A component for displaying a semi-transparent backdrop and a loading spinner over the containing element
*/
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 },
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following a similar pattern to backdrop.js here using a string state

};
}

static get styles() {
return css`
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discussed leveraging some styles from backdrop.js by changing that module to export some styles, similar to how we export tableStyles, but ultimately found that there wasn't all that much overlap aside from the color and opacity. I didn't feel it was worth the indirection- but open to opinions here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point in the near future we will be replacing the color with a more meaningful CSS variable that they both can use.

:host, .backdrop, d2l-loading-spinner {
width: 0%;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A learning point for me- when using transitions, I can't rely on display: none to keep the table interactive while this isn't on, because going from display:none; opacity: 0 to display:block; opacity: 1 doesn't count as a change in opacity, as the display:none version didn't render at all. Bit of a hassle for fade-in components like these.

This approach to change the width instead matches the implementation in backdrop.js.

height: 0%;
position: absolute;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the d2l-table-wrapper the containing block for this? i.e. it is only rendering the backdrop over the table?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No- the containing block is the scroll wrapper

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But visually yes, it only renders the backdrop over the table and leaves the rest of the page interactive

}

.backdrop, d2l-loading-spinner {
opacity: 0;
}

:host {
z-index: 999;
top: 0px;
}

.backdrop {
background-color: var(--d2l-color-regolith);
}

d2l-loading-spinner {
top: 100px;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some design input is needed here- I'm not sure exactly how we want to quantify the vertical positioning of the spinner relative to the table. 100px was my 'looks fine to me' value, but maybe there's something more clever relative to the size of the viewport or something 😄

}

: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%;
Comment on lines +58 to +59
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the width/height here is how we ensure that our components are shown- also, while these are true, the table below is non-interactive (can't highlight, etc), because these render on top.

}

d2l-loading-spinner[_state="showing"] {
opacity: 1;
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 ${BACKDROP_DELAY_MS}ms;
}

d2l-loading-spinner[_state="hiding"] {
transition: opacity ${SPINNER_FADE_OUT_DURATION_MS}ms ease-out;
}

.backdrop[_state="hiding"] {
transition: opacity ${BACKDROP_FADE_OUT_DURATION_MS}ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
* { transition: none}
}
Comment on lines +80 to +82
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may need some guidance on these animation preferences before shipping this- I'm not sure exactly what I need to be responsive to

`;
}

constructor() {
super();
this.shown = false;
this._state = null;
}

render() {
return html`
<div class="backdrop" _state=${this._state}></div>
<d2l-loading-spinner _state=${this._state}></d2l-loading-spinner>
`;
}

willUpdate(changedProperties) {
if (changedProperties.has('shown')) {
if (this.shown) {
this._state = 'showing';
} else if (changedProperties.get('shown') !== undefined) {
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 });
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Listening to the transition cancel here deals with the case where the fade-in animation wasn't complete before we started the fade-out.

If we just listen for transitionend, the promise will never enqueue in that scenario, because after the fade-in gets cancelled the fade-out is never enqueued because we're already 'faded out' in terms of opacity.

This addition receives the cancel from the fade-in animation and clears the loading state accordingly

})
]).then(() => {
this._state = null;
});
}

}

customElements.define('d2l-loading-backdrop', LoadingBackdrop);
12 changes: 11 additions & 1 deletion components/table/table-wrapper.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '../colors/colors.js';
import '../scroll-wrapper/scroll-wrapper.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';
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -313,6 +315,10 @@ export class TableWrapper extends PageableMixin(SelectionMixin(LitElement)) {
reflect: true,
type: Boolean,
},
loading: {
reflect: true,
type: Boolean
},
};
}

Expand Down Expand Up @@ -382,6 +388,7 @@ export class TableWrapper extends PageableMixin(SelectionMixin(LitElement)) {
this._tableIntersectionObserver = null;
this._tableMutationObserver = null;
this._tableScrollers = {};
this._loading = false;
}

connectedCallback() {
Expand Down Expand Up @@ -410,7 +417,10 @@ export class TableWrapper extends PageableMixin(SelectionMixin(LitElement)) {
}

render() {
const slot = html`<slot @slotchange="${this._handleSlotChange}"></slot>`;
const slot = html`
<slot @slotchange="${this._handleSlotChange}"></slot>
<d2l-loading-backdrop ?shown=${this.loading}></d2l-loading-backdrop>
`;
const useScrollWrapper = this.stickyHeadersScrollWrapper || !this.stickyHeaders;
return html`
<slot name="controls" @slotchange="${this._handleControlsSlotChange}"></slot>
Expand Down
Loading