@brightspace-ui/core 2.105.0 → 2.107.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.
@@ -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() {
@@ -8864,15 +8864,14 @@
8864
8864
  "default": "false"
8865
8865
  },
8866
8866
  {
8867
- "name": "selection-count-override",
8868
- "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.",
8869
8869
  "type": "number"
8870
8870
  },
8871
8871
  {
8872
- "name": "item-count",
8873
- "description": "Total number of items. Required when selecting all pages is allowed.",
8874
- "type": "number",
8875
- "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"
8876
8875
  },
8877
8876
  {
8878
8877
  "name": "selection-single",
@@ -8910,19 +8909,18 @@
8910
8909
  "type": "boolean",
8911
8910
  "default": "false"
8912
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
+ },
8913
8918
  {
8914
8919
  "name": "selectionCountOverride",
8915
8920
  "attribute": "selection-count-override",
8916
8921
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
8917
8922
  "type": "number"
8918
8923
  },
8919
- {
8920
- "name": "itemCount",
8921
- "attribute": "item-count",
8922
- "description": "Total number of items. Required when selecting all pages is allowed.",
8923
- "type": "number",
8924
- "default": "0"
8925
- },
8926
8924
  {
8927
8925
  "name": "selectionSingle",
8928
8926
  "attribute": "selection-single",
@@ -10059,6 +10057,11 @@
10059
10057
  "path": "./components/paging/pager-load-more.js",
10060
10058
  "description": "A pager component for load-more paging.",
10061
10059
  "attributes": [
10060
+ {
10061
+ "name": "page-size",
10062
+ "description": "The number of additional items to load.",
10063
+ "type": "number"
10064
+ },
10062
10065
  {
10063
10066
  "name": "has-more",
10064
10067
  "description": "Whether there are more items that can be loaded.",
@@ -10066,19 +10069,18 @@
10066
10069
  "default": "false"
10067
10070
  },
10068
10071
  {
10069
- "name": "item-count",
10070
- "description": "Total number of items. If not specified, neither it nor the count of items showing will be displayed.",
10071
- "type": "number",
10072
- "default": "-1"
10073
- },
10074
- {
10075
- "name": "page-size",
10076
- "description": "The number of additional items to load.",
10077
- "type": "number",
10078
- "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"
10079
10075
  }
10080
10076
  ],
10081
10077
  "properties": [
10078
+ {
10079
+ "name": "pageSize",
10080
+ "attribute": "page-size",
10081
+ "description": "The number of additional items to load.",
10082
+ "type": "number"
10083
+ },
10082
10084
  {
10083
10085
  "name": "hasMore",
10084
10086
  "attribute": "has-more",
@@ -10087,18 +10089,10 @@
10087
10089
  "default": "false"
10088
10090
  },
10089
10091
  {
10090
- "name": "itemCount",
10091
- "attribute": "item-count",
10092
- "description": "Total number of items. If not specified, neither it nor the count of items showing will be displayed.",
10093
- "type": "number",
10094
- "default": "-1"
10095
- },
10096
- {
10097
- "name": "pageSize",
10098
- "attribute": "page-size",
10099
- "description": "The number of additional items to load.",
10100
- "type": "number",
10101
- "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"
10102
10096
  },
10103
10097
  {
10104
10098
  "name": "documentLocaleSettings",
@@ -10114,7 +10108,41 @@
10114
10108
  },
10115
10109
  {
10116
10110
  "name": "d2l-test-pageable",
10117
- "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
+ ]
10118
10146
  },
10119
10147
  {
10120
10148
  "name": "d2l-test-scroll-wrapper",
@@ -10194,12 +10222,6 @@
10194
10222
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
10195
10223
  "type": "number"
10196
10224
  },
10197
- {
10198
- "name": "item-count",
10199
- "description": "Total number of items. Required when selecting all pages is allowed.",
10200
- "type": "number",
10201
- "default": "0"
10202
- },
10203
10225
  {
10204
10226
  "name": "selection-single",
10205
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.",
@@ -10214,13 +10236,6 @@
10214
10236
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
10215
10237
  "type": "number"
10216
10238
  },
10217
- {
10218
- "name": "itemCount",
10219
- "attribute": "item-count",
10220
- "description": "Total number of items. Required when selecting all pages is allowed.",
10221
- "type": "number",
10222
- "default": "0"
10223
- },
10224
10239
  {
10225
10240
  "name": "selectionSingle",
10226
10241
  "attribute": "selection-single",
@@ -10843,12 +10858,6 @@
10843
10858
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
10844
10859
  "type": "number"
10845
10860
  },
10846
- {
10847
- "name": "item-count",
10848
- "description": "Total number of items. Required when selecting all pages is allowed.",
10849
- "type": "number",
10850
- "default": "0"
10851
- },
10852
10861
  {
10853
10862
  "name": "selection-single",
10854
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.",
@@ -10863,13 +10872,6 @@
10863
10872
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
10864
10873
  "type": "number"
10865
10874
  },
10866
- {
10867
- "name": "itemCount",
10868
- "attribute": "item-count",
10869
- "description": "Total number of items. Required when selecting all pages is allowed.",
10870
- "type": "number",
10871
- "default": "0"
10872
- },
10873
10875
  {
10874
10876
  "name": "selectionSingle",
10875
10877
  "attribute": "selection-single",
@@ -11371,12 +11373,6 @@
11371
11373
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
11372
11374
  "type": "number"
11373
11375
  },
11374
- {
11375
- "name": "item-count",
11376
- "description": "Total number of items. Required when selecting all pages is allowed.",
11377
- "type": "number",
11378
- "default": "0"
11379
- },
11380
11376
  {
11381
11377
  "name": "selection-single",
11382
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.",
@@ -11422,13 +11418,6 @@
11422
11418
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
11423
11419
  "type": "number"
11424
11420
  },
11425
- {
11426
- "name": "itemCount",
11427
- "attribute": "item-count",
11428
- "description": "Total number of items. Required when selecting all pages is allowed.",
11429
- "type": "number",
11430
- "default": "0"
11431
- },
11432
11421
  {
11433
11422
  "name": "selectionSingle",
11434
11423
  "attribute": "selection-single",
@@ -11594,12 +11583,6 @@
11594
11583
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
11595
11584
  "type": "number"
11596
11585
  },
11597
- {
11598
- "name": "item-count",
11599
- "description": "Total number of items. Required when selecting all pages is allowed.",
11600
- "type": "number",
11601
- "default": "0"
11602
- },
11603
11586
  {
11604
11587
  "name": "selection-single",
11605
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.",
@@ -11635,13 +11618,6 @@
11635
11618
  "description": "ADVANCED: Temporary optional parameter used to override existing count. Will be removed soon, use with caution.",
11636
11619
  "type": "number"
11637
11620
  },
11638
- {
11639
- "name": "itemCount",
11640
- "attribute": "item-count",
11641
- "description": "Total number of items. Required when selecting all pages is allowed.",
11642
- "type": "number",
11643
- "default": "0"
11644
- },
11645
11621
  {
11646
11622
  "name": "selectionSingle",
11647
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
+ };
@@ -1,135 +1,84 @@
1
- # Localization Mixins
1
+ # LocalizeMixin
2
2
 
3
- The `LocalizeMixin` and `LocalizeStaticMixin` allow you to localize text in your components and have it displayed to the user in their preferred language.
4
-
5
- ## Providing Resources
6
-
7
- Your component must provide resources by either implementing a `resources` getter for local resources, or a `localizeConfig` getter to fetch resources asynchronously. The `importFunc` method of `localizeConfig` will be called with lowercase languages in preferential order.
3
+ The `LocalizeMixin` allows text in components to be displayed in the user's preferred language.
8
4
 
9
5
  ## Language Resources
10
6
 
11
- Resources should be key-value JSON objects, where the keys are lowercase strings and the values are in [ICU Message Syntax](https://formatjs.io/docs/core-concepts/icu-syntax/) format.
7
+ Resources are stored as key-value JSON objects.
12
8
 
13
- The ICU Message Syntax supports some cool features, such as: [simple arguments](https://formatjs.io/docs/core-concepts/icu-syntax/#simple-argument), the [`{select}` format](https://formatjs.io/docs/core-concepts/icu-syntax/#select-format) and [pluralization](https://formatjs.io/docs/core-concepts/icu-syntax/#plural-format).
9
+ ### Keys
14
10
 
15
- **Note:** Avoid using the ICU Message Syntax number, date and time formatting functionality. D2L allows our users to customize how these are localized, so use [@brightspace-ui/intl](https://github.com/BrightspaceUI/intl)'s `formatNumber`, `formatDate` and `formatTime` instead.
16
-
17
- Example:
11
+ The key should succinctly and uniquely describe the text being localized. `camelCase` is recommended, although `snake_case` and `kebab-case` are also supported.
18
12
 
19
- ```javascript
20
- {
21
- "hello": "Hello {firstName}!",
22
- "goodbye": "Goodbye."
23
- }
24
- ```
13
+ For large projects, terms may be grouped using the `:` character. For example: `parentGroup:subGroup:termName`.
25
14
 
26
- Always provide language resources for base languages (e.g. `en`, `fr`, `pt`, etc.). That way, if the user prefers a regional language (e.g. `pt-br`) that isn't recognized, it can fall back to the base language.
15
+ ### Values
27
16
 
28
- ### Static vs. Dynamic Resources
17
+ Term values must conform to the [ICU Message Syntax](https://formatjs.io/docs/core-concepts/icu-syntax/) format. It supports features such as: [simple arguments](https://formatjs.io/docs/core-concepts/icu-syntax/#simple-argument), the [`{select}` format](https://formatjs.io/docs/core-concepts/icu-syntax/#select-format) and [pluralization](https://formatjs.io/docs/core-concepts/icu-syntax/#plural-format).
29
18
 
30
- For components with local resources, use the `LocalizeStaticMixin` and implement a `static` `resources` getter that returns the local resources synchronously. To get resources asynchronously, use the `LocalizeMixin` and implement a `static` `localizeConfig` getter that returns details about where to find your resources.
19
+ > **Note:** Avoid using the ICU Message Syntax number, date and time formatting functionality. Brightspace allows customization of how these are localized, so use [@brightspace-ui/intl](https://github.com/BrightspaceUI/intl)'s `formatNumber`, `formatDate` and `formatTime` instead.
31
20
 
32
- #### Example 1: Static Resources
21
+ ### Files
33
22
 
34
- If your component has a small number of translations, it may make sense to store them locally within the component in a constant.
23
+ Store localization resources in their own directory with nothing else in it. There should be one JavaScript file for each supported locale.
35
24
 
36
25
  ```javascript
37
- import { LocalizeStaticMixin } from '@brightspace-ui/core/mixins/localize/localize-mixin.js';
38
-
39
- class MyComponent extends LocalizeStaticMixin(LitElement) {
40
-
41
- static get resources() {
42
- return {
43
- 'en': {
44
- hello: 'Hello {firstName}!'
45
- },
46
- 'fr': {
47
- hello: 'Bonjour {firstName}!'
48
- },
49
- ...
50
- };
51
- }
52
-
53
- }
26
+ // en.js
27
+ export default {
28
+ "hello": "Hello {firstName}!"
29
+ };
54
30
  ```
55
- #### Example 2: Dynamically Imported Resources
56
-
57
- This approach is better for components with many language resources. By importing them dynamically, only the resources for the requested language are actually fetched and downloaded.
58
-
59
- Store your resources in individual files, one for each language:
60
31
  ```javascript
61
- // en.js
32
+ // fr.js
62
33
  export default {
63
- 'hello': 'Hello {firstName}!'
34
+ "hello": "Bonjour {firstName}!"
64
35
  };
65
36
  ```
66
37
 
67
- **Note:** To avoid accidental imports and errors, your resource files should have a dedicated directory with nothing else in it.
38
+ Always provide language resources for base languages (e.g. `en`, `fr`, `pt`). That way, if the user prefers a regional language (e.g. `pt-br`) that isn't recognized, it can fall back to the base language (`pt`).
39
+
40
+ ## Using `LocalizeMixin`
41
+
42
+ The component should import and extend `LocalizeMixin`:
68
43
 
69
- Then create your `localizeConfig` getter:
70
44
  ```javascript
71
45
  import { LocalizeMixin } from '@brightspace-ui/core/mixins/localize/localize-mixin.js';
72
46
 
73
47
  class MyComponent extends LocalizeMixin(LitElement) {
74
48
 
75
- static get localizeConfig() {
76
- return {
77
- // Import path must be relative
78
- importFunc: async lang => (await import(`../lang/${lang}.js`)).default,
79
- // Optionally enable OSLO
80
- osloCollection: '@d2l\\my-project\\myComponent',
81
- };
82
- }
83
49
  }
84
50
  ```
85
- Occasionally, it may be desirable to localize based on the user's browser settings. To do this, add `useBrowserLangs: true` to your `localizeConfig` object. This option should only be used if *all* supported *locales* have corresponding files named with their 4-character locale code, and all supported *languages*, in addition, have 2-character files. (e.g. `en-us.js`, `en-ca.js` and `en.js`)
86
51
 
87
- If your build system does not support variable dynamic imports, you'll need to manually set up imports for each supported language:
52
+ Implement a static getter for `localizeConfig` that defines `importFunc()`. It will be passed a language which can subsequently be dynamically imported from the location of the component's resources.
53
+
54
+ The dynamic import path must be relative.
88
55
 
89
56
  ```javascript
90
57
  static get localizeConfig() {
91
58
  return {
92
- importFunc: async lang => {
93
- switch (lang) {
94
- case 'en':
95
- return (await import('./locales/en.js')).default;
96
- case 'fr':
97
- return (await import('./locales/fr.js')).default;
98
- ...
99
- }
100
- }
59
+ importFunc: async lang => (await import(`../lang/${lang}.js`)).default,
101
60
  };
102
61
  }
103
62
  ```
104
63
 
105
- **Note:** If using `LocalizeCoreElement` or a mixin that utilizes `LocalizeCoreElement` as well as `LocalizeMixin` or a mixin that uses `LocalizeMixin`, `LocalizeMixin` **must** appear before `LocalizeCoreElement` in the chain. For example:
106
-
107
- ```javascript
108
- import { LocalizeCoreElement } from '@brightspace-ui/core/helpers/localize-core-element.js';
109
- import { LocalizeMixin } from '@brightspace-ui/core/mixins/localize/localize-mixin.js';
110
-
111
- class MyComponent extends LocalizeMixin(LocalizeCoreElement(LitElement)) {
112
- ...
113
- }
114
- ```
115
-
116
- ## `localize()`
64
+ ### `localize()`
117
65
 
118
- Once your localization resources are available, the `localize()` method is used to localize a piece of text in your `render()` method.
66
+ The `localize()` method is used to localize a piece of text in the component's `render()` method.
119
67
 
120
- If your localized string contains arguments, pass them as a key-value object as the 2nd parameter:
68
+ If the localized string contains arguments, pass them as a key-value object as the 2nd parameter:
121
69
 
122
70
  ```javascript
123
71
  render() {
124
- return html`<p>${this.localize('hello', { firstName: 'Mary' })}</p>`;
72
+ const message = this.localize('hello', { firstName: 'Mary' });
73
+ return html`<p>${message}</p>`;
125
74
  }
126
75
  ```
127
76
 
128
- ## `localizeHTML()`
77
+ ### `localizeHTML()`
129
78
 
130
79
  Rich formatting can be included in localization resources and safely converted to HTML with the `localizeHTML()` method.
131
80
 
132
- ### Basic Formatting
81
+ #### Basic Formatting
133
82
 
134
83
  The following formatting elements are supported out-of-the-box:
135
84
 
@@ -144,7 +93,19 @@ Remember that `<strong>` is for content of greater importance (browsers show thi
144
93
 
145
94
  Similarly `<em>` *emphasizes* a particular piece of text (browsers show this visually using italics), whereas `<i>` only italicizes the text visually without emphasis.
146
95
 
147
- ### Links
96
+ Example:
97
+
98
+ ```json
99
+ {
100
+ "myTerm": "This is <b>bold</b> but <em>not</em> all that <strong>important</strong>."
101
+ }
102
+ ```
103
+
104
+ ```javascript
105
+ this.localizeHTML('myTerm');
106
+ ```
107
+
108
+ #### Links
148
109
 
149
110
  To wrap text in [a link](../../components/link/), define a unique tag in the localization resource:
150
111
 
@@ -164,7 +125,7 @@ this.localizeHTML('myTerm', {
164
125
  });
165
126
  ```
166
127
 
167
- ### Help Tooltips
128
+ #### Help Tooltips
168
129
 
169
130
  To use a [help tooltip](../../components/tooltip/), define a unique tag in the localization resource in addition to the tooltip's text:
170
131
 
@@ -175,7 +136,7 @@ To use a [help tooltip](../../components/tooltip/), define a unique tag in the l
175
136
  }
176
137
  ```
177
138
 
178
- Then import `generateTooltipHelp` and pass it the tooltip term value:
139
+ Then import `generateTooltipHelp` and pass it the tooltip contents value:
179
140
 
180
141
  ```javascript
181
142
  import { generateTooltipHelp } from '@brightspace-ui/core/mixins/localize/localize-mixin.js';
@@ -184,3 +145,38 @@ this.localizeHTML('octopus', {
184
145
  tooltip: generateTooltipHelp({ contents: this.localize('cephalopodTooltip') })
185
146
  });
186
147
  ```
148
+
149
+ ## Off-Stack Language Overrides
150
+
151
+ To enable OSLO, map the project's localization resources to a language collection in Brightspace by defining `osloCollection` in `localizeConfig`:
152
+
153
+ ```javascript
154
+ static get localizeConfig() {
155
+ return {
156
+ osloCollection: '@d2l\\my-project\\myComponent',
157
+ };
158
+ }
159
+ ```
160
+
161
+ > **Important:** When defining language resource keys, avoid using the Full Stop (`.`) character for grouping. OSLO does not support it.
162
+
163
+ ## Advanced
164
+
165
+ ### Chaining `LocalizeMixin`
166
+
167
+ If combining `LocalizeMixin` with `LocalizeCoreElement` (or a mixin that uses `LocalizeCoreElement`), `LocalizeMixin` **must** appear before `LocalizeCoreElement` in the chain.
168
+
169
+ ```javascript
170
+ import { LocalizeCoreElement } from '@brightspace-ui/core/helpers/localize-core-element.js';
171
+ import { LocalizeMixin } from '@brightspace-ui/core/mixins/localize/localize-mixin.js';
172
+
173
+ class MyComponent extends LocalizeMixin(LocalizeCoreElement(LitElement)) {
174
+ ...
175
+ }
176
+ ```
177
+
178
+ ### Ignoring the Brightspace Language
179
+
180
+ Occasionally, it may be desirable to localize based on the browser's language instead of the language in Brightspace.
181
+
182
+ To do this, define `useBrowserLangs` as `true` in `localizeConfig`. This option should only be used if all supported locales have corresponding files named with their 4-character and 2-character locale codes (e.g. `en-us.js`, `en-ca.js` and `en.js`).
@@ -230,31 +230,6 @@ export const LocalizeMixin = superclass => class extends _LocalizeMixinBase(supe
230
230
 
231
231
  };
232
232
 
233
- export const LocalizeStaticMixin = superclass => class extends _LocalizeMixinBase(superclass) {
234
-
235
- static async getLocalizeResources(langs) {
236
- let resolvedLang = fallbackLang;
237
- const resolvedResources = Object.assign({}, this.resources[fallbackLang]);
238
-
239
- langs.reverse().forEach((lang) => {
240
- if (this.resources[lang]) {
241
- resolvedLang = lang;
242
- Object.assign(resolvedResources, this.resources[lang]);
243
- }
244
- });
245
-
246
- return {
247
- language: resolvedLang,
248
- resources: resolvedResources
249
- };
250
- }
251
-
252
- static get resources() {
253
- return { 'en': {} };
254
- }
255
-
256
- };
257
-
258
233
  export const allowedTags = Object.freeze(['d2l-link', 'd2l-tooltip-help', 'p', 'br', 'b', 'strong', 'i', 'em']);
259
234
 
260
235
  const markupError = `localizeHTML() rich-text replacements must use localizeMarkup templates with only the following allowed elements: ${allowedTags}. For more information, see: https://github.com/BrightspaceUI/core/blob/main/mixins/localize/`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "2.105.0",
3
+ "version": "2.107.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",