@brightspace-ui/core 2.104.2 → 2.106.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.
@@ -1,12 +1,14 @@
1
1
  import '../colors/colors.js';
2
2
  import '../icons/icon.js';
3
3
  import '../tooltip/tooltip.js';
4
- import { css, html, LitElement } from 'lit';
4
+ import { css, html, LitElement, unsafeCSS } from 'lit';
5
5
  import { buttonStyles } from './button-styles.js';
6
6
  import { FocusMixin } from '../../mixins/focus/focus-mixin.js';
7
+ import { getFocusPseudoClass } from '../../helpers/focus.js';
7
8
  import { getUniqueId } from '../../helpers/uniqueId.js';
8
9
  import { ifDefined } from 'lit/directives/if-defined.js';
9
10
  import { RtlMixin } from '../../mixins/rtl/rtl-mixin.js';
11
+ import { ThemeMixin } from '../../mixins/theme/theme-mixin.js';
10
12
 
11
13
  const keyCodes = Object.freeze({
12
14
  DOWN: 40,
@@ -31,7 +33,7 @@ export const moveActions = Object.freeze({
31
33
  /**
32
34
  * A button component that provides a move action via a single button.
33
35
  */
34
- class ButtonMove extends FocusMixin(RtlMixin(LitElement)) {
36
+ class ButtonMove extends ThemeMixin(FocusMixin(RtlMixin(LitElement))) {
35
37
 
36
38
  static get properties() {
37
39
  return {
@@ -45,15 +47,35 @@ class ButtonMove extends FocusMixin(RtlMixin(LitElement)) {
45
47
  */
46
48
  description: { type: String },
47
49
  /**
48
- * Disables the button
50
+ * Disables the down interaction
49
51
  * @type {boolean}
50
52
  */
51
- disabled: { type: Boolean, reflect: true },
53
+ disabledDown: { type: Boolean, attribute: 'disabled-down', reflect: true },
52
54
  /**
53
- * Tooltip text when disabled
54
- * @type {string}
55
+ * Disables the end interaction
56
+ * @type {boolean}
57
+ */
58
+ disabledEnd: { type: Boolean, attribute: 'disabled-end', reflect: true },
59
+ /**
60
+ * Disables the home interaction
61
+ * @type {boolean}
62
+ */
63
+ disabledHome: { type: Boolean, attribute: 'disabled-home', reflect: true },
64
+ /**
65
+ * Disables the left interaction
66
+ * @type {boolean}
55
67
  */
56
- disabledTooltip: { type: String, attribute: 'disabled-tooltip' },
68
+ disabledLeft: { type: Boolean, attribute: 'disabled-left', reflect: true },
69
+ /**
70
+ * Disables the right interaction
71
+ * @type {boolean}
72
+ */
73
+ disabledRight: { type: Boolean, attribute: 'disabled-right', reflect: true },
74
+ /**
75
+ * Disables the up interaction
76
+ * @type {boolean}
77
+ */
78
+ disabledUp: { type: Boolean, attribute: 'disabled-up', reflect: true },
57
79
  /**
58
80
  * REQUIRED: Accessible text for the button
59
81
  * @type {string}
@@ -67,12 +89,22 @@ class ButtonMove extends FocusMixin(RtlMixin(LitElement)) {
67
89
  return [ buttonStyles,
68
90
  css`
69
91
  :host {
92
+ --d2l-button-move-background-color-focus: #ffffff;
93
+ --d2l-button-move-icon-background-color-hover: var(--d2l-color-mica);
94
+ --d2l-button-move-box-shadow-focus: 0 0 0 2px #ffffff, 0 0 0 4px var(--d2l-color-celestine);
95
+ --d2l-icon-fill-color: var(--d2l-color-tungsten);
70
96
  display: inline-block;
71
97
  line-height: 0;
72
98
  }
73
99
  :host([hidden]) {
74
100
  display: none;
75
101
  }
102
+ :host([theme="dark"]) {
103
+ --d2l-button-move-background-color-focus: #000000;
104
+ --d2l-button-move-icon-background-color-hover: rgba(51, 53, 54, 0.9); /* tungsten @70% @90% */
105
+ --d2l-button-move-box-shadow-focus: 0 0 0 2px black, 0 0 0 4px var(--d2l-color-celestine-plus-1);
106
+ --d2l-icon-fill-color: var(--d2l-color-sylvite);
107
+ }
76
108
  button {
77
109
  background-color: transparent;
78
110
  display: flex;
@@ -90,11 +122,14 @@ class ButtonMove extends FocusMixin(RtlMixin(LitElement)) {
90
122
  width: 0.9rem;
91
123
  }
92
124
  button:focus {
93
- background-color: #ffffff;
125
+ background-color: var(--d2l-button-move-background-color-focus);
94
126
  }
95
127
  button:hover > d2l-icon,
96
128
  button:focus > d2l-icon {
97
- background-color: var(--d2l-color-mica);
129
+ background-color: var(--d2l-button-move-icon-background-color-hover);
130
+ }
131
+ button:${unsafeCSS(getFocusPseudoClass())} {
132
+ box-shadow: var(--d2l-button-move-box-shadow-focus);
98
133
  }
99
134
  .up-icon {
100
135
  border-top-left-radius: 0.3rem;
@@ -124,25 +159,27 @@ class ButtonMove extends FocusMixin(RtlMixin(LitElement)) {
124
159
  right: -0.2rem;
125
160
  }
126
161
 
127
-
128
162
  /* Firefox includes a hidden border which messes up button dimensions */
129
163
  button::-moz-focus-inner {
130
164
  border: 0;
131
165
  }
132
- :host([disabled]) button {
133
- cursor: default;
134
- opacity: 0.5;
135
- }
136
166
  button[disabled]:hover > d2l-icon {
137
167
  background-color: transparent;
138
168
  }
169
+ :host([disabled-up]) .up-icon,
170
+ :host([disabled-down]) .down-icon {
171
+ opacity: 0.5;
172
+ }
173
+ :host([disabled-up]) .up-layer,
174
+ :host([disabled-down]) .down-layer {
175
+ cursor: default;
176
+ }
139
177
  `
140
178
  ];
141
179
  }
142
180
 
143
181
  constructor() {
144
182
  super();
145
- this.disabled = false;
146
183
  /** @ignore */
147
184
  this.autofocus = false;
148
185
  /** @internal */
@@ -155,24 +192,13 @@ class ButtonMove extends FocusMixin(RtlMixin(LitElement)) {
155
192
  return 'button';
156
193
  }
157
194
 
158
- connectedCallback() {
159
- super.connectedCallback();
160
- this.addEventListener('click', this._handleClick, true);
161
- }
162
-
163
- disconnectedCallback() {
164
- super.disconnectedCallback();
165
- this.removeEventListener('click', this._handleClick, true);
166
- }
167
-
168
195
  render() {
169
196
  return html`
170
197
  <button
171
198
  aria-describedby="${ifDefined(this.description ? this._describedById : undefined)}"
172
- aria-disabled="${ifDefined(this.disabled && this.disabledTooltip ? 'true' : undefined)}"
173
199
  aria-label="${ifDefined(this.text)}"
174
200
  ?autofocus="${this.autofocus}"
175
- ?disabled="${this.disabled && !this.disabledTooltip}"
201
+ ?disabled="${this.disabledUp && this.disabledDown && this.disabledLeft && this.disabledRight && this.disabledHome && this.disabledEnd}"
176
202
  id="${this._buttonId}"
177
203
  @keydown="${this._handleKeydown}"
178
204
  title="${ifDefined(this.text)}"
@@ -183,7 +209,6 @@ class ButtonMove extends FocusMixin(RtlMixin(LitElement)) {
183
209
  <div class="down-layer" @click="${this._handleDownClick}"></div>
184
210
  </button>
185
211
  ${this.description ? html`<span id="${this._describedById}" hidden>${this.description}</span>` : null}
186
- ${this.disabled && this.disabledTooltip ? html`<d2l-tooltip for="${this._buttonId}">${this.disabledTooltip}</d2l-tooltip>` : ''}
187
212
  `;
188
213
  }
189
214
 
@@ -195,48 +220,45 @@ class ButtonMove extends FocusMixin(RtlMixin(LitElement)) {
195
220
  }));
196
221
  }
197
222
 
198
- _handleClick(e) {
199
- if (this.disabled) {
200
- e.stopPropagation();
201
- }
202
- }
203
-
204
223
  _handleDownClick() {
224
+ if (this.disabledDown) return;
205
225
  this._dispatchAction(moveActions.down);
206
226
  }
207
227
 
208
228
  _handleKeydown(e) {
229
+
209
230
  let action;
210
231
  switch (e.keyCode) {
211
232
  case keyCodes.UP:
212
- action = moveActions.up;
233
+ if (!this.disabledUp) action = moveActions.up;
213
234
  break;
214
235
  case keyCodes.DOWN:
215
- action = moveActions.down;
236
+ if (!this.disabledDown) action = moveActions.down;
216
237
  break;
217
238
  case keyCodes.LEFT:
218
- action = moveActions.left;
239
+ if (!this.disabledLeft) action = moveActions.left;
219
240
  break;
220
241
  case keyCodes.RIGHT:
221
- action = moveActions.right;
242
+ if (!this.disabledRight) action = moveActions.right;
222
243
  break;
223
244
  case keyCodes.HOME:
224
- action = (e.ctrlKey ? moveActions.rootHome : moveActions.home);
245
+ if (!this.disabledHome) action = (e.ctrlKey ? moveActions.rootHome : moveActions.home);
225
246
  break;
226
247
  case keyCodes.END:
227
- action = (e.ctrlKey ? moveActions.rootEnd : moveActions.end);
248
+ if (!this.disabledEnd) action = (e.ctrlKey ? moveActions.rootEnd : moveActions.end);
228
249
  break;
229
250
  default:
230
251
  return;
231
252
  }
232
253
 
233
- this._dispatchAction(action);
254
+ if (action) this._dispatchAction(action);
234
255
  e.preventDefault();
235
256
  e.stopPropagation();
236
257
 
237
258
  }
238
259
 
239
260
  _handleUpClick() {
261
+ if (this.disabledUp) return;
240
262
  this._dispatchAction(moveActions.up);
241
263
  }
242
264
 
@@ -30,15 +30,7 @@
30
30
 
31
31
  <d2l-demo-snippet>
32
32
  <template>
33
- <d2l-button-move text="Reorder Item" disabled></d2l-button-move>
34
- </template>
35
- </d2l-demo-snippet>
36
-
37
- <h2>Move Button Disabled with Tooltip</h2>
38
-
39
- <d2l-demo-snippet>
40
- <template>
41
- <d2l-button-move text="Reorder Item" disabled disabled-tooltip="Optional disabled tooltip"></d2l-button-move>
33
+ <d2l-button-move text="Reorder Item" disabled-up disabled-down disabled-left disabled-right disabled-home disabled-end></d2l-button-move>
42
34
  </template>
43
35
  </d2l-demo-snippet>
44
36
 
@@ -169,7 +169,7 @@ class ListDemoNested extends LitElement {
169
169
 
170
170
  _renderList(items, nested, includeControls = false, showLoadMore = false) {
171
171
  return html`
172
- <d2l-list ?grid="${!this.disableListGrid}" drag-multiple slot="${ifDefined(nested ? 'nested' : undefined)}">
172
+ <d2l-list ?grid="${!this.disableListGrid}" drag-multiple slot="${ifDefined(nested ? 'nested' : undefined)}" item-count="${this._items.length}">
173
173
  ${ includeControls ? this._renderListControls() : nothing }
174
174
  ${repeat(items, item => item.key, item => html`
175
175
  ${this._renderListItem(item)}
@@ -268,7 +268,6 @@ class ListDemoNested extends LitElement {
268
268
  <d2l-pager-load-more slot="pager"
269
269
  @d2l-pager-load-more="${this._handlePagerLoadMore}"
270
270
  ?has-more="${this._lastItemLoadedIndex < this._items.length - 1}"
271
- item-count="${this._items.length}"
272
271
  page-size="${this._remainingItemCount < this._pageSize ? this._remainingItemCount : this._pageSize}">
273
272
  </d2l-pager-load-more>
274
273
  `;
@@ -201,7 +201,6 @@ class DemoList extends LitElement {
201
201
  <d2l-pager-load-more slot="pager"
202
202
  @d2l-pager-load-more="${this._handlePagerLoadMore}"
203
203
  ?has-more="${this._lastItemLoadedIndex < this.items.length - 1}"
204
- item-count="${this.items.length}"
205
204
  page-size="${remainingItemCount < this._pageSize ? remainingItemCount : this._pageSize}">
206
205
  </d2l-pager-load-more>
207
206
  </d2l-list>
@@ -64,8 +64,6 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
64
64
  this.dragMultiple = false;
65
65
  this.extendSeparators = false;
66
66
  this.grid = false;
67
- this._itemsShowingCount = 0;
68
- this._itemsShowingTotalCount = 0;
69
67
  this._listItemChanges = [];
70
68
  this._childHasExpandCollapseToggle = false;
71
69
 
@@ -77,7 +75,7 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
77
75
 
78
76
  connectedCallback() {
79
77
  super.connectedCallback();
80
- this.addEventListener('d2l-list-items-showing-count-change', this._handleListItemsShowingCountChange);
78
+ this.addEventListener('d2l-list-item-showing-count-change', this._handleListItemShowingCountChange);
81
79
  this.addEventListener('d2l-list-item-nested-change', (e) => this._handleListIemNestedChange(e));
82
80
  }
83
81
 
@@ -190,13 +188,8 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
190
188
  return items[index];
191
189
  }
192
190
 
193
- async _getItemsShowingCount() {
194
- if (this.slot === 'nested') return this._itemsShowingCount;
195
- else return this._getListItemsShowingTotalCount(false);
196
- }
197
-
198
- _getLastItemIndex() {
199
- return this._itemsShowingCount - 1;
191
+ _getItemShowingCount() {
192
+ return this.getItems().length;
200
193
  }
201
194
 
202
195
  _getLazyLoadItems() {
@@ -204,13 +197,6 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
204
197
  return items.length > 0 ? items[0]._getFlattenedListItems().lazyLoadListItems : new Map();
205
198
  }
206
199
 
207
- async _getListItemsShowingTotalCount(refresh) {
208
- if (refresh) {
209
- this._itemsShowingTotalCount = this.getItems().length;
210
- }
211
- return this._itemsShowingTotalCount;
212
- }
213
-
214
200
  _handleKeyDown(e) {
215
201
  if (!this.grid || this.slot === 'nested' || e.keyCode !== keyCodes.TAB) return;
216
202
  e.preventDefault();
@@ -236,33 +222,26 @@ class List extends PageableMixin(SelectionMixin(LitElement)) {
236
222
  this._listChildrenUpdatedSubscribers.updateSubscribers();
237
223
  }
238
224
 
239
- _handleListItemsShowingCountChange() {
225
+ _handleListItemShowingCountChange() {
240
226
  if (this.slot === 'nested') return;
241
227
 
242
228
  // debounce the updates for first render case
243
- if (this._updateItemsShowingTotalCountRequested) return;
244
-
245
- this._updateItemsShowingTotalCountRequested = true;
246
- setTimeout(async() => {
247
- const oldCount = this._itemsShowingTotalCount;
248
- const newCount = await this._getListItemsShowingTotalCount(true);
249
- if (oldCount !== newCount) this._updatePagerCount(newCount);
250
- this._updateItemsShowingTotalCountRequested = false;
229
+ if (this._updateItemShowingCountRequested) return;
230
+
231
+ this._updateItemShowingCountRequested = true;
232
+ setTimeout(() => {
233
+ this._updateItemShowingCount();
234
+ this._updateItemShowingCountRequested = false;
251
235
  }, 0);
252
236
  }
253
237
 
254
- async _handleSlotChange(e) {
255
- const items = this.getItems(e.target);
256
- if (this._itemsShowingCount === items.length) return;
257
- this._itemsShowingCount = items.length;
258
-
259
- this._updatePagerCount(await this._getListItemsShowingTotalCount(true));
238
+ _handleSlotChange() {
239
+ this._updateItemShowingCount();
260
240
 
261
241
  /** @ignore */
262
- this.dispatchEvent(new CustomEvent('d2l-list-items-showing-count-change', {
242
+ this.dispatchEvent(new CustomEvent('d2l-list-item-showing-count-change', {
263
243
  bubbles: true,
264
- composed: true,
265
- detail: { count: this._itemsShowingCount }
244
+ composed: true
266
245
  }));
267
246
  }
268
247
 
@@ -7,16 +7,9 @@ The paging components and mixins can be used to provide consistent paging functi
7
7
  <script type="module">
8
8
  import '@brightspace-ui/core/components/paging/pager-load-more.js';
9
9
  </script>
10
- <d2l-pager-load-more has-more page-size="3" item-count="15"></d2l-pager-load-more>
10
+ <d2l-pager-load-more has-more page-size="3"></d2l-pager-load-more>
11
11
  ```
12
12
 
13
- ## Best Practices
14
- <!-- docs: start best practices -->
15
- <!-- docs: start dos -->
16
- * Consider the performance impact of acquiring the optional total `item-count`. The `item-count` provides useful context for the user, but counting large numbers of rows can be detrimental to performance. As a very general guide, when the total number of rows that needs to be counted is < 50,000, it's not a performance concern.
17
- <!-- docs: end dos -->
18
- <!-- docs: end best practices -->
19
-
20
13
  ## Load More Paging [d2l-pager-load-more]
21
14
 
22
15
  The `d2l-pager-load-more` component can be used in conjunction with pageable components such as `d2l-list` to provide load-more paging functionality. The pager will dispatch the `d2l-pager-load-more` when clicked, and then the consumer handles the event by loading more items, updating the pager state, and signalling completion by calling `complete()` on the event detail. Focus will be automatically moved on the first new item once complete.
@@ -24,10 +17,10 @@ The `d2l-pager-load-more` component can be used in conjunction with pageable com
24
17
  See [Pageable Lists](../../components/list/#pageable-lists).
25
18
 
26
19
  ```html
27
- <d2l-list>
20
+ <d2l-list item-count="85">
28
21
  <d2l-list-item ...></d2l-list-item>
29
22
  <d2l-list-item ...></d2l-list-item>
30
- <d2l-pager-load-more slot="pager" has-more page-size="10" item-count="85"></d2l-pager-load-more>
23
+ <d2l-pager-load-more slot="pager" has-more page-size="10"></d2l-pager-load-more>
31
24
  </d2l-list>
32
25
  ```
33
26
 
@@ -44,7 +37,6 @@ pager.addEventListener('d2l-pager-load-more', e => {
44
37
  | Property | Type | Description |
45
38
  |---|---|---|
46
39
  | `has-more` | Boolean, default: `false` | Whether there are more items that can be loaded. |
47
- | `item-count` | Number | Total number of items. If not specified, neither it nor the count of items showing will be displayed. |
48
40
  | `page-size` | Number, default: 50 | The number of additional items to load. |
49
41
 
50
42
  ### Events
@@ -27,12 +27,12 @@
27
27
 
28
28
  <d2l-demo-snippet>
29
29
  <template>
30
- <d2l-test-pageable>
30
+ <d2l-test-pageable item-count="12">
31
31
  <ul>
32
32
  <li><a href="https://some-website">item 1</a></li>
33
33
  <li><a href="https://some-website">item 2</a></li>
34
34
  </ul>
35
- <d2l-pager-load-more id="pager1" slot="pager" has-more page-size="3" item-count="12"></d2l-pager-load-more>
35
+ <d2l-pager-load-more id="pager1" slot="pager" has-more page-size="3"></d2l-pager-load-more>
36
36
  </d2l-test-pageable>
37
37
  <script>
38
38
  document.querySelector('#pager1').addEventListener('d2l-pager-load-more', window.handleLoadMore);
@@ -1,35 +1,62 @@
1
+ import { CollectionMixin } from '../../mixins/collection/collection-mixin.js';
1
2
  import { html } from 'lit';
3
+ import { SubscriberRegistryController } from '../../controllers/subscriber/subscriberControllers.js';
2
4
 
3
- export const PageableMixin = superclass => class extends superclass {
5
+ export const PageableMixin = superclass => class extends CollectionMixin(superclass) {
6
+
7
+ static get properties() {
8
+ return {
9
+ _itemShowingCount: { state: true },
10
+ };
11
+ }
4
12
 
5
13
  constructor() {
6
14
  super();
7
- this._pageable = true;
15
+
16
+ this._itemShowingCount = 0;
17
+ this._pageableSubscriberRegistry = new SubscriberRegistryController(this, 'pageable', {
18
+ onSubscribe: this._updatePageableSubscriber.bind(this),
19
+ updateSubscribers: this._updatePageableSubscribers.bind(this)
20
+ });
8
21
  }
9
22
 
10
- /* must be implemented by consumer */
11
- _getItemByIndex(index) { } // eslint-disable-line no-unused-vars
23
+ firstUpdated(changedProperties) {
24
+ super.firstUpdated(changedProperties);
25
+ this._updateItemShowingCount();
26
+ }
27
+
28
+ updated(changedProperties) {
29
+ super.updated(changedProperties);
30
+
31
+ if (changedProperties.has('itemCount') || changedProperties.has('_itemShowingCount')) {
32
+ this._pageableSubscriberRegistry.updateSubscribers();
33
+ }
34
+ }
12
35
 
13
36
  /* must be implemented by consumer */
14
- async _getItemsShowingCount() { }
37
+ _getItemByIndex(index) { } // eslint-disable-line no-unused-vars
15
38
 
16
39
  /* must be implemented by consumer */
17
- _getLastItemIndex() { }
40
+ _getItemShowingCount() { }
18
41
 
19
- async _handlePagerSlotChange(e) {
20
- this._updatePagerCount(await this._getItemsShowingCount(), e.target);
42
+ _getLastItemIndex() {
43
+ return this._itemShowingCount - 1;
21
44
  }
22
45
 
23
46
  _renderPagerContainer() {
24
- return html`<slot name="pager" @slotchange="${this._handlePagerSlotChange}"></slot>`;
47
+ return html`<slot name="pager"></slot>`;
25
48
  }
26
49
 
27
- _updatePagerCount(count, slot) {
28
- if (!slot) slot = this.shadowRoot.querySelector('slot[name="pager"]');
29
- const elements = slot.assignedElements({ flatten: true });
30
- if (elements.length > 0) {
31
- elements[0].itemShowingCount = count;
32
- }
50
+ _updateItemShowingCount() {
51
+ this._itemShowingCount = this._getItemShowingCount();
52
+ }
53
+
54
+ _updatePageableSubscriber(subscriber) {
55
+ subscriber._pageableInfo = { itemShowingCount: this._itemShowingCount, itemCount: this.itemCount };
56
+ }
57
+
58
+ _updatePageableSubscribers(subscribers) {
59
+ subscribers.forEach(subscriber => this._updatePageableSubscriber(subscriber));
33
60
  }
34
61
 
35
62
  };
@@ -0,0 +1,33 @@
1
+ import { EventSubscriberController, IdSubscriberController } from '../../controllers/subscriber/subscriberControllers.js';
2
+
3
+ export const PageableSubscriberMixin = superclass => class extends superclass {
4
+
5
+ static get properties() {
6
+ return {
7
+ /**
8
+ * Id of the `PageableMixin` component this component wants to observe (if not located within that component)
9
+ * @type {string}
10
+ */
11
+ pageableFor: { type: String, reflect: true, attribute: 'pageable-for' },
12
+ _pageableInfo: { state: true }
13
+ };
14
+ }
15
+
16
+ constructor() {
17
+ super();
18
+
19
+ this._pageableInfo = { itemCount: null, itemShowingCount: 0 };
20
+ this._pageableEventSubscriber = new EventSubscriberController(this, 'pageable');
21
+ this._pageableIdSubscriber = new IdSubscriberController(this, 'pageable', { idPropertyName: 'pageableFor' });
22
+ }
23
+
24
+ async getUpdateComplete() {
25
+ await super.getUpdateComplete();
26
+ await (this.pageableFor ? this._pageableIdSubscriber._subscriptionComplete : this._pageableEventSubscriber._subscriptionComplete);
27
+ }
28
+
29
+ _getPageableRegistries() {
30
+ return this.pageableFor ? this._pageableIdSubscriber.registries : [ this._pageableEventSubscriber.registry ];
31
+ }
32
+
33
+ };
@@ -2,7 +2,6 @@ import '../colors/colors.js';
2
2
  import '../loading-spinner/loading-spinner.js';
3
3
  import { css, html, LitElement, nothing } from 'lit';
4
4
  import { buttonStyles } from '../button/button-styles.js';
5
- import { findComposedAncestor } from '../../helpers/dom.js';
6
5
  import { FocusMixin } from '../../mixins/focus/focus-mixin.js';
7
6
  import { formatNumber } from '@brightspace-ui/intl/lib/number.js';
8
7
  import { getFirstFocusableDescendant } from '../../helpers/focus.js';
@@ -10,6 +9,7 @@ import { getSeparator } from '@brightspace-ui/intl/lib/list.js';
10
9
  import { labelStyles } from '../typography/styles.js';
11
10
  import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
12
11
  import { offscreenStyles } from '../offscreen/offscreen.js';
12
+ import { PageableSubscriberMixin } from './pageable-subscriber-mixin.js';
13
13
 
14
14
  const nativeFocus = document.createElement('div').focus;
15
15
 
@@ -17,7 +17,7 @@ const nativeFocus = document.createElement('div').focus;
17
17
  * A pager component for load-more paging.
18
18
  * @fires d2l-pager-load-more - Dispatched when the user clicks the load-more button. Consumers must call the provided "complete" method once items have been loaded.
19
19
  */
20
- class LoadMore extends FocusMixin(LocalizeCoreElement(LitElement)) {
20
+ class LoadMore extends PageableSubscriberMixin(FocusMixin(LocalizeCoreElement(LitElement))) {
21
21
 
22
22
  static get properties() {
23
23
  return {
@@ -26,21 +26,11 @@ class LoadMore extends FocusMixin(LocalizeCoreElement(LitElement)) {
26
26
  * @type {boolean}
27
27
  */
28
28
  hasMore: { type: Boolean, attribute: 'has-more', reflect: true },
29
- /**
30
- * Total number of items. If not specified, neither it nor the count of items showing will be displayed.
31
- * @type {number}
32
- */
33
- itemCount: { type: Number, attribute: 'item-count', reflect: true },
34
29
  /**
35
30
  * The number of additional items to load.
36
31
  * @type {number}
37
32
  */
38
33
  pageSize: { type: Number, attribute: 'page-size', reflect: true },
39
- /**
40
- * The number of items showing. Assigned by PageableMixin.
41
- * @ignore
42
- */
43
- itemShowingCount: { attribute: false, type: Number },
44
34
  _loading: { state: true }
45
35
  };
46
36
  }
@@ -85,9 +75,8 @@ class LoadMore extends FocusMixin(LocalizeCoreElement(LitElement)) {
85
75
  constructor() {
86
76
  super();
87
77
  this.hasMore = false;
88
- this.itemCount = -1;
78
+
89
79
  /** @ignore */
90
- this.itemShowingCount = 0;
91
80
  this.pageSize = 50;
92
81
  this._loading = false;
93
82
  }
@@ -97,7 +86,9 @@ class LoadMore extends FocusMixin(LocalizeCoreElement(LitElement)) {
97
86
  }
98
87
 
99
88
  render() {
100
- if (!this.hasMore) return;
89
+ if (!this.hasMore) return nothing;
90
+ const { itemCount, itemShowingCount } = this._pageableInfo;
91
+
101
92
  return html`
102
93
  ${this._loading ? html`
103
94
  <span class="d2l-offscreen" role="alert">${this.localize('components.pager-load-more.status-loading')}</span>
@@ -107,10 +98,10 @@ class LoadMore extends FocusMixin(LocalizeCoreElement(LitElement)) {
107
98
  <d2l-loading-spinner size="24"></d2l-loading-spinner>
108
99
  ` : html`
109
100
  <span class="action">${this.localize('components.pager-load-more.action', { count: formatNumber(this.pageSize) })}</span>
110
- ${this.itemCount > -1 ? html`
101
+ ${itemCount !== null ? html`
111
102
  <span class="d2l-offscreen">${getSeparator({ nonBreaking: true })}</span>
112
103
  <span class="separator"></span>
113
- <span class="info">${this.localize('components.pager-load-more.info', { showingCount: formatNumber(this.itemShowingCount), totalCount: this.itemCount, totalCountFormatted: formatNumber(this.itemCount) })}</span>
104
+ <span class="info">${this.localize('components.pager-load-more.info', { showingCount: formatNumber(itemShowingCount), totalCount: itemCount, totalCountFormatted: formatNumber(itemCount) })}</span>
114
105
  ` : nothing}
115
106
  `}
116
107
  </button>
@@ -119,7 +110,7 @@ class LoadMore extends FocusMixin(LocalizeCoreElement(LitElement)) {
119
110
 
120
111
  async _handleClick() {
121
112
  if (this._loading) return;
122
- const pageable = findComposedAncestor(this, node => node._pageable);
113
+ const pageable = this._getPageableRegistries()[0];
123
114
  if (!pageable) return;
124
115
  const lastItemIndex = pageable._getLastItemIndex();
125
116
 
@@ -1,3 +1,4 @@
1
+ import { CollectionMixin } from '../../mixins/collection/collection-mixin.js';
1
2
  import { RtlMixin } from '../../mixins/rtl/rtl-mixin.js';
2
3
 
3
4
  const keyCodes = {
@@ -35,15 +36,10 @@ export class SelectionInfo {
35
36
 
36
37
  }
37
38
 
38
- export const SelectionMixin = superclass => class extends RtlMixin(superclass) {
39
+ export const SelectionMixin = superclass => class extends RtlMixin(CollectionMixin(superclass)) {
39
40
 
40
41
  static get properties() {
41
42
  return {
42
- /**
43
- * Total number of items. Required when selecting all pages is allowed.
44
- * @type {number}
45
- */
46
- itemCount: { type: Number, attribute: 'item-count' },
47
43
  /**
48
44
  * Whether to render with single selection behaviour. If `selection-single` is specified, the nested `d2l-selection-input` elements will render radios instead of checkboxes, and the selection component will maintain a single selected item.
49
45
  * @type {boolean}
@@ -59,7 +55,6 @@ export const SelectionMixin = superclass => class extends RtlMixin(superclass) {
59
55
 
60
56
  constructor() {
61
57
  super();
62
- this.itemCount = 0;
63
58
  this.selectionSingle = false;
64
59
  this._selectAllPages = false;
65
60
  this._selectionObservers = new Map();
@@ -9,6 +9,7 @@ class BaseController {
9
9
  this._name = name;
10
10
  this._options = options;
11
11
  this._eventName = `d2l-subscribe-${this._name}`;
12
+ this._subscriptionComplete = Promise.resolve();
12
13
  }
13
14
  }
14
15
 
@@ -111,7 +112,12 @@ export class EventSubscriberController extends BaseSubscriber {
111
112
 
112
113
  hostConnected() {
113
114
  // delay subscription otherwise import/upgrade order can cause selection mixin to miss event
114
- requestAnimationFrame(() => this._subscribe());
115
+ this._subscriptionComplete = new Promise(resolve => {
116
+ requestAnimationFrame(() => {
117
+ this._subscribe();
118
+ resolve();
119
+ });
120
+ });
115
121
  }
116
122
 
117
123
  hostDisconnected() {
@@ -449,20 +449,39 @@
449
449
  "type": "string"
450
450
  },
451
451
  {
452
- "name": "disabled-tooltip",
453
- "description": "Tooltip text when disabled",
454
- "type": "string"
452
+ "name": "disabled-down",
453
+ "description": "Disables the down interaction",
454
+ "type": "boolean"
455
+ },
456
+ {
457
+ "name": "disabled-end",
458
+ "description": "Disables the end interaction",
459
+ "type": "boolean"
460
+ },
461
+ {
462
+ "name": "disabled-home",
463
+ "description": "Disables the home interaction",
464
+ "type": "boolean"
465
+ },
466
+ {
467
+ "name": "disabled-left",
468
+ "description": "Disables the left interaction",
469
+ "type": "boolean"
470
+ },
471
+ {
472
+ "name": "disabled-right",
473
+ "description": "Disables the right interaction",
474
+ "type": "boolean"
475
+ },
476
+ {
477
+ "name": "disabled-up",
478
+ "description": "Disables the up interaction",
479
+ "type": "boolean"
455
480
  },
456
481
  {
457
482
  "name": "text",
458
483
  "description": "REQUIRED: Accessible text for the button",
459
484
  "type": "string"
460
- },
461
- {
462
- "name": "disabled",
463
- "description": "Disables the button",
464
- "type": "boolean",
465
- "default": "false"
466
485
  }
467
486
  ],
468
487
  "properties": [
@@ -473,23 +492,46 @@
473
492
  "type": "string"
474
493
  },
475
494
  {
476
- "name": "disabledTooltip",
477
- "attribute": "disabled-tooltip",
478
- "description": "Tooltip text when disabled",
479
- "type": "string"
495
+ "name": "disabledDown",
496
+ "attribute": "disabled-down",
497
+ "description": "Disables the down interaction",
498
+ "type": "boolean"
499
+ },
500
+ {
501
+ "name": "disabledEnd",
502
+ "attribute": "disabled-end",
503
+ "description": "Disables the end interaction",
504
+ "type": "boolean"
505
+ },
506
+ {
507
+ "name": "disabledHome",
508
+ "attribute": "disabled-home",
509
+ "description": "Disables the home interaction",
510
+ "type": "boolean"
511
+ },
512
+ {
513
+ "name": "disabledLeft",
514
+ "attribute": "disabled-left",
515
+ "description": "Disables the left interaction",
516
+ "type": "boolean"
517
+ },
518
+ {
519
+ "name": "disabledRight",
520
+ "attribute": "disabled-right",
521
+ "description": "Disables the right interaction",
522
+ "type": "boolean"
523
+ },
524
+ {
525
+ "name": "disabledUp",
526
+ "attribute": "disabled-up",
527
+ "description": "Disables the up interaction",
528
+ "type": "boolean"
480
529
  },
481
530
  {
482
531
  "name": "text",
483
532
  "attribute": "text",
484
533
  "description": "REQUIRED: Accessible text for the button",
485
534
  "type": "string"
486
- },
487
- {
488
- "name": "disabled",
489
- "attribute": "disabled",
490
- "description": "Disables the button",
491
- "type": "boolean",
492
- "default": "false"
493
535
  }
494
536
  ],
495
537
  "events": [
@@ -8822,15 +8864,14 @@
8822
8864
  "default": "false"
8823
8865
  },
8824
8866
  {
8825
- "name": "selection-count-override",
8826
- "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
8867
+ "name": "item-count",
8868
+ "description": "Total number of items. If not specified, features like select-all-pages will be disabled.",
8827
8869
  "type": "number"
8828
8870
  },
8829
8871
  {
8830
- "name": "item-count",
8831
- "description": "Total number of items. Required when selecting all pages is allowed.",
8832
- "type": "number",
8833
- "default": "0"
8872
+ "name": "selection-count-override",
8873
+ "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
8874
+ "type": "number"
8834
8875
  },
8835
8876
  {
8836
8877
  "name": "selection-single",
@@ -8868,19 +8909,18 @@
8868
8909
  "type": "boolean",
8869
8910
  "default": "false"
8870
8911
  },
8912
+ {
8913
+ "name": "itemCount",
8914
+ "attribute": "item-count",
8915
+ "description": "Total number of items. If not specified, features like select-all-pages will be disabled.",
8916
+ "type": "number"
8917
+ },
8871
8918
  {
8872
8919
  "name": "selectionCountOverride",
8873
8920
  "attribute": "selection-count-override",
8874
8921
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
8875
8922
  "type": "number"
8876
8923
  },
8877
- {
8878
- "name": "itemCount",
8879
- "attribute": "item-count",
8880
- "description": "Total number of items. Required when selecting all pages is allowed.",
8881
- "type": "number",
8882
- "default": "0"
8883
- },
8884
8924
  {
8885
8925
  "name": "selectionSingle",
8886
8926
  "attribute": "selection-single",
@@ -10017,6 +10057,11 @@
10017
10057
  "path": "./components/paging/pager-load-more.js",
10018
10058
  "description": "A pager component for load-more paging.",
10019
10059
  "attributes": [
10060
+ {
10061
+ "name": "page-size",
10062
+ "description": "The number of additional items to load.",
10063
+ "type": "number"
10064
+ },
10020
10065
  {
10021
10066
  "name": "has-more",
10022
10067
  "description": "Whether there are more items that can be loaded.",
@@ -10024,19 +10069,18 @@
10024
10069
  "default": "false"
10025
10070
  },
10026
10071
  {
10027
- "name": "item-count",
10028
- "description": "Total number of items. If not specified, neither it nor the count of items showing will be displayed.",
10029
- "type": "number",
10030
- "default": "-1"
10031
- },
10032
- {
10033
- "name": "page-size",
10034
- "description": "The number of additional items to load.",
10035
- "type": "number",
10036
- "default": "50"
10072
+ "name": "pageable-for",
10073
+ "description": "Id of the `PageableMixin` component this component wants to observe (if not located within that component)",
10074
+ "type": "string"
10037
10075
  }
10038
10076
  ],
10039
10077
  "properties": [
10078
+ {
10079
+ "name": "pageSize",
10080
+ "attribute": "page-size",
10081
+ "description": "The number of additional items to load.",
10082
+ "type": "number"
10083
+ },
10040
10084
  {
10041
10085
  "name": "hasMore",
10042
10086
  "attribute": "has-more",
@@ -10045,18 +10089,10 @@
10045
10089
  "default": "false"
10046
10090
  },
10047
10091
  {
10048
- "name": "itemCount",
10049
- "attribute": "item-count",
10050
- "description": "Total number of items. If not specified, neither it nor the count of items showing will be displayed.",
10051
- "type": "number",
10052
- "default": "-1"
10053
- },
10054
- {
10055
- "name": "pageSize",
10056
- "attribute": "page-size",
10057
- "description": "The number of additional items to load.",
10058
- "type": "number",
10059
- "default": "50"
10092
+ "name": "pageableFor",
10093
+ "attribute": "pageable-for",
10094
+ "description": "Id of the `PageableMixin` component this component wants to observe (if not located within that component)",
10095
+ "type": "string"
10060
10096
  },
10061
10097
  {
10062
10098
  "name": "documentLocaleSettings",
@@ -10072,7 +10108,41 @@
10072
10108
  },
10073
10109
  {
10074
10110
  "name": "d2l-test-pageable",
10075
- "path": "./components/paging/test/pageable-component.js"
10111
+ "path": "./components/paging/test/pageable-component.js",
10112
+ "attributes": [
10113
+ {
10114
+ "name": "item-count",
10115
+ "description": "Total number of items. If not specified, features like select-all-pages will be disabled.",
10116
+ "type": "number"
10117
+ }
10118
+ ],
10119
+ "properties": [
10120
+ {
10121
+ "name": "itemCount",
10122
+ "attribute": "item-count",
10123
+ "description": "Total number of items. If not specified, features like select-all-pages will be disabled.",
10124
+ "type": "number"
10125
+ }
10126
+ ]
10127
+ },
10128
+ {
10129
+ "name": "d2l-test-pageable-simple",
10130
+ "path": "./components/paging/test/pageable-component.js",
10131
+ "attributes": [
10132
+ {
10133
+ "name": "item-count",
10134
+ "description": "Total number of items. If not specified, features like select-all-pages will be disabled.",
10135
+ "type": "number"
10136
+ }
10137
+ ],
10138
+ "properties": [
10139
+ {
10140
+ "name": "itemCount",
10141
+ "attribute": "item-count",
10142
+ "description": "Total number of items. If not specified, features like select-all-pages will be disabled.",
10143
+ "type": "number"
10144
+ }
10145
+ ]
10076
10146
  },
10077
10147
  {
10078
10148
  "name": "d2l-test-scroll-wrapper",
@@ -10152,12 +10222,6 @@
10152
10222
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
10153
10223
  "type": "number"
10154
10224
  },
10155
- {
10156
- "name": "item-count",
10157
- "description": "Total number of items. Required when selecting all pages is allowed.",
10158
- "type": "number",
10159
- "default": "0"
10160
- },
10161
10225
  {
10162
10226
  "name": "selection-single",
10163
10227
  "description": "Whether to render with single selection behaviour. If `selection-single` is specified, the nested `d2l-selection-input` elements will render radios instead of checkboxes, and the selection component will maintain a single selected item.",
@@ -10172,13 +10236,6 @@
10172
10236
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
10173
10237
  "type": "number"
10174
10238
  },
10175
- {
10176
- "name": "itemCount",
10177
- "attribute": "item-count",
10178
- "description": "Total number of items. Required when selecting all pages is allowed.",
10179
- "type": "number",
10180
- "default": "0"
10181
- },
10182
10239
  {
10183
10240
  "name": "selectionSingle",
10184
10241
  "attribute": "selection-single",
@@ -10801,12 +10858,6 @@
10801
10858
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
10802
10859
  "type": "number"
10803
10860
  },
10804
- {
10805
- "name": "item-count",
10806
- "description": "Total number of items. Required when selecting all pages is allowed.",
10807
- "type": "number",
10808
- "default": "0"
10809
- },
10810
10861
  {
10811
10862
  "name": "selection-single",
10812
10863
  "description": "Whether to render with single selection behaviour. If `selection-single` is specified, the nested `d2l-selection-input` elements will render radios instead of checkboxes, and the selection component will maintain a single selected item.",
@@ -10821,13 +10872,6 @@
10821
10872
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
10822
10873
  "type": "number"
10823
10874
  },
10824
- {
10825
- "name": "itemCount",
10826
- "attribute": "item-count",
10827
- "description": "Total number of items. Required when selecting all pages is allowed.",
10828
- "type": "number",
10829
- "default": "0"
10830
- },
10831
10875
  {
10832
10876
  "name": "selectionSingle",
10833
10877
  "attribute": "selection-single",
@@ -11329,12 +11373,6 @@
11329
11373
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
11330
11374
  "type": "number"
11331
11375
  },
11332
- {
11333
- "name": "item-count",
11334
- "description": "Total number of items. Required when selecting all pages is allowed.",
11335
- "type": "number",
11336
- "default": "0"
11337
- },
11338
11376
  {
11339
11377
  "name": "selection-single",
11340
11378
  "description": "Whether to render with single selection behaviour. If `selection-single` is specified, the nested `d2l-selection-input` elements will render radios instead of checkboxes, and the selection component will maintain a single selected item.",
@@ -11380,13 +11418,6 @@
11380
11418
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
11381
11419
  "type": "number"
11382
11420
  },
11383
- {
11384
- "name": "itemCount",
11385
- "attribute": "item-count",
11386
- "description": "Total number of items. Required when selecting all pages is allowed.",
11387
- "type": "number",
11388
- "default": "0"
11389
- },
11390
11421
  {
11391
11422
  "name": "selectionSingle",
11392
11423
  "attribute": "selection-single",
@@ -11552,12 +11583,6 @@
11552
11583
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
11553
11584
  "type": "number"
11554
11585
  },
11555
- {
11556
- "name": "item-count",
11557
- "description": "Total number of items. Required when selecting all pages is allowed.",
11558
- "type": "number",
11559
- "default": "0"
11560
- },
11561
11586
  {
11562
11587
  "name": "selection-single",
11563
11588
  "description": "Whether to render with single selection behaviour. If `selection-single` is specified, the nested `d2l-selection-input` elements will render radios instead of checkboxes, and the selection component will maintain a single selected item.",
@@ -11593,13 +11618,6 @@
11593
11618
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
11594
11619
  "type": "number"
11595
11620
  },
11596
- {
11597
- "name": "itemCount",
11598
- "attribute": "item-count",
11599
- "description": "Total number of items. Required when selecting all pages is allowed.",
11600
- "type": "number",
11601
- "default": "0"
11602
- },
11603
11621
  {
11604
11622
  "name": "selectionSingle",
11605
11623
  "attribute": "selection-single",
@@ -0,0 +1,36 @@
1
+ # CollectionMixin
2
+
3
+ The `CollectionMixin` describes a collection of items like a list or table. It has one attribute, `item-count`, which optionally defines the total number of items in the collection. This may be greater than the number of items currently displayed, and is useful for actions like select-all and paging.
4
+
5
+ ## Best Practices
6
+ <!-- docs: start best practices -->
7
+ <!-- docs: start dos -->
8
+ * Consider the performance impact of acquiring the optional total `item-count`. The `item-count` provides useful context for the user, but counting large numbers of rows can be detrimental to performance. As a very general guide, when the total number of rows that needs to be counted is < 50,000, it's not a performance concern.
9
+ <!-- docs: end dos -->
10
+ <!-- docs: end best practices -->
11
+
12
+ ## Usage
13
+
14
+ Apply the mixin and access the `itemCount` property as needed. Note that `itemCount` has a default value of `null` to indicate that no count was specified.
15
+
16
+ ```js
17
+ import { CollectionMixin } from '@brightspace-ui/core/mixins/collection-mixin.js';
18
+
19
+ class MyComponent extends CollectionMixin(LitElement) {
20
+ render() {
21
+ const itemCountToDisplay = this.itemCount !== null ? this.itemCount : 'Unspecified';
22
+ return html`
23
+ <p>Total number of items: ${itemCountToDisplay}</p>
24
+ <slot></slot>
25
+ `;
26
+ }
27
+ }
28
+ ```
29
+
30
+ <!-- docs: start hidden content -->
31
+ ### Properties
32
+
33
+ | Property | Type | Description |
34
+ |---|---|---|
35
+ | `item-count` | Number | Total number of items. Required when selecting all pages is allowed. |
36
+ <!-- docs: end hidden content -->
@@ -0,0 +1,18 @@
1
+ export const CollectionMixin = superclass => class extends superclass {
2
+
3
+ static get properties() {
4
+ return {
5
+ /**
6
+ * Total number of items. If not specified, features like select-all-pages will be disabled.
7
+ * @type {number}
8
+ */
9
+ itemCount: { type: Number, attribute: 'item-count', reflect: true },
10
+ };
11
+ }
12
+
13
+ constructor() {
14
+ super();
15
+ this.itemCount = null;
16
+ }
17
+
18
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "2.104.2",
3
+ "version": "2.106.0",
4
4
  "description": "A collection of accessible, free, open-source web components for building Brightspace applications",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/BrightspaceUI/core.git",