@brightspace-ui/core 2.137.4 → 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.
@@ -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() {