@brightspace-ui/core 2.76.3 → 2.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/components/button/button-icon.js +1 -0
  2. package/components/inputs/demo/input-search.html +8 -0
  3. package/components/inputs/docs/input-search.md +1 -0
  4. package/components/inputs/input-search.js +18 -0
  5. package/components/list/README.md +67 -0
  6. package/components/list/demo/demo-list-nested-lazy-load.js +133 -0
  7. package/components/list/demo/demo-list-nested.js +279 -0
  8. package/components/list/demo/list-demo-scenarios.js +321 -0
  9. package/components/list/demo/list-drag-and-drop.html +2 -2
  10. package/components/list/demo/list-expand-collapse.html +134 -0
  11. package/components/list/list-item-checkbox-mixin.js +2 -8
  12. package/components/list/list-item-drag-drop-mixin.js +78 -14
  13. package/components/list/list-item-drag-image.js +5 -3
  14. package/components/list/list-item-expand-collapse-mixin.js +168 -0
  15. package/components/list/list-item-generic-layout.js +21 -12
  16. package/components/list/list-item-mixin.js +88 -11
  17. package/components/list/list.js +45 -9
  18. package/components/selection/selection-summary.js +43 -12
  19. package/custom-elements.json +399 -194
  20. package/lang/ar.js +1 -0
  21. package/lang/cy.js +1 -0
  22. package/lang/da.js +1 -0
  23. package/lang/de.js +1 -0
  24. package/lang/en.js +1 -0
  25. package/lang/es-es.js +1 -0
  26. package/lang/es.js +1 -0
  27. package/lang/fr-fr.js +1 -0
  28. package/lang/fr.js +1 -0
  29. package/lang/hi.js +1 -0
  30. package/lang/ja.js +1 -0
  31. package/lang/ko.js +1 -0
  32. package/lang/nl.js +1 -0
  33. package/lang/pt.js +1 -0
  34. package/lang/sv.js +1 -0
  35. package/lang/tr.js +1 -0
  36. package/lang/zh-cn.js +1 -0
  37. package/lang/zh-tw.js +1 -0
  38. package/package.json +1 -1
  39. package/components/list/demo/list-drag-and-drop.js +0 -181
@@ -3,17 +3,19 @@ import '../inputs/input-checkbox.js';
3
3
  import { css, html, LitElement } from 'lit';
4
4
  import { bodySmallStyles } from '../typography/styles.js';
5
5
  import { formatNumber } from '@brightspace-ui/intl/lib/number.js';
6
+ import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
6
7
  import { RtlMixin } from '../../mixins/rtl-mixin.js';
7
8
  import { SkeletonMixin } from '../skeleton/skeleton-mixin.js';
8
9
 
9
- class ListItemDragImage extends SkeletonMixin(RtlMixin(LitElement)) {
10
+ class ListItemDragImage extends LocalizeCoreElement(SkeletonMixin(RtlMixin(LitElement))) {
10
11
 
11
12
  static get properties() {
12
13
  return {
13
14
  /**
14
15
  * @ignore
15
16
  */
16
- count: { type: Number }
17
+ count: { type: Number },
18
+ includePlusSign: { type: Boolean, attribute: 'include-plus-sign' }
17
19
  };
18
20
  }
19
21
 
@@ -103,7 +105,7 @@ class ListItemDragImage extends SkeletonMixin(RtlMixin(LitElement)) {
103
105
  render() {
104
106
  return html`
105
107
  <div class="first">
106
- <div class="count d2l-body-small">${formatNumber(this.count)}</div>
108
+ <div class="count d2l-body-small">${this.includePlusSign ? this.localize('components.count-badge.plus', { number: this.count }) : formatNumber(this.count)}</div>
107
109
  <svg width="18" height="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
108
110
  <path fill="#494c4e" d="M8 16v1c0 .5-.4 1-1 1H6c-.6 0-1-.5-1-1v-1c0-.6.4-1 1-1h1c.6 0 1 .4 1 1M13 16v1c0 .5-.4 1-1 1h-1c-.6 0-1-.5-1-1v-1c0-.6.4-1 1-1h1c.6 0 1 .4 1 1M8 11v1c0 .6-.4 1-1 1H6c-.6 0-1-.4-1-1v-1c0-.6.4-1 1-1h1c.6 0 1 .4 1 1M13 11v1c0 .6-.4 1-1 1h-1c-.6 0-1-.4-1-1v-1c0-.6.4-1 1-1h1c.6 0 1 .4 1 1M8 6v1c0 .6-.4 1-1 1H6c-.6 0-1-.4-1-1V6c0-.6.4-1 1-1h1c.6 0 1 .4 1 1M13 6v1c0 .6-.4 1-1 1h-1c-.6 0-1-.4-1-1V6c0-.6.4-1 1-1h1c.6 0 1 .4 1 1M8 1v1c0 .6-.4 1-1 1H6c-.6 0-1-.4-1-1V1c0-.5.4-1 1-1h1c.6 0 1 .5 1 1M13 1v1c0 .6-.4 1-1 1h-1c-.6 0-1-.4-1-1V1c0-.5.4-1 1-1h1c.6 0 1 .5 1 1"/>
109
111
  </svg>
@@ -0,0 +1,168 @@
1
+ import '../button/button-icon.js';
2
+ import '../loading-spinner/loading-spinner.js';
3
+ import { css, html, nothing } from 'lit';
4
+ import { EventSubscriberController } from '../../controllers/subscriber/subscriberControllers.js';
5
+
6
+ const dragIntervalDelay = 100;
7
+ const dragHoverDropTime = 1000;
8
+
9
+ export const ListItemExpandCollapseMixin = superclass => class extends superclass {
10
+
11
+ static get properties() {
12
+ return {
13
+ /**
14
+ * Whether to show the expand collapse toggle
15
+ * @type {boolean}
16
+ */
17
+ expandable: { type: Boolean },
18
+ /**
19
+ * Default state for expand collapse toggle - if not set, collapsed will be the default state
20
+ * @type {boolean}
21
+ */
22
+ expanded: { type: Boolean, reflect: true },
23
+ _siblingHasNestedItems: { state: true },
24
+ _renderExpandCollapseSlot: { type: Boolean, reflect: true, attribute: '_render-expand-collapse-slot' },
25
+ _showNestedLoadingSpinner: { state: true }
26
+ };
27
+ }
28
+
29
+ static get styles() {
30
+ const styles = [ css`
31
+ :host {
32
+ --d2l-expand-collapse-slot-transition-duration: 0.3s;
33
+ }
34
+ .d2l-list-expand-collapse {
35
+ width: 0;
36
+ }
37
+ :host([dir="rtl"][_render-expand-collapse-slot]) .d2l-list-expand-collapse {
38
+ padding: 0.4rem 0 0 0.3rem;
39
+ }
40
+ .d2l-list-expand-collapse d2l-button-icon {
41
+ --d2l-button-icon-min-height: 1.2rem;
42
+ --d2l-button-icon-min-width: 1.2rem;
43
+ }
44
+ .d2l-list-expand-collapse:hover d2l-button-icon {
45
+ background-color: var(--d2l-button-icon-background-color-hover);
46
+ border-radius: var(--d2l-button-icon-border-radius);
47
+ }
48
+ :host([_render-expand-collapse-slot]) .d2l-list-expand-collapse {
49
+ padding: 0.4rem 0.3rem 0 0;
50
+ width: 1.2rem;
51
+ }
52
+ .d2l-list-expand-collapse-action {
53
+ cursor: pointer;
54
+ display: block;
55
+ height: 100%;
56
+ }
57
+ .d2l-list-nested-loading {
58
+ display: flex;
59
+ justify-content: center;
60
+ padding: 0.4rem;
61
+ }
62
+
63
+ @media (prefers-reduced-motion: no-preference) {
64
+ .d2l-list-expand-collapse {
65
+ transition: width var(--d2l-expand-collapse-slot-transition-duration) cubic-bezier(0, 0.7, 0.5, 1);
66
+ }
67
+ }
68
+ ` ];
69
+
70
+ super.styles && styles.unshift(super.styles);
71
+ return styles;
72
+ }
73
+
74
+ constructor() {
75
+ super();
76
+ this._siblingHasNestedItems = false;
77
+ this._renderExpandCollapseSlot = false;
78
+ this._showNestedLoadingSpinner = false;
79
+ this._parentChildUpdateSubscription = new EventSubscriberController(this, {}, { eventName: 'd2l-list-child-status' });
80
+ }
81
+
82
+ connectedCallback() {
83
+ super.connectedCallback();
84
+ // mixin requires key for events
85
+ if (!this.key && this.expandable) {
86
+ console.warn('ListItemExpandCollapseMixin requires a key.');
87
+ this.expandable = false;
88
+ }
89
+ }
90
+
91
+ updated(changedProperties) {
92
+ if (changedProperties.has('_siblingHasNestedItems') || changedProperties.has('expandable')) {
93
+ this._renderExpandCollapseSlot = this.expandable || this._siblingHasNestedItems;
94
+ }
95
+ if (changedProperties.has('_draggingOver') && this._draggingOver && this.dropNested && !this.expanded && this.expandable) {
96
+ let elapsedHoverTime = 0;
97
+ let dragIntervalId = null;
98
+ const watchDraggingOver = () => {
99
+ if (elapsedHoverTime >= dragHoverDropTime) {
100
+ if (this._draggingOver) {
101
+ this._toggleExpandCollapse();
102
+ }
103
+ clearInterval(dragIntervalId);
104
+ } else {
105
+ if (!this._draggingOver) {
106
+ clearInterval(dragIntervalId);
107
+ } else {
108
+ elapsedHoverTime += dragIntervalDelay;
109
+ }
110
+ }
111
+ };
112
+ // check if they are still hovered over same item every 100ms
113
+ dragIntervalId = setInterval(watchDraggingOver, dragIntervalDelay);
114
+ }
115
+ if (changedProperties.has('expanded') || changedProperties.has('_hasNestedList')) {
116
+ this._showNestedLoadingSpinner = this.expanded && !this._hasNestedList;
117
+ }
118
+ }
119
+
120
+ updateSiblingHasChildren(siblingHasNestedItems) {
121
+ this._siblingHasNestedItems = siblingHasNestedItems;
122
+ }
123
+
124
+ _renderExpandCollapse() {
125
+ if (!this.expandable) {
126
+ return nothing;
127
+ }
128
+ return html`
129
+ <d2l-button-icon
130
+ icon="${this.expanded ? 'tier1:arrow-collapse-small' : 'tier1:arrow-expand-small' }"
131
+ aria-expanded="${this.expanded ? 'true' : 'false'}"
132
+ text="${this.label}"
133
+ @click="${this._toggleExpandCollapse}"></d2l-button-icon>`;
134
+ }
135
+
136
+ _renderExpandCollapseAction() {
137
+ if (this.selectable || !this.expandable || this.noPrimaryAction) {
138
+ return nothing;
139
+ }
140
+
141
+ return html`<div class="d2l-list-expand-collapse-action" @click="${this._toggleExpandCollapse}"></div>`;
142
+ }
143
+
144
+ _renderNestedLoadingSpinner() {
145
+ if (!this.expandable || !this._showNestedLoadingSpinner) {
146
+ return nothing;
147
+ }
148
+ return html`
149
+ <div class="d2l-list-nested-loading">
150
+ <d2l-loading-spinner size="40"></d2l-loading-spinner>
151
+ </div>`;
152
+ }
153
+
154
+ _toggleExpandCollapse(e = null) {
155
+ if (e) {
156
+ e.stopPropagation();
157
+ }
158
+ if (!this.expandable) {
159
+ return;
160
+ }
161
+ this.expanded = !this.expanded;
162
+ /** Dispatched whenever the list item expand state is toggled. */
163
+ this.dispatchEvent(new CustomEvent('d2l-list-item-expand-collapse-toggled', {
164
+ composed: true,
165
+ bubbles: true
166
+ }));
167
+ }
168
+ };
@@ -62,7 +62,8 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
62
62
  display: grid;
63
63
  grid-template-columns:
64
64
  [start outside-control-start] minmax(0, min-content)
65
- [control-start outside-control-end] minmax(0, min-content)
65
+ [expand-collapse-start outside-control-end] minmax(0, min-content)
66
+ [control-start expand-collapse-end] minmax(0, min-content)
66
67
  [control-end content-start] minmax(0, auto)
67
68
  [content-end actions-start] minmax(0, min-content)
68
69
  [end actions-end];
@@ -79,7 +80,7 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
79
80
  grid-column: control-start / end;
80
81
  }
81
82
  :host(.d2l-dragging-over) ::slotted([slot="nested"]) {
82
- z-index: 6; /* must be greater than item's drop-target to allow dropping onto items within nested list */
83
+ z-index: 7; /* must be greater than item's drop-target to allow dropping onto items within nested list */
83
84
  }
84
85
 
85
86
  ::slotted([slot="drop-target"]) {
@@ -87,10 +88,11 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
87
88
  position: absolute;
88
89
  top: 0;
89
90
  width: 100%;
90
- z-index: 5;
91
+ z-index: 6;
91
92
  }
92
93
 
93
94
  ::slotted([slot="outside-control"]),
95
+ ::slotted([slot="expand-collapse"]),
94
96
  ::slotted([slot="control"]),
95
97
  ::slotted([slot="content"]),
96
98
  ::slotted([slot="actions"]) {
@@ -98,7 +100,13 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
98
100
  }
99
101
  ::slotted([slot="outside-control"]) {
100
102
  grid-column: outside-control-start / outside-control-end;
101
- width: 2.2rem;
103
+ width: fit-content;
104
+ }
105
+
106
+ ::slotted([slot="expand-collapse"]) {
107
+ cursor: pointer;
108
+ grid-column: expand-collapse-start / expand-collapse-end;
109
+ z-index: 2;
102
110
  }
103
111
 
104
112
  ::slotted([slot="control"]) {
@@ -117,7 +125,7 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
117
125
  ::slotted([slot="actions"]) {
118
126
  grid-column: actions-start / actions-end;
119
127
  justify-self: end;
120
- z-index: 4;
128
+ z-index: 5;
121
129
  }
122
130
 
123
131
  ::slotted([slot="outside-control-action"]),
@@ -136,14 +144,14 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
136
144
  grid-column: control-start / end;
137
145
  height: 100%;
138
146
  width: 100%;
139
- z-index: 2;
147
+ z-index: 3;
140
148
  }
141
149
  :host([no-primary-action]) ::slotted([slot="control-action"]) {
142
150
  grid-column: control-start / control-end;
143
151
  }
144
152
  ::slotted([slot="content-action"]) {
145
153
  grid-column: content-start / end;
146
- z-index: 3;
154
+ z-index: 4;
147
155
  }
148
156
  :host([no-primary-action]) ::slotted([slot="content-action"]) {
149
157
  display: none;
@@ -154,7 +162,7 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
154
162
  grid-row: 1 / 2;
155
163
  }
156
164
  ::slotted([slot="control-container"]) {
157
- grid-column: control-start / end;
165
+ grid-column: expand-collapse-start / end;
158
166
  grid-row: 1 / 2;
159
167
  }
160
168
  `;
@@ -192,13 +200,14 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
192
200
  <slot name="control-container"></slot>
193
201
  <slot name="outside-control-container"></slot>
194
202
  <slot name="drop-target"></slot>
195
- <slot name="content-action" class="d2l-cell" data-cell-num="5"></slot>
203
+ <slot name="content-action" class="d2l-cell" data-cell-num="6"></slot>
196
204
  <slot name="outside-control-action" class="d2l-cell" data-cell-num="1"></slot>
197
205
  <slot name="outside-control" class="d2l-cell" data-cell-num="2"></slot>
198
206
  <slot name="control-action" class="d2l-cell" data-cell-num="3"></slot>
199
- <slot name="control" class="d2l-cell" data-cell-num="4"></slot>
200
- <slot name="actions" class="d2l-cell" data-cell-num="6"></slot>
201
- <slot name="content" class="d2l-cell" data-cell-num="7" @focus="${!this.noPrimaryAction ? this._preventFocus : null}"></slot>
207
+ <slot name="expand-collapse" class="d2l-cell" data-cell-num="4"></slot>
208
+ <slot name="control" class="d2l-cell" data-cell-num="5"></slot>
209
+ <slot name="actions" class="d2l-cell" data-cell-num="7"></slot>
210
+ <slot name="content" class="d2l-cell" data-cell-num="8" @focus="${!this.noPrimaryAction ? this._preventFocus : null}"></slot>
202
211
  <slot name="nested"></slot>
203
212
  `;
204
213
  }
@@ -2,14 +2,18 @@ import '../colors/colors.js';
2
2
  import './list-item-generic-layout.js';
3
3
  import './list-item-placement-marker.js';
4
4
  import '../tooltip/tooltip.js';
5
+ import '../expand-collapse/expand-collapse-content.js';
5
6
  import { css, html, nothing } from 'lit';
6
7
  import { findComposedAncestor, getComposedParent } from '../../helpers/dom.js';
7
8
  import { classMap } from 'lit/directives/class-map.js';
9
+ import { composeMixins } from '../../helpers/composeMixins.js';
8
10
  import { getFirstFocusableDescendant } from '../../helpers/focus.js';
9
11
  import { getUniqueId } from '../../helpers/uniqueId.js';
10
12
  import { ifDefined } from 'lit/directives/if-defined.js';
13
+ import { LabelledMixin } from '../../mixins/labelled-mixin.js';
11
14
  import { ListItemCheckboxMixin } from './list-item-checkbox-mixin.js';
12
15
  import { ListItemDragDropMixin } from './list-item-drag-drop-mixin.js';
16
+ import { ListItemExpandCollapseMixin } from './list-item-expand-collapse-mixin.js';
13
17
  import { ListItemRoleMixin } from './list-item-role-mixin.js';
14
18
  import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
15
19
  import ResizeObserver from 'resize-observer-polyfill';
@@ -43,7 +47,18 @@ const ro = new ResizeObserver(entries => {
43
47
 
44
48
  const defaultBreakpoints = [842, 636, 580, 0];
45
49
 
46
- export const ListItemMixin = superclass => class extends LocalizeCoreElement(ListItemDragDropMixin(ListItemCheckboxMixin(ListItemRoleMixin(RtlMixin(superclass))))) {
50
+ /**
51
+ * @property label - The hidden label for the checkbox and expand collapse control
52
+ */
53
+ export const ListItemMixin = superclass => class extends composeMixins(
54
+ superclass,
55
+ LabelledMixin,
56
+ LocalizeCoreElement,
57
+ ListItemExpandCollapseMixin,
58
+ ListItemDragDropMixin,
59
+ ListItemCheckboxMixin,
60
+ ListItemRoleMixin,
61
+ RtlMixin) {
47
62
 
48
63
  static get properties() {
49
64
  return {
@@ -77,7 +92,8 @@ export const ListItemMixin = superclass => class extends LocalizeCoreElement(Lis
77
92
  _focusingPrimaryAction: { type: Boolean, attribute: '_focusing-primary-action', reflect: true },
78
93
  _highlight: { type: Boolean, reflect: true },
79
94
  _highlighting: { type: Boolean, reflect: true },
80
- _tooltipShowing: { type: Boolean, attribute: '_tooltip-showing', reflect: true }
95
+ _tooltipShowing: { type: Boolean, attribute: '_tooltip-showing', reflect: true },
96
+ _hasNestedList: { state: true }
81
97
  };
82
98
  }
83
99
 
@@ -262,14 +278,16 @@ export const ListItemMixin = superclass => class extends LocalizeCoreElement(Lis
262
278
  margin-left: 1rem;
263
279
  margin-right: 0;
264
280
  }
281
+
265
282
  d2l-selection-input {
266
- margin: 0.55rem 0.9rem 0.55rem 0;
283
+ margin: 0.55rem 0.55rem 0.55rem 0;
267
284
  }
268
285
  .d2l-list-item-content-extend-separators d2l-selection-input {
269
286
  margin-left: 0.9rem;
270
287
  }
288
+
271
289
  d2l-list-item-drag-handle {
272
- margin: 0.25rem 0 0.25rem 0.4rem;
290
+ margin: 0.25rem 0.3rem;
273
291
  }
274
292
  :host([dir="rtl"]) d2l-selection-input {
275
293
  margin-left: 0.9rem;
@@ -356,6 +374,7 @@ export const ListItemMixin = superclass => class extends LocalizeCoreElement(Lis
356
374
  this._displayKeyboardTooltip = false;
357
375
  this._fullscreenWithin = false;
358
376
  this._fullscreenWithinCount = 0;
377
+ this._hasNestedList = false;
359
378
  }
360
379
 
361
380
  get breakpoints() {
@@ -375,6 +394,9 @@ export const ListItemMixin = superclass => class extends LocalizeCoreElement(Lis
375
394
  if (this.role === 'rowgroup') {
376
395
  addTabListener();
377
396
  }
397
+ if (!this.selectable && !this.expandable) {
398
+ this.labelRequired = false;
399
+ }
378
400
  }
379
401
 
380
402
  disconnectedCallback() {
@@ -439,6 +461,30 @@ export const ListItemMixin = superclass => class extends LocalizeCoreElement(Lis
439
461
  else this.scrollIntoView({ behavior: 'smooth', block: alignToTop ? 'start' : 'end' });
440
462
  }
441
463
 
464
+ _getFlattenedListItems(listItem) {
465
+ const listItems = new Map();
466
+ const lazyLoadListItems = new Map();
467
+ this._getListItems(listItems, lazyLoadListItems, listItem);
468
+ return { listItems, lazyLoadListItems };
469
+ }
470
+
471
+ _getListItems(listItems, lazyLoadListItems, listItem) {
472
+ if (!listItem) {
473
+ const rootList = this._getRootList();
474
+ const rootListItems = rootList.getItems();
475
+ rootListItems.forEach(listItem => this._getListItems(listItems, lazyLoadListItems, listItem));
476
+ } else {
477
+ listItems.set(listItem.key, listItem);
478
+ if (listItem.expandable && !listItem._hasNestedList) {
479
+ lazyLoadListItems.set(listItem.key, listItem);
480
+ }
481
+ if (listItem._hasNestedList) {
482
+ const nestedList = listItem._getNestedList();
483
+ nestedList.getItems().forEach(listItem => this._getListItems(listItems, lazyLoadListItems, listItem));
484
+ }
485
+ }
486
+ }
487
+
442
488
  _getNestedList() {
443
489
  if (!this.shadowRoot) return;
444
490
  const nestedSlot = this.shadowRoot.querySelector('slot[name="nested"]');
@@ -458,6 +504,16 @@ export const ListItemMixin = superclass => class extends LocalizeCoreElement(Lis
458
504
  }
459
505
  }
460
506
 
507
+ _getParentList(node) {
508
+ if (!node) node = this;
509
+ let parentList;
510
+ while (parentList?.tagName !== 'D2L-LIST') {
511
+ node = getComposedParent(node);
512
+ if (node.tagName === 'D2L-LIST') parentList = node;
513
+ }
514
+ return parentList;
515
+ }
516
+
461
517
  _getParentListItem() {
462
518
  const parentListItem = findComposedAncestor(this.parentNode, node => this._isListItem(node));
463
519
  return parentListItem;
@@ -530,6 +586,18 @@ export const ListItemMixin = superclass => class extends LocalizeCoreElement(Lis
530
586
  this._hovering = false;
531
587
  }
532
588
 
589
+ _onNestedSlotChange() {
590
+ if (this.selectable) {
591
+ this._onNestedSlotChangeCheckboxMixin();
592
+ }
593
+ const nestedList = this._getNestedList();
594
+ if (this._hasNestedList !== !!nestedList) {
595
+ this._hasNestedList = !!nestedList;
596
+ /** @ignore */
597
+ this.dispatchEvent(new CustomEvent('d2l-list-item-nested-change', { bubbles: true, composed: true }));
598
+ }
599
+ }
600
+
533
601
  _renderListItem({ illustration, content, actions, nested } = {}) {
534
602
  const classes = {
535
603
  'd2l-visible-on-ancestor-target': true,
@@ -539,7 +607,6 @@ export const ListItemMixin = superclass => class extends LocalizeCoreElement(Lis
539
607
 
540
608
  const primaryAction = ((!this.noPrimaryAction && this._renderPrimaryAction) ? this._renderPrimaryAction(this._contentId) : null);
541
609
  const tooltipForId = (primaryAction ? this._primaryActionId : (this.selectable ? this._checkboxId : null));
542
-
543
610
  const innerView = html`
544
611
  <d2l-list-item-generic-layout
545
612
  align-nested="${ifDefined(this.draggable && this.selectable ? 'control' : undefined)}"
@@ -556,12 +623,15 @@ export const ListItemMixin = superclass => class extends LocalizeCoreElement(Lis
556
623
  ${this._renderDragHandle(this._renderOutsideControl)}
557
624
  ${this._renderDragTarget(this.dragTargetHandleOnly ? this._renderOutsideControlHandleOnly : this._renderOutsideControlAction)}
558
625
  <div slot="control-container"></div>
559
- ${this.selectable ? html`
560
- <div slot="control">${this._renderCheckbox()}</div>
561
- <div slot="control-action"
626
+ <div slot="expand-collapse" class="d2l-list-expand-collapse" @click="${this._toggleExpandCollapse}">
627
+ ${this._renderExpandCollapse()}
628
+ </div>
629
+ ${this.selectable ? html`<div slot="control">${this._renderCheckbox()}</div>` : nothing}
630
+ ${this.selectable || this.expandable ? html`<div slot="control-action"
562
631
  @mouseenter="${this._onMouseEnter}"
563
632
  @mouseleave="${this._onMouseLeave}">
564
633
  ${this._renderCheckboxAction('')}
634
+ ${this._renderExpandCollapseAction()}
565
635
  </div>` : nothing }
566
636
  ${primaryAction ? html`
567
637
  <div slot="content-action"
@@ -583,9 +653,7 @@ export const ListItemMixin = superclass => class extends LocalizeCoreElement(Lis
583
653
  class="d2l-list-item-actions-container">
584
654
  <slot name="actions" class="d2l-list-item-actions">${actions}</slot>
585
655
  </div>
586
- <div slot="nested" @d2l-selection-provider-connected="${this._onSelectionProviderConnected}">
587
- <slot name="nested" @slotchange="${this._onNestedSlotChange}">${nested}</slot>
588
- </div>
656
+ ${this._renderNested(nested)}
589
657
  </d2l-list-item-generic-layout>
590
658
  `;
591
659
 
@@ -598,6 +666,15 @@ export const ListItemMixin = superclass => class extends LocalizeCoreElement(Lis
598
666
 
599
667
  }
600
668
 
669
+ _renderNested(nested) {
670
+ const nestedSlot = html`<slot name="nested" @slotchange="${this._onNestedSlotChange}">${nested}</slot>`;
671
+ return html`
672
+ <div slot="nested" @d2l-selection-provider-connected="${this._onSelectionProviderConnected}">
673
+ ${this.expandable ? html`<d2l-expand-collapse-content ?expanded="${this.expanded}">${this._renderNestedLoadingSpinner()}${nestedSlot}</d2l-expand-collapse-content>` : nestedSlot}
674
+ </div>
675
+ `;
676
+ }
677
+
601
678
  _renderOutsideControl(dragHandle) {
602
679
  return html`<div slot="outside-control">${dragHandle}</div>`;
603
680
  }
@@ -2,6 +2,7 @@ import { css, html, LitElement } from 'lit';
2
2
  import { getNextFocusable, getPreviousFocusable } from '../../helpers/focus.js';
3
3
  import { SelectionInfo, SelectionMixin } from '../selection/selection-mixin.js';
4
4
  import { PageableMixin } from '../paging/pageable-mixin.js';
5
+ import { SubscriberRegistryController } from '../../controllers/subscriber/subscriberControllers.js';
5
6
 
6
7
  const keyCodes = {
7
8
  TAB: 9
@@ -66,11 +67,19 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
66
67
  this._itemsShowingCount = 0;
67
68
  this._itemsShowingTotalCount = 0;
68
69
  this._listItemChanges = [];
70
+ this._childHasExpandCollapseToggle = false;
71
+
72
+ this._listChildrenUpdatedSubscribers = new SubscriberRegistryController(
73
+ this,
74
+ { onSubscribe: this._updateActiveSubscriber.bind(this), updateSubscribers: this._updateActiveSubscribers.bind(this) },
75
+ { eventName: 'd2l-list-child-status' }
76
+ );
69
77
  }
70
78
 
71
79
  connectedCallback() {
72
80
  super.connectedCallback();
73
81
  this.addEventListener('d2l-list-items-showing-count-change', this._handleListItemsShowingCountChange);
82
+ this.addEventListener('d2l-list-item-nested-change', (e) => this._handleListIemNestedChange(e));
74
83
  }
75
84
 
76
85
  disconnectedCallback() {
@@ -80,7 +89,8 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
80
89
 
81
90
  firstUpdated(changedProperties) {
82
91
  super.firstUpdated(changedProperties);
83
-
92
+ // check if list items are expandable on first render so we adjust sibling spacing appropriately
93
+ this._handleListIemNestedChange();
84
94
  this.addEventListener('d2l-list-item-selected', e => {
85
95
 
86
96
  // batch the changes from select-all and nested lists
@@ -174,6 +184,10 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
174
184
  return new SelectionInfo(keys, selectionInfo.state);
175
185
  }
176
186
 
187
+ getSubscriberController() {
188
+ return this._listChildrenUpdatedSubscribers;
189
+ }
190
+
177
191
  _getItemByIndex(index) {
178
192
  const items = this.getItems();
179
193
  if (index > items.length - 1) return;
@@ -189,16 +203,14 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
189
203
  return this._itemsShowingCount - 1;
190
204
  }
191
205
 
206
+ _getLazyLoadItems() {
207
+ const items = this.getItems();
208
+ return items.length > 0 ? items[0]._getFlattenedListItems().lazyLoadListItems : new Map();
209
+ }
210
+
192
211
  async _getListItemsShowingTotalCount(refresh) {
193
212
  if (refresh) {
194
- this._itemsShowingTotalCount = await this.getItems().reduce(async(count, item) => {
195
- await item.updateComplete;
196
- if (item._selectionProvider) {
197
- return (await count + await item._selectionProvider._getListItemsShowingTotalCount(true));
198
- } else {
199
- return await count;
200
- }
201
- }, this._itemsShowingCount);
213
+ this._itemsShowingTotalCount = this.getItems().length;
202
214
  }
203
215
  return this._itemsShowingTotalCount;
204
216
  }
@@ -212,6 +224,22 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
212
224
  if (focusable) focusable.focus();
213
225
  }
214
226
 
227
+ _handleListIemNestedChange(e) {
228
+ if (e) {
229
+ e.stopPropagation();
230
+ }
231
+ const items = this.getItems();
232
+ let aChildHasToggleEnabled = false;
233
+ for (const item of items) {
234
+ if (item.expandable) {
235
+ aChildHasToggleEnabled = true;
236
+ break;
237
+ }
238
+ }
239
+ this._childHasExpandCollapseToggle = aChildHasToggleEnabled;
240
+ this._listChildrenUpdatedSubscribers.updateSubscribers();
241
+ }
242
+
215
243
  _handleListItemsShowingCountChange() {
216
244
  if (this.slot === 'nested') return;
217
245
 
@@ -242,6 +270,14 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
242
270
  }));
243
271
  }
244
272
 
273
+ _updateActiveSubscriber(subscriber) {
274
+ subscriber.updateSiblingHasChildren(this._childHasExpandCollapseToggle);
275
+ }
276
+
277
+ _updateActiveSubscribers(subscribers) {
278
+ subscribers.forEach(subscriber => subscriber.updateSiblingHasChildren(this._childHasExpandCollapseToggle));
279
+ }
280
+
245
281
  }
246
282
 
247
283
  customElements.define('d2l-list', List);
@@ -1,4 +1,4 @@
1
- import { css, html, LitElement } from 'lit';
1
+ import { css, html, LitElement, nothing } from 'lit';
2
2
  import { bodyCompactStyles } from '../typography/styles.js';
3
3
  import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
4
4
  import { SelectionInfo } from './selection-mixin.js';
@@ -35,25 +35,56 @@ class Summary extends LocalizeCoreElement(SelectionObserverMixin(LitElement)) {
35
35
  }
36
36
 
37
37
  render() {
38
+ if (!this._summary) {
39
+ return nothing;
40
+ }
41
+ return html`
42
+ <p class="d2l-body-compact">${this._summary}</p>
43
+ `;
44
+ }
45
+
46
+ willUpdate(changedProperties) {
47
+ if (changedProperties.has('_provider') || changedProperties.has('selectionInfo')) {
48
+ this._updateSelectSummary();
49
+ }
50
+ }
51
+
52
+ _updateSelectSummary() {
38
53
  if (this._provider && this._provider.selectionSingle) return;
39
54
 
40
55
  let count;
41
- let summary;
42
56
  if (this._provider && this._provider.selectionCountOverride !== undefined) {
43
57
  count = this._provider.selectionCountOverride;
44
- summary = (this._provider.selectionCountOverride === 0 && this.noSelectionText ?
45
- this.noSelectionText : this.localize('components.selection.selected', 'count', count));
58
+ this._summary = this._provider.selectionCountOverride === 0 && this.noSelectionText ?
59
+ this.noSelectionText : this.localize('components.selection.selected', 'count', count);
46
60
  } else {
47
- count = (this.selectionInfo.state === SelectionInfo.states.allPages ?
48
- this._provider.itemCount : this.selectionInfo.keys.length);
61
+ /* If lazy loading items is supported (ex. d2l-list) then check the keys to determine if the plus sign should be included.
62
+ * If lazy loading is not supported (ex. d2l-table-wrapper), then skip this.
63
+ */
64
+ let includePlus = false;
65
+ if (this._provider && this._provider._getLazyLoadItems) {
66
+ const lazyLoadListItems = this._provider._getLazyLoadItems();
67
+ if (lazyLoadListItems.size > 0) {
68
+ for (const selectedItemKey of this.selectionInfo.keys) {
69
+ if (lazyLoadListItems.has(selectedItemKey)) {
70
+ includePlus = true;
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ }
49
76
 
50
- summary = (this.selectionInfo.state === SelectionInfo.states.none && this.noSelectionText ?
51
- this.noSelectionText : this.localize('components.selection.selected', 'count', count));
52
- }
77
+ count = this.selectionInfo.state === SelectionInfo.states.allPages ?
78
+ this._provider.itemCount : this.selectionInfo.keys.length;
53
79
 
54
- return html`
55
- <p class="d2l-body-compact">${summary}</p>
56
- `;
80
+ if (this.selectionInfo.state === SelectionInfo.states.none && this.noSelectionText) {
81
+ this._summary = this.noSelectionText;
82
+ } else if (includePlus) {
83
+ this._summary = this.localize('components.selection.selected-plus', 'count', count);
84
+ } else {
85
+ this._summary = this.localize('components.selection.selected', 'count', count);
86
+ }
87
+ }
57
88
  }
58
89
 
59
90
  }