@brightspace-ui/core 3.277.0 → 3.278.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,10 +1,10 @@
1
+ import './backdrop-stale-overlay.js';
1
2
  import '../colors/colors.js';
2
3
  import '../loading-spinner/loading-spinner.js';
3
- import './backdrop-dirty-overlay.js';
4
- import { css, html, LitElement, nothing } from 'lit';
4
+ import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
5
+ import { freshness, FreshnessMixin } from '../../mixins/freshness/freshness-mixin.js';
5
6
  import { getComposedChildren, getComposedParent } from '../../helpers/dom.js';
6
7
  import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
7
-
8
8
  import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js';
9
9
  import { styleMap } from 'lit/directives/style-map.js';
10
10
 
@@ -12,7 +12,6 @@ const BACKDROP_DELAY_MS = 800;
12
12
  const FADE_DURATION_MS = 500;
13
13
  const SPINNER_DELAY_MS = FADE_DURATION_MS;
14
14
  const LOADING_ANNOUNCEMENT_DELAY = 1000;
15
-
16
15
  const LOADING_SPINNER_SIZE = 50;
17
16
 
18
17
  const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
@@ -20,56 +19,31 @@ const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
20
19
  /**
21
20
  * A component for displaying a semi-transparent backdrop and a loading spinner over the containing element
22
21
  */
23
- class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitElement)) {
22
+ class BackdropLoading extends PropertyRequiredMixin(FreshnessMixin(LocalizeCoreElement(LitElement))) {
24
23
 
25
24
  static properties = {
26
- /**
27
- * The state of data in the element being overlaid. Set to 'clean' when the data represents the user's latest selections, 'dirty' when the data does not represent the user's latest selections, and 'loading' if the data is being actively refreshed
28
- * @type {'clean'|'dirty'|'loading'}
29
- */
30
- dataState: {
31
- reflect: true,
32
- type: String
33
- },
34
25
  /**
35
26
  * Used to identify content that the backdrop should make inert
36
27
  * @type {string}
37
28
  */
38
29
  for: { type: String, required: true },
39
- /**
40
- * The text displayed on the dirty state overlay when the 'dirty' dataState is set.
41
- * @type {string}
42
- */
43
- dirtyText: {
44
- reflect: true,
45
- attribute: 'dirty-text',
46
- type: String
47
- },
48
- /**
49
- * The text displayed on the button of the dirty state overlay when the 'dirty' dataState is set.
50
- * @type {string}
51
- */
52
- dirtyButtonText: {
53
- reflect: true,
54
- attribute: 'dirty-button-text',
55
- type: String
56
- },
57
- _state: { type: String, reflect: true },
58
- _spinnerTop: { state: true },
59
- _ariaContent: { state: true }
30
+ _ariaContent: { state: true },
31
+ _loadingSpinnerTop: { state: true },
32
+ _staleOverlayTop: { state: true },
33
+ _state: { type: String, reflect: true }
60
34
  };
61
35
 
62
36
  static styles = [css`
63
- #visible {
37
+ .backdrop-container {
64
38
  display: none;
65
39
  }
66
40
 
67
41
  :host([_state="showing"]),
68
42
  :host([_state="shown"]),
69
43
  :host([_state="hiding"]),
70
- :host([_state="showing"]) #visible,
71
- :host([_state="shown"]) #visible,
72
- :host([_state="hiding"]) #visible {
44
+ :host([_state="showing"]) .backdrop-container,
45
+ :host([_state="shown"]) .backdrop-container,
46
+ :host([_state="hiding"]) .backdrop-container {
73
47
  display: flex;
74
48
  inset: 0;
75
49
  justify-content: center;
@@ -99,13 +73,13 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme
99
73
  opacity: 1;
100
74
  transition: opacity ${FADE_DURATION_MS}ms ease-in ${SPINNER_DELAY_MS}ms;
101
75
  }
102
- :host([_state="shown"][dataState="dirty"]) d2l-loading-spinner,
76
+ :host([_state="shown"][freshness="${unsafeCSS(freshness.stale)}"]) d2l-loading-spinner,
103
77
  :host([_state="hiding"]) d2l-loading-spinner {
104
78
  opacity: 0;
105
79
  transition: opacity ${FADE_DURATION_MS}ms ease-out;
106
80
  }
107
81
 
108
- d2l-backdrop-dirty-overlay {
82
+ d2l-backdrop-stale-overlay {
109
83
  background-color: var(--d2l-theme-backdrop-dialog-color);
110
84
  height: fit-content;
111
85
  justify-content: center;
@@ -114,12 +88,12 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme
114
88
  top: 0;
115
89
  z-index: 1000;
116
90
  }
117
- :host([_state="shown"]) d2l-backdrop-dirty-overlay {
91
+ :host([_state="shown"]) d2l-backdrop-stale-overlay {
118
92
  opacity: 1;
119
93
  transition: opacity ${FADE_DURATION_MS}ms ease-in;
120
94
  }
121
- :host([_state="shown"][dataState="loading"]) d2l-backdrop-dirty-overlay,
122
- :host([_state="hiding"]) d2l-backdrop-dirty-overlay {
95
+ :host([_state="shown"][freshness="${unsafeCSS(freshness.loading)}"]) d2l-backdrop-stale-overlay,
96
+ :host([_state="hiding"]) d2l-backdrop-stale-overlay {
123
97
  opacity: 0;
124
98
  transition: opacity ${FADE_DURATION_MS}ms ease-out;
125
99
  }
@@ -131,46 +105,46 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme
131
105
 
132
106
  constructor() {
133
107
  super();
134
- this.dataState = 'clean';
135
- this._state = 'hidden';
136
- this._spinnerTop = 0;
137
- this._dirtyDialogTop = 0;
138
108
  this._ariaContent = '';
109
+ this._loadingSpinnerTop = 0;
110
+ this._staleOverlayTop = 0;
111
+ this._state = 'hidden';
139
112
  }
140
113
 
141
114
  render() {
142
115
  const backdropVisible = this._state !== 'hidden';
143
116
 
117
+ const backdropContainer = backdropVisible ? html`<div class="backdrop-container">
118
+ <div class="backdrop" @transitionend="${this.#handleTransitionEnd}" @transitioncancel="${this.#handleTransitionEnd}"></div>
119
+ <d2l-loading-spinner style="${styleMap({ top: `${this._loadingSpinnerTop}px` })}" size="${LOADING_SPINNER_SIZE}"></d2l-loading-spinner>
120
+ </div>` : nothing;
121
+
122
+ const backdropStaleOverlay = backdropVisible ? html`<d2l-backdrop-stale-overlay
123
+ button-text="${this.freshnessStaleButtonText}"
124
+ text="${this.freshnessStaleText}"
125
+ ?inert="${this.freshness !== freshness.stale}"
126
+ style="${styleMap({ top: `${this._staleOverlayTop}px` })}">
127
+ </d2l-backdrop-stale-overlay>` : nothing;
128
+
144
129
  return html`
145
- ${backdropVisible ?
146
- html`<div id="visible">
147
- <div class="backdrop" @transitionend="${this.#handleTransitionEnd}" @transitioncancel="${this.#handleTransitionEnd}"></div>
148
- <d2l-loading-spinner style=${styleMap({ top: `${this._spinnerTop}px` })} size="${LOADING_SPINNER_SIZE}"></d2l-loading-spinner>
149
- </div>` : nothing
150
- }
151
- <div aria-live="polite" id="d2l-live-region">
152
- ${backdropVisible ?
153
- html`<d2l-backdrop-dirty-overlay
154
- style=${styleMap({ top: `${this._dirtyDialogTop}px` })}
155
- description="${this.dirtyText}"
156
- action="${this.dirtyButtonText}"
157
- ?inert=${this.dataState !== 'dirty'}
158
- ></d2l-backdrop-dirty-overlay>` : nothing }
159
- <d2l-offscreen>
160
- ${this._ariaContent}
161
- </d2l-offscreen>
130
+ ${backdropContainer}
131
+ <div aria-live="polite" class="backdrop-stale-container">
132
+ ${backdropStaleOverlay}
133
+ <d2l-offscreen>${this._ariaContent}</d2l-offscreen>
162
134
  </div>
163
135
  `;
164
136
  }
137
+
165
138
  updated(changedProperties) {
166
- if (changedProperties.get('_state') && changedProperties.get('_state') === 'hidden')
167
- {
168
- this.#centerLoadingSpinnerAndDialog();
139
+ super.updated(changedProperties);
140
+
141
+ if (changedProperties.get('_state') && changedProperties.get('_state') === 'hidden') {
142
+ this.#updatePosition();
169
143
  }
170
144
 
171
145
  if (changedProperties.has('_state')) {
172
146
  if (this._state === 'showing') {
173
- if (this.dataState === 'loading') {
147
+ if (this.freshness === freshness.loading) {
174
148
  setTimeout(() => {
175
149
  if (this._state === 'showing') this._state = 'shown';
176
150
  }, BACKDROP_DELAY_MS);
@@ -180,26 +154,27 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme
180
154
  }
181
155
  }
182
156
  }
157
+
183
158
  willUpdate(changedProperties) {
184
- if (changedProperties.has('dataState') && changedProperties.get('dataState') !== undefined) {
185
- this.#clearLiveArea();
159
+ super.willUpdate(changedProperties);
186
160
 
187
- const oldState = changedProperties.get('dataState');
188
- const newState = this.dataState;
161
+ if (changedProperties.has('freshness') && changedProperties.get('freshness') !== undefined) {
162
+ this.#clearLiveRegionContent();
189
163
 
190
- // Calculate announcements
191
- if (newState === 'loading') {
192
- this.#setLiveArea(this.localize('components.backdrop-loading.loadingAnnouncement'), { delay: LOADING_ANNOUNCEMENT_DELAY });
193
- } else if (oldState === 'loading' && newState === 'clean') {
194
- this.#setLiveArea(this.localize('components.backdrop-loading.loadingCompleteAnnouncement'));
164
+ const oldState = changedProperties.get('freshness');
165
+ const newState = this.freshness;
166
+
167
+ if (newState === freshness.loading) {
168
+ this.#setLiveRegionContent(this.localize('components.backdrop-loading.loadingAnnouncement'), { delay: LOADING_ANNOUNCEMENT_DELAY });
169
+ } else if (oldState === freshness.loading && newState === freshness.fresh) {
170
+ this.#setLiveRegionContent(this.localize('components.backdrop-loading.loadingCompleteAnnouncement'));
195
171
  }
196
172
 
197
- // Update backdrop
198
- if (oldState === 'clean') {
173
+ if (oldState === freshness.fresh) {
199
174
  this.#show();
200
- } else if (newState === 'clean') {
175
+ } else if (newState === freshness.fresh) {
201
176
  this.#fade();
202
- } else if (oldState === 'loading' && newState === 'dirty') {
177
+ } else if (oldState === freshness.loading && newState === freshness.stale) {
203
178
  setTimeout(() => {
204
179
  if (this._state === 'showing') this._state = 'shown';
205
180
  }, BACKDROP_DELAY_MS);
@@ -207,52 +182,19 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme
207
182
  }
208
183
  }
209
184
 
210
- async #centerLoadingSpinnerAndDialog() {
211
- if (this._state === 'hidden') { return; }
212
-
213
- const loadingSpinner = this.shadowRoot.querySelector('d2l-loading-spinner');
214
- if (!loadingSpinner) { return; }
215
-
216
- const boundingRect = this.shadowRoot.querySelector('#visible').getBoundingClientRect();
217
-
218
- // Calculate the centerpoint of the visible portion of the element
219
- const upperVisibleBound = Math.max(0, boundingRect.top);
220
- const lowerVisibleBound = Math.min(window.innerHeight, boundingRect.bottom);
221
- const visibleHeight = lowerVisibleBound - upperVisibleBound;
222
- const centeringOffset = (visibleHeight / 4);
223
-
224
- // Calculate if an offset is required to move to the top of the viewport before centering
225
- const topOffset = Math.max(0, -boundingRect.top); // measures the distance below the top of the viewport, which is negative if the element starts above the viewport
226
-
227
- // Adjust for the size of the spinner
228
- const spinnerSizeOffset = LOADING_SPINNER_SIZE / 2;
229
-
230
- this._spinnerTop = centeringOffset + topOffset - spinnerSizeOffset;
185
+ #announcementTimeoutId;
231
186
 
232
- // Adjust for the size of the dirty dialog
233
- const dirtyOverlay = this.shadowRoot.querySelector('d2l-backdrop-dirty-overlay');
234
- if (dirtyOverlay) {
235
- await this.shadowRoot.querySelector('d2l-empty-state-action-button')?.getUpdateComplete();
236
- const dirtyDialogSizeOffset = dirtyOverlay.getBoundingClientRect().height / 2;
237
-
238
- this._dirtyDialogTop = centeringOffset + topOffset - dirtyDialogSizeOffset;
239
- }
240
- }
241
-
242
- #clearLiveArea() {
187
+ #clearLiveRegionContent() {
243
188
  this._ariaContent = '';
244
189
 
245
- if (this.announcementTimeout) {
246
- clearTimeout(this.announcementTimeout);
247
- }
248
-
249
- this.announcementTimeout = null;
190
+ if (this.#announcementTimeoutId) clearTimeout(this.#announcementTimeoutId);
191
+ this.#announcementTimeoutId = null;
250
192
  }
251
193
 
252
194
  #fade() {
253
195
  let hideImmediately = reduceMotion || this._state === 'showing';
254
196
  if (this._state === 'shown') {
255
- const currentOpacity = getComputedStyle(this.shadowRoot.querySelector('#d2l-live-region')).opacity;
197
+ const currentOpacity = getComputedStyle(this.shadowRoot.querySelector('.backdrop-stale-container')).opacity;
256
198
  hideImmediately ||= (currentOpacity === '0');
257
199
  }
258
200
 
@@ -266,14 +208,10 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme
266
208
  #getBackdropTarget() {
267
209
  const parent = getComposedParent(this);
268
210
 
269
- const targetedChildren = getComposedChildren(
270
- parent,
271
- (elem) => elem.id === this.for,
272
- false
273
- );
274
-
275
- if (targetedChildren.length === 0) { throw new Error(`Backdrop cannot find sibling identified by 'for' property with value ${this.for}`);}
276
-
211
+ const targetedChildren = getComposedChildren(parent, (elem) => elem.id === this.for, false);
212
+ if (targetedChildren.length === 0) {
213
+ throw new Error(`Backdrop cannot find sibling identified by 'for' property with value ${this.for}`);
214
+ }
277
215
  return targetedChildren[0];
278
216
  }
279
217
 
@@ -287,24 +225,53 @@ class LoadingBackdrop extends PropertyRequiredMixin(LocalizeCoreElement(LitEleme
287
225
  this._state = 'hidden';
288
226
 
289
227
  const containingBlock = this.#getBackdropTarget();
290
-
291
228
  if (containingBlock.dataset.initiallyInert !== '1') containingBlock.removeAttribute('inert');
292
229
  }
293
230
 
294
- #setLiveArea(content, { delay } = {}) {
295
- this.announcementTimeout = setTimeout(() => this._ariaContent = content, delay || 0);
231
+ #setLiveRegionContent(content, { delay } = {}) {
232
+ this.#announcementTimeoutId = setTimeout(() => this._ariaContent = content, delay || 0);
296
233
  }
297
234
 
298
235
  #show() {
299
236
  this._state = reduceMotion ? 'shown' : 'showing';
300
237
 
301
238
  const containingBlock = this.#getBackdropTarget();
302
-
303
239
  if (containingBlock.getAttribute('inert') !== null) containingBlock.dataset.initiallyInert = '1';
304
-
305
240
  containingBlock.setAttribute('inert', 'inert');
306
241
  }
307
242
 
243
+ async #updatePosition() {
244
+ if (this._state === 'hidden') return;
245
+
246
+ const loadingSpinner = this.shadowRoot.querySelector('d2l-loading-spinner');
247
+ if (!loadingSpinner) return;
248
+
249
+ const backdropContainerRect = this.shadowRoot.querySelector('.backdrop-container').getBoundingClientRect();
250
+
251
+ // Calculate the centerpoint of the visible portion of the element
252
+ const upperVisibleBound = Math.max(0, backdropContainerRect.top);
253
+ const lowerVisibleBound = Math.min(window.innerHeight, backdropContainerRect.bottom);
254
+ const visibleHeight = lowerVisibleBound - upperVisibleBound;
255
+ const centeringOffset = (visibleHeight / 4);
256
+
257
+ // Calculate if an offset is required to move to the top of the viewport before centering
258
+ const topOffset = Math.max(0, -backdropContainerRect.top); // measures the distance below the top of the viewport, which is negative if the element starts above the viewport
259
+
260
+ // Adjust for the size of the spinner
261
+ const spinnerSizeOffset = LOADING_SPINNER_SIZE / 2;
262
+
263
+ this._loadingSpinnerTop = centeringOffset + topOffset - spinnerSizeOffset;
264
+
265
+ // Adjust for the size of the stale overlay
266
+ const staleOverlay = this.shadowRoot.querySelector('d2l-backdrop-stale-overlay');
267
+ if (staleOverlay) {
268
+ await this.shadowRoot.querySelector('d2l-empty-state-action-button')?.getUpdateComplete();
269
+ const staleOverlaySizeOffset = staleOverlay.getBoundingClientRect().height / 2;
270
+
271
+ this._staleOverlayTop = centeringOffset + topOffset - staleOverlaySizeOffset;
272
+ }
273
+ }
274
+
308
275
  }
309
276
 
310
- customElements.define('d2l-backdrop-loading', LoadingBackdrop);
277
+ customElements.define('d2l-backdrop-loading', BackdropLoading);
@@ -0,0 +1,58 @@
1
+ import '../button/button-subtle.js';
2
+ import { css, html, LitElement } from 'lit';
3
+ import { bodyCompactStyles } from '../typography/styles.js';
4
+ import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
5
+
6
+ /**
7
+ * A component to render as an overlay over another element with stale data.
8
+ */
9
+ class BackdropStaleOverlay extends LocalizeCoreElement(LitElement) {
10
+
11
+ static properties = {
12
+ /**
13
+ * The action button text
14
+ * @type {string}
15
+ */
16
+ buttonText: { type: String, attribute: 'button-text' },
17
+ /**
18
+ * The text displayed on the overlay
19
+ * @type {string}
20
+ */
21
+ text: { type: String }
22
+ };
23
+
24
+ static styles = [bodyCompactStyles, css`
25
+ :host {
26
+ align-items: center;
27
+ border: 1px solid var(--d2l-color-mica);
28
+ border-radius: 0.3rem;
29
+ column-gap: 0.75rem;
30
+ display: flex;
31
+ flex-wrap: wrap;
32
+ padding: 1.2rem 1.5rem;
33
+ }
34
+ `];
35
+
36
+ render() {
37
+ const message = this.text || this.localize('components.backdrop-stale-overlay.message');
38
+ const buttonText = this.buttonText || this.localize('intl-common:actions:reload');;
39
+
40
+ return html`
41
+ <p class="d2l-body-compact">${message}</p>
42
+ <d2l-button-subtle
43
+ @click="${this.#handleActionClick}"
44
+ text="${buttonText}">
45
+ </d2l-button-subtle>
46
+ `;
47
+ }
48
+
49
+ #handleActionClick(e) {
50
+ e.stopPropagation();
51
+
52
+ /** Dispatched when the action button on the overlay is clicked */
53
+ this.dispatchEvent(new CustomEvent('d2l-backdrop-stale-overlay-action', { bubbles: true, composed: true }));
54
+ }
55
+
56
+ }
57
+
58
+ customElements.define('d2l-backdrop-stale-overlay', BackdropStaleOverlay);
@@ -18,7 +18,7 @@ import '../../inputs/input-radio.js';
18
18
  import '../../inputs/input-radio-group.js';
19
19
 
20
20
  import { css, html, nothing } from 'lit';
21
- import { tableStyles, TableWrapper } from '../table-wrapper.js';
21
+ import { tableFreshness, tableStyles, TableWrapper } from '../table-wrapper.js';
22
22
  import { DemoPassthroughMixin } from '../../demo/demo-passthrough-mixin.js';
23
23
  import { ifDefined } from 'lit/directives/if-defined.js';
24
24
 
@@ -83,12 +83,12 @@ class TestTable extends DemoPassthroughMixin(TableWrapper, 'd2l-table-wrapper')
83
83
  this._data = data();
84
84
  this._sortField = undefined;
85
85
  this._sortDesc = false;
86
- this.dataState = 'clean';
86
+ this.freshness = tableFreshness.fresh;
87
87
  }
88
88
 
89
89
  render() {
90
90
  return html`
91
- <d2l-table-wrapper item-count="${ifDefined(this.paging ? 500 : undefined)}" dirty-text="This text indicates we're in the dirty state" dirty-button-text="Refresh">
91
+ <d2l-table-wrapper item-count="${ifDefined(this.paging ? 500 : undefined)}" freshness-stale-text="This table data is out of date." freshness-stale-button-text="Reload Me!">
92
92
  <d2l-table-controls slot="controls" ?no-sticky="${!this.stickyControls}" select-all-pages-allowed>
93
93
  <d2l-selection-action
94
94
  text="Sticky controls"
@@ -100,10 +100,10 @@ class TestTable extends DemoPassthroughMixin(TableWrapper, 'd2l-table-wrapper')
100
100
  icon="tier1:${this.stickyHeaders ? 'check' : 'close-default'}"
101
101
  @d2l-selection-action-click="${this._toggleStickyHeaders}">
102
102
  </d2l-selection-action>
103
- <d2l-input-radio-group label="Date State" horizontal label-hidden name="dataState" @change=${this._handleDataStateChange}>
104
- <d2l-input-radio label="Clean" value="clean" ?checked=${this.dataState === 'clean'}></d2l-input-radio>
105
- <d2l-input-radio label="Dirty" value="dirty" ?checked=${this.dataState === 'dirty'}></d2l-input-radio>
106
- <d2l-input-radio label="Loading" value="loading" ?checked=${this.dataState === 'loading'}></d2l-input-radio>
103
+ <d2l-input-radio-group label="Freshness" horizontal label-hidden name="freshness" @change=${this._handleFreshnessChange}>
104
+ <d2l-input-radio label="Fresh" value="${tableFreshness.fresh}" ?checked=${this.freshness === tableFreshness.fresh}></d2l-input-radio>
105
+ <d2l-input-radio label="Stale" value="${tableFreshness.stale}" ?checked=${this.freshness === tableFreshness.stale}></d2l-input-radio>
106
+ <d2l-input-radio label="Loading" value="${tableFreshness.loading}" ?checked=${this.freshness === tableFreshness.loading}></d2l-input-radio>
107
107
  </d2l-input-radio-group>
108
108
  </d2l-table-controls>
109
109
 
@@ -170,8 +170,8 @@ class TestTable extends DemoPassthroughMixin(TableWrapper, 'd2l-table-wrapper')
170
170
  `;
171
171
  }
172
172
 
173
- _handleDataStateChange(e) {
174
- this.dataState = e.detail.value;
173
+ _handleFreshnessChange(e) {
174
+ this.freshness = e.detail.value;
175
175
  }
176
176
 
177
177
  async _handlePagerLoadMore(e) {
@@ -2,13 +2,15 @@ import '../colors/colors.js';
2
2
  import '../scroll-wrapper/scroll-wrapper.js';
3
3
  import '../backdrop/backdrop-loading.js';
4
4
  import { css, html, LitElement, nothing } from 'lit';
5
+ import { freshness, FreshnessMixin } from '../../mixins/freshness/freshness-mixin.js';
5
6
  import { cssSizes } from '../inputs/input-checkbox-styles.js';
6
7
  import { getComposedParent } from '../../helpers/dom.js';
7
8
  import { isPopoverSupported } from '../popover/popover-mixin.js';
8
9
  import { PageableMixin } from '../paging/pageable-mixin.js';
9
- import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js';
10
10
  import { SelectionMixin } from '../selection/selection-mixin.js';
11
11
 
12
+ export const tableFreshness = freshness;
13
+
12
14
  export const tableStyles = css`
13
15
  .d2l-table {
14
16
  border-collapse: separate; /* needed to override reset stylesheets */
@@ -263,9 +265,24 @@ const SELECTORS = {
263
265
  * @slot controls - Slot for `d2l-table-controls` to be rendered above the table
264
266
  * @slot pager - Slot for `d2l-pager-load-more` to be rendered below the table
265
267
  */
266
- export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionMixin(LitElement))) {
268
+ export class TableWrapper extends FreshnessMixin(PageableMixin(SelectionMixin(LitElement))) {
267
269
 
268
270
  static properties = {
271
+ /**
272
+ * Todo: remove once consumers are updated to freshness. Use freshness instead.
273
+ * @ignore
274
+ */
275
+ dataState: { reflect: true, type: String },
276
+ /**
277
+ * Todo: remove once consumers are updated to freshness-stale-text. Use freshness-stale-text instead.
278
+ * @ignore
279
+ */
280
+ dirtyText: { reflect: true, attribute: 'dirty-text', type: String },
281
+ /**
282
+ * Todo: remove once consumers are updated to freshness-stale-button-text. Use freshness-stale-button-text instead.
283
+ * @ignore
284
+ */
285
+ dirtyButtonText: { reflect: true, attribute: 'dirty-button-text', type: String },
269
286
  /**
270
287
  * Hides the column borders on "default" table type
271
288
  * @type {boolean}
@@ -307,40 +324,6 @@ export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionM
307
324
  attribute: '_no-scroll-width',
308
325
  reflect: true,
309
326
  type: Boolean,
310
- },
311
- /**
312
- * The state of data in the table. Set to 'clean' when the data represents the user's latest selections, 'dirty' when the data does not represent the user's latest selections, and 'loading' if the data is being actively refreshed
313
- * @type {'clean'|'dirty'|'loading'}
314
- */
315
- dataState: {
316
- reflect: true,
317
- type: String
318
- },
319
- /**
320
- * The text displayed on the dirty state overlay when the 'dirty' dataState is set.
321
- * @type {string}
322
- */
323
- dirtyText: {
324
- reflect: true,
325
- attribute: 'dirty-text',
326
- required: {
327
- dependentProps: ['dataState'],
328
- validator: (_value, elem, hasValue) => hasValue || elem.dataState !== 'dirty'
329
- },
330
- type: String
331
- },
332
- /**
333
- * The text displayed on the button dirty state overlay when the 'dirty' dataState is set.
334
- * @type {string}
335
- */
336
- dirtyButtonText: {
337
- reflect: true,
338
- attribute: 'dirty-button-text',
339
- required: {
340
- dependentProps: ['dataState'],
341
- validator: (_value, elem, hasValue) => hasValue || elem.dataState !== 'dirty'
342
- },
343
- type: String
344
327
  }
345
328
  };
346
329
 
@@ -409,10 +392,6 @@ export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionM
409
392
  this._tableIntersectionObserver = null;
410
393
  this._tableMutationObserver = null;
411
394
  this._tableScrollers = {};
412
- this.dataState = 'clean';
413
-
414
- this.dirtyText = null;
415
- this.dirtyButtonText = null;
416
395
  }
417
396
 
418
397
  connectedCallback() {
@@ -444,7 +423,7 @@ export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionM
444
423
  const slot = html`
445
424
  <div style="position:relative">
446
425
  <slot id="table-slot" @slotchange="${this._handleSlotChange}"></slot>
447
- <d2l-backdrop-loading @d2l-backdrop-dirty-overlay-action=${this._handleDirtyButton} for="table-slot" dataState=${this.dataState} dirty-text="${this.dirtyText}" dirty-button-text="${this.dirtyButtonText}"></d2l-backdrop-loading>
426
+ <d2l-backdrop-loading @d2l-backdrop-stale-overlay-action="${this._handleStaleButton}" for="table-slot" freshness="${this.freshness}" freshness-stale-text="${this.freshnessStaleText}" freshness-stale-button-text="${this.freshnessStaleButtonText}"></d2l-backdrop-loading>
448
427
  </div>
449
428
  `;
450
429
  const useScrollWrapper = this.stickyHeadersScrollWrapper || !this.stickyHeaders;
@@ -469,6 +448,19 @@ export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionM
469
448
  }
470
449
  }
471
450
 
451
+ willUpdate(changedProperties) {
452
+ super.willUpdate(changedProperties);
453
+
454
+ // Todo: remove these once consumers are updated to freshness properties
455
+ if (this.dataState) {
456
+ if (this.dataState === 'clean') this.freshness = freshness.fresh;
457
+ else if (this.dataState === 'dirty') this.freshness = freshness.stale;
458
+ else if (this.dataState === 'loading') this.freshness = freshness.loading;
459
+ }
460
+ if (this.dirtyText) this.freshnessStaleText = this.dirtyText;
461
+ if (this.dirtyButtonText) this.freshnessStaleButtonText = this.dirtyButtonText;
462
+ }
463
+
472
464
  #hasIntersected = false;
473
465
 
474
466
  #noScrollWidthTimeout = null;
@@ -571,11 +563,6 @@ export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionM
571
563
  this._handleControlsChange();
572
564
  }
573
565
 
574
- _handleDirtyButton() {
575
- /** Dispatched when the action button on the dirty overlay is clicked */
576
- this.dispatchEvent(new CustomEvent('d2l-table-dirty-button-clicked'));
577
- }
578
-
579
566
  _handlePopoverClose(e) {
580
567
  this._updateStickyAncestor(e.target, false);
581
568
  }
@@ -630,6 +617,14 @@ export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionM
630
617
  this._handleTableChange();
631
618
  }
632
619
 
620
+ _handleStaleButton() {
621
+ /** Dispatched when the action button on the stale overlay is clicked */
622
+ this.dispatchEvent(new CustomEvent('d2l-table-stale-button-click'));
623
+
624
+ /** @ignore Todo: remove once consumers are updated to d2l-table-stale-button-click */
625
+ this.dispatchEvent(new CustomEvent('d2l-table-dirty-button-clicked'));
626
+ }
627
+
633
628
  async _handleTableChange(mutationRecords) {
634
629
  const updateList = [];
635
630