@brightspace-ui/core 2.137.3 → 2.138.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.
@@ -189,6 +189,7 @@ The `d2l-filter-dimension-set` component is the main dimension type that will wo
189
189
 
190
190
  | Property | Type | Description |
191
191
  |---|---|---|
192
+ | `has-more` | Boolean | Whether the dimension has more values to load. Must be used with selected-first and manual search-type. |
192
193
  | `header-text` | String | A heading displayed above the list items. This is usually unnecessary, but can be used to emphasize or promote something specific about the list of items to help orient users. |
193
194
  | `introductory-text` | String | The introductory text to display at the top of the filter dropdown |
194
195
  | `key` | String, required | Unique identifier for the dimension |
@@ -232,6 +233,36 @@ This component is built to be used alongside the [d2l-filter-dimension-set](#d2l
232
233
  | `selected` | Boolean, default: `false` | Whether the value in the filter is selected or not |
233
234
  <!-- docs: end hidden content -->
234
235
 
236
+ ## Search and Paging
237
+
238
+ Most filters will not need search or paging features since filter value lists are generally short. For longer lists of filter values when Search is necessary, it can be enabled by setting search-type to `automatic` or `manual`.
239
+
240
+ `automatic` search runs a basic case-insensitive text comparison on the dimension values that are loaded in the browser, having no awareness of server-side values that are not yet loaded.
241
+
242
+ `manual` search dispatches a `d2l-filter-dimension-search` event delegating the search to the component's consumer. The event's detail will contain the key of the dimension from where the event was dispatched (`key`), the text value used for the search (`value`) and a callback (`searchCompleteCallback`). This callback gives the consumer control of which keys to display, either by setting `displayAllKeys` to `true` or passing a list of the keys to display as `keysToDisplay` (all other keys will be hidden). The dimension will be in a loading state until the callback is called.
243
+ ```js
244
+ e.detail.searchCompleteCallback({ keysToDisplay: keysToDisplay });
245
+ e.detail.searchCompleteCallback({ displayAllKeys: true });
246
+ ```
247
+
248
+ As with Search, paging is often unnecessary since filter lists are generally short. For long lists of filter values, load-more paging can be enabled by setting `has-more` on a dimension set, which will display a `d2l-pager-load-more` button at the end of the values. Note however that paging requires the search type to be set to `manual`. Clicking the button replaces its text with a loading spinner and dispatches a `d2l-filter-dimension-load-more` event whose detail, like the search event, contains the dimension key (`key`), active search value (`value`) and a callback (`loadMoreCompleteCallback`) that works just like `searchCompleteCallback` described above. The pager will also be in a loading state until the callback is called.
249
+ ```js
250
+ e.detail.loadMoreCompleteCallback({ keysToDisplay: keysToDisplay });
251
+ e.detail.loadMoreCompleteCallback({ displayAllKeys: true });
252
+ ```
253
+
254
+ ### Selection and manual search/paging
255
+
256
+ The filter component depends entirely on the consumer to include the selected filter values in order for the selected counts and `d2l-filter-tags` to display the correct values. Ideally, all values should be loaded into the dimensions and the event callbacks should be leveraged to set the visibility on those values. However, in the cases where this is not possible and new values are being added/removed manually from the dimension, then selection should be persisted. This means that selected items should always be loaded and included in the dimension and they should not be removed in order to maintain the functionality of counts and filter tags.
257
+ <!-- docs: demo -->
258
+ ```html
259
+ <script type="module">
260
+ import '@brightspace-ui/core/components/filter/demo/filter-load-more-demo.js'
261
+ </script>
262
+ <d2l-filter-load-more-demo>
263
+ </d2l-filter-load-more-demo>
264
+ ```
265
+
235
266
  ## Counts
236
267
 
237
268
  The `count` property displays a count next to each filter value to indicate the number of results a value will yield. This helps users more effectively explore data and make selections, so it’s a good idea to provide these counts if it can be done performantly.
@@ -11,6 +11,11 @@ class FilterDimensionSet extends LitElement {
11
11
 
12
12
  static get properties() {
13
13
  return {
14
+ /**
15
+ * Whether the dimension has more values to load. Manual search and selected first should be set if has more is being used
16
+ * @type {boolean}
17
+ */
18
+ hasMore: { type: Boolean, attribute: 'has-more' },
14
19
  /**
15
20
  * A heading displayed above the list items. This is usually unnecessary, but can be used to emphasize or promote something specific about the list of items to help orient users.
16
21
  * @type {string}
@@ -31,11 +36,6 @@ class FilterDimensionSet extends LitElement {
31
36
  * @type {boolean}
32
37
  */
33
38
  loading: { type: Boolean },
34
- /**
35
- * Whether the dimension has more values to load
36
- * @type {boolean}
37
- */
38
- hasMore: { type: Boolean, attribute: 'has-more' },
39
39
  /**
40
40
  * Whether to hide the search input, perform a simple text search, or fire an event on search
41
41
  * @type {'none'|'automatic'|'manual'}
@@ -47,7 +47,7 @@ class FilterDimensionSet extends LitElement {
47
47
  */
48
48
  selectAll: { type: Boolean, attribute: 'select-all' },
49
49
  /**
50
- * Whether to render the selected items at the top of the filter
50
+ * Whether to render the selected items at the top of the filter. Forced on if load more paging is being used
51
51
  * @type {boolean}
52
52
  */
53
53
  selectedFirst: { type: Boolean, attribute: 'selected-first' },
@@ -12,13 +12,7 @@ For example, this causes a text input to be skeletized:
12
12
  <d2l-input-text label="Name" skeleton></d2l-input-text>
13
13
  ```
14
14
 
15
- In a typical scenario, many skeleton-aware components would have their `skeleton` attributes bound to a single property on the host component, making it easy to toggle them all together:
16
-
17
- ```html
18
- <d2l-input-text label="Name" ?skeleton="${this.skeleton}"></d2l-text-input>
19
- <d2l-input-date label="Due Date" ?skeleton="${this.skeleton}"></d2l-input-date>
20
- <my-element ?skeleton="${this.skeleton}"></my-element>
21
- ```
15
+ A parent component could contain several skeleton-aware components. In this case, the parent would extend [`SkeletonGroupMixin`](#skeleton-groups), which would automatically handle the skeleton state of its contents.
22
16
 
23
17
  ## Skeletizing Custom Elements with SkeletonMixin
24
18
 
@@ -133,6 +127,23 @@ For example:
133
127
 
134
128
  When skeletized, this heading will take up `45%` of the available width.
135
129
 
130
+ ## Skeleton groups
131
+ Skeleton groups can be used to ensure a collection of components all appear at the same time. This can be used to prevent individual components from popping in before everything has loaded.
132
+
133
+ ```js
134
+ import { SkeletonGroupMixin } from '@brightspace-ui/core/skeleton/skeleton-group-mixin.js';
135
+
136
+ class MyElement extends SkeletonGroupMixin(LitElement) {
137
+
138
+ render() {
139
+ return html`
140
+ // Anything that can be skeletonized.
141
+ // All components will remain in skeleton state until they have all loaded
142
+ `;
143
+ }
144
+ }
145
+ ```
146
+
136
147
  ## Future Enhancements
137
148
 
138
149
  Looking for an enhancement not listed here? Is there a core component that should support skeletons but doesn't yet? Create a GitHub issue!
@@ -0,0 +1,71 @@
1
+ import '../../switch/switch.js';
2
+ import './skeleton-group-test-wrapper.js';
3
+ import './skeleton-test-box.js';
4
+ import './skeleton-test-container.js';
5
+ import './skeleton-test-heading.js';
6
+
7
+ import { css, html, LitElement } from 'lit';
8
+ import { SkeletonGroupMixin } from '../skeleton-group-mixin.js';
9
+
10
+ class SkeletonTestNestedGroup extends SkeletonGroupMixin(LitElement) {
11
+ static get properties() {
12
+ return {
13
+ _skeletonParent: { state: true },
14
+ _skeletonContainer: { state: true },
15
+ _skeletonHeading: { state: true },
16
+ };
17
+ }
18
+ static get styles() {
19
+ return css`
20
+ .controls {
21
+ align-items: center;
22
+ display: flex;
23
+ gap: 0.6rem;
24
+ margin-bottom: 0.6rem;
25
+ }
26
+ `;
27
+ }
28
+
29
+ constructor() {
30
+ super();
31
+ this._skeletonParent = false;
32
+ this._skeletonContainer = false;
33
+ this._skeletonHeading = false;
34
+ }
35
+
36
+ render() {
37
+ return html`
38
+ <div class="controls">
39
+ <d2l-switch @click="${this._loadGroup}" ?on="${this._skeletonSetExplicitly}" text="parent skeleton"></d2l-switch>
40
+ <d2l-switch @click="${this._loadList}" ?on="${this._skeletonContainer}" text="container skeleton"></d2l-switch>
41
+ <d2l-switch @click="${this._loadInput}" ?on="${this._skeletonHeading}" text="heading skeleton"></d2l-switch>
42
+ </div>
43
+ <d2l-skeleton-group-test-wrapper>
44
+ <d2l-test-skeleton-heading level="1">Heading 1</d2l-test-skeleton-heading>
45
+ <d2l-skeleton-group-test-wrapper ?skeleton="${this._skeletonContainer}">
46
+ <d2l-test-skeleton-heading level="3" ?skeleton="${this._skeletonHeading}">Inner heading</d2l-test-skeleton-heading>
47
+ <d2l-test-skeleton-box></d2l-test-skeleton-box>
48
+ </d2l-skeleton-group-test-wrapper>
49
+ <d2l-skeleton-group-test-wrapper>
50
+ <d2l-test-skeleton-heading level="3">Heading 3</d2l-test-skeleton-heading>
51
+ <d2l-test-skeleton-container></d2l-test-skeleton-container>
52
+ </d2l-skeleton-group-test-wrapper>
53
+ </d2l-skeleton-group-test-wrapper>
54
+ `;
55
+ }
56
+
57
+ _loadGroup() {
58
+ this._skeletonParent = !this._skeletonParent;
59
+ this.skeleton = this._skeletonParent;
60
+ }
61
+
62
+ _loadInput() {
63
+ this._skeletonHeading = !this._skeletonHeading;
64
+ }
65
+
66
+ _loadList() {
67
+ this._skeletonContainer = !this._skeletonContainer;
68
+ }
69
+ }
70
+ customElements.define('d2l-test-nested-skeleton-group', SkeletonTestNestedGroup);
71
+
@@ -0,0 +1,9 @@
1
+ import { html, LitElement } from 'lit';
2
+ import { SkeletonGroupMixin } from '../skeleton-group-mixin.js';
3
+
4
+ class SkeletonGroupTestWrapper extends SkeletonGroupMixin(LitElement) {
5
+ render() {
6
+ return html`<slot></slot>`;
7
+ }
8
+ }
9
+ customElements.define('d2l-skeleton-group-test-wrapper', SkeletonGroupTestWrapper);
@@ -0,0 +1,91 @@
1
+ import '../../switch/switch.js';
2
+ import '../../button/button-subtle.js';
3
+ import './skeleton-test-container.js';
4
+ import { css, html, LitElement } from 'lit';
5
+ import { SkeletonGroupMixin } from '../skeleton-group-mixin.js';
6
+
7
+ class SkeletonTestGroup extends LitElement {
8
+ static get properties() {
9
+ return {
10
+ _items: { state: true },
11
+ _loadAsGroup: { state: true },
12
+ };
13
+ }
14
+ static get styles() {
15
+ return css`
16
+ .controls {
17
+ align-items: center;
18
+ display: flex;
19
+ gap: 0.6rem;
20
+ justify-content: space-between;
21
+ margin-bottom: 0.6rem;
22
+ }
23
+ d2l-test-skeleton-container {
24
+ margin-bottom: 0.6rem;
25
+ }
26
+ `;
27
+ }
28
+
29
+ constructor() {
30
+ super();
31
+ this._items = [1, 2, 3];
32
+ this._loadAsGroup = true;
33
+ }
34
+
35
+ render() {
36
+ return html`
37
+ <div class="controls">
38
+ <div>
39
+ <d2l-button-subtle @click="${this._loadItems}" text="Load items" icon="tier1:download"></d2l-button-subtle>
40
+ <d2l-button-subtle @click="${this._addItem}" text="Add item" icon="tier1:add"></d2l-button-subtle>
41
+ <d2l-button-subtle @click="${this._removeItem}" text="Remove item" icon="tier1:delete"></d2l-button-subtle>
42
+ </div>
43
+ <d2l-switch @click="${this._toggleLoadType}" text="Wait for all elements to load" ?on="${this._loadAsGroup}"></d2l-switch>
44
+ </div>
45
+
46
+ ${this._loadAsGroup ? this._renderGroup() : this._renderIndividual() }
47
+ `;
48
+ }
49
+
50
+ _addItem() {
51
+ this._items.push((this._items.length + 1));
52
+ this.requestUpdate();
53
+ }
54
+
55
+ _loadItems() {
56
+ this._items.forEach(id => {
57
+ const item = this.shadowRoot.getElementById(id);
58
+ item.skeleton = true;
59
+ setTimeout(() => item.skeleton = false, Math.random() * 2000);
60
+ });
61
+ }
62
+
63
+ _removeItem() {
64
+ this._items.pop();
65
+ this.requestUpdate();
66
+ }
67
+
68
+ _renderContents() {
69
+ return html`
70
+ ${this._items.map(item => html`<d2l-test-skeleton-container skeleton id="${item}"></d2l-test-skeleton-container>`)}
71
+ `;
72
+ }
73
+
74
+ _renderGroup() {
75
+ return html`<d2l-test-skeleton-group-on>${this._renderContents()}</d2l-test-skeleton-group-on>`;
76
+ }
77
+
78
+ _renderIndividual() {
79
+ return html`<div class="panels">${this._renderContents()}</div>`;
80
+ }
81
+
82
+ _toggleLoadType() {
83
+ this._loadAsGroup = !this._loadAsGroup;
84
+ }
85
+ }
86
+ customElements.define('d2l-test-skeleton-group', SkeletonTestGroup);
87
+
88
+ class SkeletonTestGroupOn extends SkeletonGroupMixin(LitElement) {
89
+ render() { return html`<slot></slot>`; }
90
+ }
91
+ customElements.define('d2l-test-skeleton-group-on', SkeletonTestGroupOn);
@@ -11,6 +11,8 @@
11
11
  import './skeleton-test-heading.js';
12
12
  import './skeleton-test-link.js';
13
13
  import './skeleton-test-paragraph.js';
14
+ import './skeleton-group-nested-test.js';
15
+ import './skeleton-group-test.js';
14
16
  </script>
15
17
  </head>
16
18
  <body unresolved>
@@ -62,6 +64,15 @@
62
64
  <d2l-test-skeleton-container></d2l-test-skeleton-container>
63
65
  </d2l-demo-snippet>
64
66
 
67
+ <h2>Skeleton group</h2>
68
+ <d2l-demo-snippet>
69
+ <d2l-test-skeleton-group></d2l-test-skeleton-group>
70
+ </d2l-demo-snippet>
71
+
72
+ <h2>Nested skeleton groups</h2>
73
+ <d2l-demo-snippet>
74
+ <d2l-test-nested-skeleton-group></d2l-test-nested-skeleton-group>
75
+ </d2l-demo-snippet>
65
76
  </d2l-demo-page>
66
77
  </body>
67
78
  </html>
@@ -2,9 +2,9 @@ import '../../colors/colors.js';
2
2
  import '../../inputs/input-checkbox.js';
3
3
  import { css, html, LitElement } from 'lit';
4
4
  import { bodyCompactStyles } from '../../typography/styles.js';
5
- import { SkeletonMixin } from '../skeleton-mixin.js';
5
+ import { SkeletonGroupMixin } from '../skeleton-group-mixin.js';
6
6
 
7
- export class SkeletonTestContainer extends SkeletonMixin(LitElement) {
7
+ export class SkeletonTestContainer extends SkeletonGroupMixin(LitElement) {
8
8
 
9
9
  static get styles() {
10
10
  return [
@@ -36,7 +36,7 @@ export class SkeletonTestContainer extends SkeletonMixin(LitElement) {
36
36
  <div class="d2l-demo-box d2l-skeletize-container">
37
37
  <div class="d2l-skeletize">Container with Skeletons Inside</div>
38
38
  <span class="d2l-body-compact">No skeleton</span>
39
- <d2l-input-checkbox checked ?skeleton="${this.skeleton}">Skeleton</d2l-input-checkbox>
39
+ <d2l-input-checkbox checked>Skeleton</d2l-input-checkbox>
40
40
  </div>
41
41
  `;
42
42
  }
@@ -0,0 +1,49 @@
1
+ import { dedupeMixin } from '@open-wc/dedupe-mixin';
2
+ import { SkeletonMixin } from './skeleton-mixin.js';
3
+ import { SubscriberRegistryController } from '../../controllers/subscriber/subscriberControllers.js';
4
+
5
+ export const SkeletonGroupMixin = dedupeMixin(superclass => class extends SkeletonMixin(superclass) {
6
+
7
+ static get properties() {
8
+ return {
9
+ _anySubscribersWithSkeletonActive : { state: true },
10
+ };
11
+ }
12
+
13
+ constructor() {
14
+ super();
15
+ this._anySubscribersWithSkeletonActive = false;
16
+ this._skeletonSubscribers = new SubscriberRegistryController(this, 'skeleton', {
17
+ onSubscribe: this.onSubscriberChange.bind(this),
18
+ onUnsubscribe: this.onSubscriberChange.bind(this),
19
+ updateSubscribers: this._checkSubscribersSkeletonState.bind(this),
20
+ });
21
+ }
22
+
23
+ updated(changedProperties) {
24
+ super.updated(changedProperties);
25
+ if (changedProperties.has('skeleton')) {
26
+ this._skeletonSubscribers.updateSubscribers();
27
+ }
28
+ }
29
+
30
+ onSubscriberChange() {
31
+ this._skeletonSubscribers.updateSubscribers();
32
+ }
33
+
34
+ _checkSubscribersSkeletonState(subscribers) {
35
+ this._anySubscribersWithSkeletonActive = [...subscribers.values()].some(subscriber => (
36
+ subscriber._skeletonSetExplicitly || subscriber._anySubscribersWithSkeletonActive
37
+ ));
38
+
39
+ this.setSkeletonActive(this._skeletonSetExplicitly || this._anySubscribersWithSkeletonActive || this._skeletonSetByParent);
40
+
41
+ subscribers.forEach(subscriber => {
42
+ subscriber.setSkeletonActive(this._skeletonActive);
43
+ subscriber.setSkeletonSetByParent(this._skeletonActive && !subscriber._skeletonSetExplicitly);
44
+ });
45
+
46
+ this._parentSkeleton?.registry?.onSubscriberChange();
47
+ }
48
+
49
+ });
@@ -1,6 +1,7 @@
1
1
  import '../colors/colors.js';
2
2
  import { css } from 'lit';
3
3
  import { dedupeMixin } from '@open-wc/dedupe-mixin';
4
+ import { EventSubscriberController } from '../../controllers/subscriber/subscriberControllers.js';
4
5
  import { RtlMixin } from '../../mixins/rtl/rtl-mixin.js';
5
6
 
6
7
  // DE50056: starting in Safari 16, the pulsing animation causes FACE
@@ -152,10 +153,10 @@ export const SkeletonMixin = dedupeMixin(superclass => class extends RtlMixin(su
152
153
  static get properties() {
153
154
  return {
154
155
  /**
155
- * Renders the input as a [skeleton loader](https://github.com/BrightspaceUI/core/tree/main/components/skeleton)
156
+ * Render the component as a [skeleton loader](https://github.com/BrightspaceUI/core/tree/main/components/skeleton).
156
157
  * @type {boolean}
157
158
  */
158
- skeleton: { reflect: true, type: Boolean }
159
+ skeleton: { reflect: true, type: Boolean },
159
160
  };
160
161
  }
161
162
 
@@ -167,7 +168,53 @@ export const SkeletonMixin = dedupeMixin(superclass => class extends RtlMixin(su
167
168
 
168
169
  constructor() {
169
170
  super();
170
- this.skeleton = false;
171
+ this._skeletonSetByParent = false;
172
+ this._skeletonSetExplicitly = false;
173
+ this._skeletonActive = false;
174
+ this._skeletonWait = false;
175
+
176
+ this._parentSkeleton = new EventSubscriberController(this, 'skeleton', {
177
+ onSubscribe: this._onSubscribe.bind(this),
178
+ onUnsubscribe: this._onUnsubscribe.bind(this)
179
+ });
180
+ }
181
+
182
+ get skeleton() {
183
+ return this._skeletonActive;
184
+ }
185
+
186
+ set skeleton(val) {
187
+ const oldVal = this._skeletonSetExplicitly;
188
+ if (oldVal === val) return;
189
+ this._skeletonSetExplicitly = val;
190
+
191
+ // keep _skeletonActive aligned with _skeletonSetExplicitly. _skeletonActive may be modified separately by a parent SkeletonGroup
192
+ this._skeletonActive = val;
193
+
194
+ this.requestUpdate('skeleton', oldVal);
195
+ this._parentSkeleton?.registry?.onSubscriberChange();
196
+ }
197
+
198
+ setSkeletonActive(skeletonActive) {
199
+ const oldVal = this._skeletonActive;
200
+ if (skeletonActive !== oldVal) {
201
+ this._skeletonActive = skeletonActive;
202
+ this.requestUpdate('skeleton', oldVal);
203
+ }
204
+ }
205
+
206
+ setSkeletonSetByParent(skeletonSetByParent) {
207
+ this._skeletonSetByParent = skeletonSetByParent;
208
+ }
209
+
210
+ _onSubscribe() {
211
+ this._skeletonWait = true;
212
+ }
213
+
214
+ _onUnsubscribe() {
215
+ this._skeletonWait = false;
216
+ this._skeletonActive = this._skeletonSetExplicitly;
217
+ this.requestUpdate('skeleton', this._skeletonSetExplicitly);
171
218
  }
172
219
 
173
220
  });
@@ -115,6 +115,8 @@ export class SubscriberRegistryController extends BaseController {
115
115
  }
116
116
 
117
117
  _handleSubscribe(e) {
118
+ if (e.detail.subscriber === this._host) { return; }
119
+
118
120
  e.stopPropagation();
119
121
  e.detail.registry = this._host;
120
122
  e.detail.registryController = this;
@@ -137,7 +139,9 @@ export class EventSubscriberController extends BaseSubscriber {
137
139
  }
138
140
 
139
141
  hostConnected() {
140
- this._subscriptionComplete = this._keepTrying(() => this._subscribe(), 40, 400);
142
+ requestAnimationFrame(() => {
143
+ this._subscriptionComplete = this._keepTrying(() => this._subscribe(), 40, 400);
144
+ });
141
145
  }
142
146
 
143
147
  hostDisconnected() {