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
43 changes: 41 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 @@ -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)}">
<d2l-list-controls slot="controls" select-all-pages-allowed>
<d2l-list-controls slot="controls" select-all-pages-allowed ?select-all-pages-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 +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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In this iteration, disabling controls is a responsibility assumed by the caller, since they're picking and choosing which controls to include in their slot.

The alternative would be to go programmatically disable everything within the control slot- this might not be desirable for all use cases, especially if those controls are part of a flow in which the loading backdrop would go away, E.G.

  • User clicks a checkbox in the list controls to only show students who are failing the course in a list.
  • Since this would require a non-trivial load time, the backdrop is triggered and the overlay appears
  • The user changes their mind, they actually want to keep the non-failing students in the list
  • If the list controls were all greyed out, they would have no choice but to click the refresh button, wait for it to load, then switch back to the selection they wanted

}
}
}

_handleAddItem() {
const newKey = getUniqueId();
this.items.push({
Expand All @@ -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(() => {
Expand All @@ -237,5 +271,10 @@ class DemoList extends LitElement {

}

_handleReload() {
this.dataState = 'loading';
setTimeout(() => { this.dataState = 'clean'; }, 2000);
}

}
customElements.define('d2l-demo-list', DemoList);
67 changes: 65 additions & 2 deletions 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 @@ -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 }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This state attribute is a defensive measure to avoid breaking or changing existing list users. It records if the backdrop feature has ever been used, which opts into the new styles below.

};
}

Expand Down Expand Up @@ -161,6 +197,19 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
flex-basis: 100%;
height: 0;
}

:host([_backdropTriggered]) {
position: relative;
}
Comment on lines +201 to +203

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The backdrop must be rendered within a stacking context so that it doesn't leak out and render over a parent element or the entire page.

:host([_backdropTriggered]) slot {
z-index: 2;
}
:host([_backdropTriggered]) d2l-backdrop-loading {
z-index: 1;
}
:host([_backdropTriggered]) #list-slot {
z-index: 0;
}
Comment on lines +204 to +212

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For most cases, I'd expect that we could ignore this by:

  • Only including the items that will be stacked on top of each other in the stacking context, by introducing a position:relative div that wraps around those specific elements
  • Rely on the element ordering to specify the stacking order

Unfortunately, adding the position:relative div around just those elements was causing a 1px discrepancy in height when combined with a negative margin (slack thread).

To avoid addressing that difficult issue, I've instead opted to make the list itself a stacking context, and manually specify an order that ensures that only the list items (in #list-slot) are overlaid by the backdrop.

`;
}

Expand All @@ -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)
Expand Down Expand Up @@ -250,12 +303,14 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
return html`
<slot name="controls"></slot>
<slot name="header"></slot>
<div role="${role}" aria-label="${ifDefined(ariaLabel)}" class="d2l-list-content">
<div id="list-slot" role="${role}" class="d2l-list-content" aria-label="${ifDefined(ariaLabel)}">
<slot @keydown="${this._handleKeyDown}" @slotchange="${this._handleSlotChange}"></slot>
</div>
<d2l-backdrop-loading @d2l-backdrop-dirty-overlay-action=${this._handleDirtyButton} for="list-slot" .dataState='${this.dataState}' dirty-text="${this.dirtyText}" dirty-button-text="${this.dirtyButtonText}"></d2l-backdrop-loading>
${this._renderPagerContainer()}
`;
}

willUpdate(changedProperties) {
super.willUpdate(changedProperties);
if (changedProperties.has('breakpoints') && changedProperties.get('breakpoints') !== undefined) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
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' },
/**
* Whether to disable and visually grey out the select all items checkbox
* @type {boolean}
*/
selectAllPagesDisabled: { type: Boolean, attribute: 'select-all-pages-disabled' },
Comment on lines 41 to +46

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Having both of these properties is not ideal since the difference between Allowed and Disabled isn't clear just in their names.

I also considered using disabled as the attribute name, but I thought this carried the implication that all of the slotted items would be disabled too, which is not the case by design.

Any ideas on how to make this nicer without breaking the existing API are welcome 😄

_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.selectAllPagesDisabled = 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.selectAllPagesDisabled} ></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