@brightspace-ui/core 2.112.8 → 2.114.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.
@@ -9,7 +9,9 @@ class TestScrollWrapper extends RtlMixin(LitElement) {
9
9
  return {
10
10
  hideActions: { attribute: 'hide-actions', type: Boolean },
11
11
  scroll: { attribute: 'scroll', type: Number },
12
- width: { type: Number }
12
+ splitScrollers: { attribute: 'split-scrollers', type: Boolean },
13
+ width: { type: Number },
14
+ _customScrollers: { state: true }
13
15
  };
14
16
  }
15
17
 
@@ -22,6 +24,16 @@ class TestScrollWrapper extends RtlMixin(LitElement) {
22
24
  background: linear-gradient(to right, #e66465, #9198e5);
23
25
  height: 100px;
24
26
  }
27
+ .d2l-scroll-wrapper-gradient-secondary {
28
+ background: linear-gradient(to left, #e66465, #9198e5);
29
+ height: 40px;
30
+ position: relative;
31
+ }
32
+ .d2l-scroll-wrapper-gradient-secondary button {
33
+ inset-inline-end: 0;
34
+ position: absolute;
35
+ top: 0;
36
+ }
25
37
  `;
26
38
  }
27
39
 
@@ -29,24 +41,46 @@ class TestScrollWrapper extends RtlMixin(LitElement) {
29
41
  super();
30
42
  this.hideActions = false;
31
43
  this.scroll = 0;
44
+ this.splitScrollers = false;
32
45
  this.width = 300;
46
+ this._customScrollers = {};
33
47
  }
34
48
 
35
49
  firstUpdated(changedProperties) {
36
50
  super.firstUpdated(changedProperties);
37
- if (this.scroll === 0) return;
38
- requestAnimationFrame(() => {
39
- this.shadowRoot.querySelector('d2l-scroll-wrapper').scrollDistance(this.scroll, false);
40
- });
51
+ if (this.scroll !== 0) {
52
+ requestAnimationFrame(() => this.shadowRoot.querySelector('d2l-scroll-wrapper').scrollDistance(this.scroll, false));
53
+ }
54
+ if (this.splitScrollers) {
55
+ this._customScrollers = { primary: this.shadowRoot.querySelector('.primary'), secondary: this.shadowRoot.querySelectorAll('.secondary') };
56
+ }
41
57
  }
42
58
 
43
59
  render() {
44
60
  const style = {
45
61
  width: `${this.width}px`
46
62
  };
47
- return html`
48
- <d2l-scroll-wrapper ?hide-actions="${this.hideActions}">
63
+
64
+ const secondaryScroller = html`
65
+ <div class="secondary">
66
+ <div class="d2l-scroll-wrapper-gradient-secondary" style="${styleMap(style)}">
67
+ Secondary scroller (No mouse scroll)
68
+ <button>Focus</button>
69
+ </div>
70
+ </div>
71
+ `;
72
+
73
+ const contents = this.splitScrollers ? html`
74
+ ${secondaryScroller}
75
+ <div class="primary">
49
76
  <div class="d2l-scroll-wrapper-gradient" style="${styleMap(style)}"></div>
77
+ </div>
78
+ ${secondaryScroller}
79
+ ` : html`<div class="d2l-scroll-wrapper-gradient" style="${styleMap(style)}"></div>`;
80
+
81
+ return html`
82
+ <d2l-scroll-wrapper ?hide-actions="${this.hideActions}" .customScrollers="${this._customScrollers}">
83
+ ${contents}
50
84
  </d2l-scroll-wrapper>
51
85
  `;
52
86
  }
@@ -31,6 +31,15 @@
31
31
  </template>
32
32
  </d2l-demo-snippet>
33
33
 
34
+ <h2>Split scrollers</h2>
35
+ <d2l-demo-snippet>
36
+ <template>
37
+ <div style="max-width: 700px;">
38
+ <d2l-test-scroll-wrapper width="1000" split-scrollers></d2l-test-scroll-wrapper>
39
+ </div>
40
+ </template>
41
+ </d2l-demo-snippet>
42
+
34
43
  </d2l-demo-page>
35
44
  </body>
36
45
  </html>
@@ -2,12 +2,37 @@ import '../colors/colors.js';
2
2
  import '../icons/icon.js';
3
3
  import { css, html, LitElement, unsafeCSS } from 'lit';
4
4
  import { getFocusPseudoClass } from '../../helpers/focus.js';
5
- import { ifDefined } from 'lit/directives/if-defined.js';
6
5
  import ResizeObserver from 'resize-observer-polyfill/dist/ResizeObserver.es.js';
7
6
  import { RtlMixin } from '../../mixins/rtl/rtl-mixin.js';
8
7
 
9
8
  const RTL_MULTIPLIER = navigator.userAgent.indexOf('Edge/') > 0 ? 1 : -1; /* legacy-Edge doesn't reverse scrolling in RTL */
10
9
  const SCROLL_AMOUNT = 0.8;
10
+ const PRINT_MEDIA_QUERY_LIST = matchMedia('print');
11
+
12
+ let focusStyleSheet;
13
+ function getFocusStyleSheet() {
14
+ if (!focusStyleSheet) {
15
+ focusStyleSheet = new CSSStyleSheet();
16
+ focusStyleSheet.replaceSync(css`
17
+ .d2l-scroll-wrapper-focus:${unsafeCSS(getFocusPseudoClass())} {
18
+ box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px var(--d2l-color-celestine), 0 2px 12px 0 rgba(0, 0, 0, 0.15);
19
+ outline: none;
20
+ }`);
21
+ }
22
+ return focusStyleSheet;
23
+ }
24
+
25
+ function getStyleSheetInsertionPoint(elem) {
26
+ if (elem.nodeType === Node.DOCUMENT_NODE || elem.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
27
+ if (elem.querySelector('.d2l-scroll-wrapper-focus') !== null) {
28
+ return elem;
29
+ }
30
+ }
31
+ if (elem.parentNode) {
32
+ return getStyleSheetInsertionPoint(elem.parentNode);
33
+ }
34
+ return null;
35
+ }
11
36
 
12
37
  /**
13
38
  *
@@ -18,6 +43,14 @@ class ScrollWrapper extends RtlMixin(LitElement) {
18
43
 
19
44
  static get properties() {
20
45
  return {
46
+ /**
47
+ * An object containing custom primary/secondary scroll containers
48
+ * @type {Object}
49
+ */
50
+ customScrollers: {
51
+ attribute: false,
52
+ type: Object
53
+ },
21
54
  /**
22
55
  * Whether to hide left/right scroll buttons
23
56
  * @type {boolean}
@@ -31,6 +64,7 @@ class ScrollWrapper extends RtlMixin(LitElement) {
31
64
  reflect: true,
32
65
  type: Boolean
33
66
  },
67
+ _printMode: { state: true },
34
68
  _scrollbarLeft: {
35
69
  attribute: 'scrollbar-left',
36
70
  reflect: true,
@@ -55,13 +89,8 @@ class ScrollWrapper extends RtlMixin(LitElement) {
55
89
  }
56
90
  .d2l-scroll-wrapper-container {
57
91
  box-sizing: border-box;
58
- outline: none;
59
- overflow-x: auto;
60
92
  overflow-y: var(--d2l-scroll-wrapper-overflow-y, visible);
61
93
  }
62
- .d2l-scroll-wrapper-container:${unsafeCSS(getFocusPseudoClass())} {
63
- box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px var(--d2l-color-celestine), 0 2px 12px 0 rgba(0, 0, 0, 0.15);
64
- }
65
94
  :host([h-scrollbar]) .d2l-scroll-wrapper-container {
66
95
  border-left: 1px dashed var(--d2l-color-mica);
67
96
  border-right: 1px dashed var(--d2l-color-mica);
@@ -121,47 +150,48 @@ class ScrollWrapper extends RtlMixin(LitElement) {
121
150
  :host([scrollbar-left]) .d2l-scroll-wrapper-button-left {
122
151
  display: none;
123
152
  }
124
-
125
- /* hide wrapper visuals from print view */
126
- @media print {
127
- .d2l-scroll-wrapper-actions {
128
- display: none;
129
- }
130
- .d2l-scroll-wrapper-container {
131
- overflow-x: visible;
132
- }
133
- :host([h-scrollbar]) .d2l-scroll-wrapper-container {
134
- border-left: none;
135
- border-right: none;
136
- }
137
- }
138
153
  `;
139
154
  }
140
155
 
141
156
  constructor() {
142
157
  super();
158
+ this.customScrollers = {};
143
159
  this.hideActions = false;
160
+ this._allScrollers = [];
161
+ this._baseContainer = null;
144
162
  this._container = null;
145
163
  this._hScrollbar = true;
146
- this._resizeObserver = null;
164
+ this._printMode = PRINT_MEDIA_QUERY_LIST.matches;
165
+ this._resizeObserver = new ResizeObserver(() => requestAnimationFrame(() => this.checkScrollbar()));
147
166
  this._scrollbarLeft = false;
148
167
  this._scrollbarRight = false;
168
+ this._syncDriver = null;
169
+ this._syncDriverTimeout = null;
170
+ this._checkScrollThresholds = this._checkScrollThresholds.bind(this);
171
+ this._handlePrintChange = this._handlePrintChange.bind(this);
172
+ this._synchronizeScroll = this._synchronizeScroll.bind(this);
173
+ }
174
+
175
+ connectedCallback() {
176
+ super.connectedCallback();
177
+ PRINT_MEDIA_QUERY_LIST?.addEventListener('change', this._handlePrintChange);
149
178
  }
150
179
 
151
180
  disconnectedCallback() {
152
181
  super.disconnectedCallback();
153
- if (this._resizeObserver) this._resizeObserver.disconnect();
182
+ this._disconnectAll();
183
+ PRINT_MEDIA_QUERY_LIST?.removeEventListener('change', this._handlePrintChange);
154
184
  }
155
185
 
156
186
  firstUpdated(changedProperties) {
157
187
  super.firstUpdated(changedProperties);
158
- this._container = this.shadowRoot.querySelector('.d2l-scroll-wrapper-container');
159
- this._resizeObserver = new ResizeObserver(() => requestAnimationFrame(() => this.checkScrollbar()));
160
- this._resizeObserver.observe(this._container);
188
+ this._updateScrollTargets();
161
189
  }
162
190
 
163
191
  render() {
164
- const tabindex = this._hScrollbar ? '0' : undefined;
192
+ // when printing, just get scroll-wrapper out of the way
193
+ if (this._printMode) return html`<slot></slot>`;
194
+
165
195
  const actions = !this.hideActions ? html`
166
196
  <div class="d2l-scroll-wrapper-actions">
167
197
  <div class="d2l-scroll-wrapper-button d2l-scroll-wrapper-button-left" @click="${this._scrollLeft}">
@@ -173,10 +203,17 @@ class ScrollWrapper extends RtlMixin(LitElement) {
173
203
  </div>` : null;
174
204
  return html`
175
205
  ${actions}
176
- <div class="d2l-scroll-wrapper-container" @scroll="${this._checkScrollThresholds}" tabindex="${ifDefined(tabindex)}"><slot></slot></div>
206
+ <div class="d2l-scroll-wrapper-container"><slot></slot></div>
177
207
  `;
178
208
  }
179
209
 
210
+ updated(changedProperties) {
211
+ super.updated(changedProperties);
212
+
213
+ if (changedProperties.has('customScrollers')) this._updateScrollTargets();
214
+ if (changedProperties.has('_hScrollbar')) this._updateTabIndex();
215
+ }
216
+
180
217
  checkScrollbar() {
181
218
  if (!this._container) return;
182
219
  this._hScrollbar = this._container.offsetWidth !== this._container.scrollWidth;
@@ -201,12 +238,39 @@ class ScrollWrapper extends RtlMixin(LitElement) {
201
238
 
202
239
  _checkScrollThresholds() {
203
240
  if (!this._container) return;
204
- const lowerScrollValue = this._container.scrollWidth - this._container.offsetWidth - Math.abs(this._container.scrollLeft);
241
+ const lowerScrollValue = this._container.scrollWidth - this._baseContainer.offsetWidth - Math.abs(this._container.scrollLeft);
205
242
  this._scrollbarLeft = (this._container.scrollLeft === 0);
206
243
  this._scrollbarRight = (lowerScrollValue <= 0);
207
244
 
208
245
  }
209
246
 
247
+ _disconnectAll() {
248
+ this._resizeObserver?.disconnect();
249
+
250
+ if (this._container) {
251
+ this._container.style.removeProperty('overflow-x');
252
+ this._container.classList.remove('d2l-scroll-wrapper-focus');
253
+ this._container.removeAttribute('tabindex');
254
+ this._container.removeEventListener('scroll', this._synchronizeScroll);
255
+ this._container.removeEventListener('scroll', this._checkScrollThresholds);
256
+ this._secondaryScrollers.forEach(element => {
257
+ element.style.removeProperty('overflow-x');
258
+ element.removeEventListener('scroll', this._synchronizeScroll);
259
+ });
260
+ }
261
+ }
262
+
263
+ async _handlePrintChange() {
264
+ if (!this._printMode) {
265
+ this._disconnectAll();
266
+ }
267
+ this._printMode = PRINT_MEDIA_QUERY_LIST.matches;
268
+ if (!this._printMode) {
269
+ await this.updateComplete;
270
+ this._updateScrollTargets();
271
+ }
272
+ }
273
+
210
274
  _scrollLeft() {
211
275
  if (!this._container) return;
212
276
  const scrollDistance = this._container.clientWidth * SCROLL_AMOUNT * -1;
@@ -219,6 +283,58 @@ class ScrollWrapper extends RtlMixin(LitElement) {
219
283
  this.scrollDistance(scrollDistance, true);
220
284
  }
221
285
 
286
+ _synchronizeScroll(e) {
287
+ if (this._syncDriver && e.target !== this._syncDriver) return;
288
+ if (this._syncDriverTimeout) clearTimeout(this._syncDriverTimeout);
289
+
290
+ this._syncDriver = e.target;
291
+ this._allScrollers.forEach(element => {
292
+ if (element && element !== e.target) element.scrollLeft = e.target.scrollLeft;
293
+ });
294
+ this._syncDriverTimeout = setTimeout(() => this._syncDriver = null, 100);
295
+ }
296
+
297
+ _updateScrollTargets() {
298
+ this._disconnectAll();
299
+
300
+ if (this._printMode) return;
301
+
302
+ this._baseContainer = this.shadowRoot.querySelector('.d2l-scroll-wrapper-container');
303
+ this._container = this.customScrollers?.primary || this._baseContainer;
304
+ this._secondaryScrollers = this.customScrollers?.secondary || [];
305
+ if (this._secondaryScrollers.length === undefined) this._secondaryScrollers = [ this._secondaryScrollers ];
306
+ this._allScrollers = [ this._container, ...this._secondaryScrollers ];
307
+
308
+ if (this._container) {
309
+ this._container.classList.add('d2l-scroll-wrapper-focus');
310
+ const styleRoot = getStyleSheetInsertionPoint(this._container);
311
+ if (styleRoot && 'adoptedStyleSheets' in styleRoot) {
312
+ const sheet = getFocusStyleSheet();
313
+ if (styleRoot.adoptedStyleSheets.indexOf(sheet) === -1) {
314
+ styleRoot.adoptedStyleSheets = [...styleRoot.adoptedStyleSheets, sheet];
315
+ }
316
+ }
317
+ this._container.style.overflowX = 'auto';
318
+ this._resizeObserver.observe(this._container);
319
+ this._container.addEventListener('scroll', this._checkScrollThresholds);
320
+ this._updateTabIndex();
321
+ }
322
+
323
+ if (this._secondaryScrollers.length) {
324
+ this._secondaryScrollers.forEach(element => {
325
+ element.style.overflowX = 'hidden';
326
+ element.addEventListener('scroll', this._synchronizeScroll);
327
+ });
328
+ this._container.addEventListener('scroll', this._synchronizeScroll);
329
+ this._synchronizeScroll({ target: this._container });
330
+ }
331
+ }
332
+
333
+ _updateTabIndex() {
334
+ if (!this._container) return;
335
+ if (this._hScrollbar) this._container.tabIndex = 0;
336
+ else this._container.removeAttribute('tabindex');
337
+ }
222
338
  }
223
339
 
224
340
  customElements.define('d2l-scroll-wrapper', ScrollWrapper);
@@ -486,6 +486,7 @@ When using lists, use the list-specific `d2l-list-controls` instead, which exten
486
486
  | Property | Type | Description |
487
487
  |---|---|---|
488
488
  | `no-selection` | Boolean | Whether to render select-all and selection summary |
489
+ | `no-selection-text` | String | Text to display if no items are selected (overrides pageable counts) |
489
490
  | `no-sticky` | Boolean | Disables sticky positioning for the controls |
490
491
  | `select-all-pages-allowed` | Boolean | Whether all pages can be selected |
491
492
  <!-- docs: end hidden content -->
@@ -24,6 +24,11 @@ export class SelectionControls extends PageableSubscriberMixin(SelectionObserver
24
24
  * @type {boolean}
25
25
  */
26
26
  noSelection: { type: Boolean, attribute: 'no-selection' },
27
+ /**
28
+ * ADVANCED: Text to display if no items are selected (overrides pageable counts)
29
+ * @type {string}
30
+ */
31
+ noSelectionText: { type: String, attribute: 'no-selection-text' },
27
32
  /**
28
33
  * Disables sticky positioning for the controls
29
34
  * @type {boolean}
@@ -150,8 +155,11 @@ export class SelectionControls extends PageableSubscriberMixin(SelectionObserver
150
155
  if (changedProperties.has('noSticky')) {
151
156
  this._stickyObserverUpdate();
152
157
  }
153
- if (changedProperties.has('_pageableInfo')) {
154
- this._noSelectionText = this._getNoSelectionText();
158
+ }
159
+
160
+ willUpdate(changedProperties) {
161
+ if (changedProperties.has('noSelectionText') || changedProperties.has('_pageableInfo')) {
162
+ this._noSelectionText = this.noSelectionText || this._getNoSelectionText();
155
163
  }
156
164
  }
157
165
 
@@ -44,7 +44,7 @@ class Summary extends LocalizeCoreElement(SelectionObserverMixin(LitElement)) {
44
44
  }
45
45
 
46
46
  willUpdate(changedProperties) {
47
- if (changedProperties.has('_provider') || changedProperties.has('selectionInfo')) {
47
+ if (changedProperties.has('_provider') || changedProperties.has('selectionInfo') || changedProperties.has('noSelectionText')) {
48
48
  this._updateSelectSummary();
49
49
  }
50
50
  }
@@ -58,7 +58,7 @@
58
58
  <h2>Default, Sticky columns</h2>
59
59
  <d2l-demo-snippet overflow-hidden>
60
60
  <template>
61
- <div style="overflow: auto; width: 300px;">
61
+ <div style="overflow: auto; width: 400px;">
62
62
  <d2l-test-table type="default" sticky-headers sticky-controls></d2l-test-table>
63
63
  </div>
64
64
  </template>
@@ -67,12 +67,19 @@
67
67
  <h2>Default with Scroll-Wrapper (no sticky)</h2>
68
68
  <d2l-demo-snippet overflow-hidden>
69
69
  <template>
70
- <div style="height: 300px; overflow: auto;">
71
- <d2l-test-table type="default" sticky-controls style="width: 300px;"></d2l-test-table>
70
+ <div style="height: 400px; overflow: auto;">
71
+ <d2l-test-table type="default" sticky-controls style="width: 400px;"></d2l-test-table>
72
72
  </div>
73
73
  </template>
74
74
  </d2l-demo-snippet>
75
75
 
76
+ <h2>Scroll-wrapper + sticky</h2>
77
+ <d2l-demo-snippet>
78
+ <template>
79
+ <d2l-test-table sticky-headers sticky-controls sticky-headers-scroll-wrapper style="width: 400px;"></d2l-test-table>
80
+ </template>
81
+ </d2l-demo-snippet>
82
+
76
83
  <div style="margin-bottom: 1000px;"></div>
77
84
  </d2l-demo-page>
78
85
  </body>
@@ -2,6 +2,7 @@ import '../colors/colors.js';
2
2
  import '../scroll-wrapper/scroll-wrapper.js';
3
3
  import { css, html, LitElement, nothing } from 'lit';
4
4
  import { PageableMixin } from '../paging/pageable-mixin.js';
5
+ import ResizeObserver from 'resize-observer-polyfill/dist/ResizeObserver.es.js';
5
6
  import { RtlMixin } from '../../mixins/rtl/rtl-mixin.js';
6
7
  import { SelectionMixin } from '../selection/selection-mixin.js';
7
8
 
@@ -114,10 +115,35 @@ export const tableStyles = css`
114
115
 
115
116
  /* sticky-headers */
116
117
 
118
+ /* all sticky cells */
119
+ d2l-table-wrapper[sticky-headers] .d2l-table > * > tr > .d2l-table-sticky-cell,
120
+ d2l-table-wrapper[sticky-headers] .d2l-table > * > tr > [sticky] {
121
+ position: -webkit-sticky;
122
+ position: sticky;
123
+ z-index: 1;
124
+ }
125
+ d2l-table-wrapper:not([dir="rtl"])[sticky-headers] .d2l-table > * > tr > .d2l-table-sticky-cell,
126
+ d2l-table-wrapper:not([dir="rtl"])[sticky-headers] .d2l-table > * > tr > [sticky] {
127
+ left: 0;
128
+ }
129
+ d2l-table-wrapper[dir="rtl"][sticky-headers] .d2l-table > * > tr > .d2l-table-sticky-cell,
130
+ d2l-table-wrapper[dir="rtl"][sticky-headers] .d2l-table > * > tr > [sticky] {
131
+ right: 0;
132
+ }
133
+
134
+ /* non-header sticky cells */
135
+ d2l-table-wrapper[sticky-headers] .d2l-table > * > tr:not([selected]) {
136
+ background-color: inherit; /* white background so sticky cells layer on top of non-sticky cells */
137
+ }
138
+ d2l-table-wrapper[sticky-headers] .d2l-table > tbody > tr:not([header]):not(.d2l-table-header) > .d2l-table-sticky-cell,
139
+ d2l-table-wrapper[sticky-headers] .d2l-table > tbody > tr:not([header]):not(.d2l-table-header) > [sticky] {
140
+ background-color: inherit;
141
+ }
142
+
117
143
  /* all header cells */
118
144
  d2l-table-wrapper[sticky-headers] .d2l-table > thead > tr > th,
119
- d2l-table-wrapper[sticky-headers] .d2l-table > * > tr.d2l-table-header > *,
120
- d2l-table-wrapper[sticky-headers] .d2l-table > * > tr[header] > * {
145
+ d2l-table-wrapper[sticky-headers]:not([sticky-headers-scroll-wrapper]) .d2l-table > * > tr.d2l-table-header > *,
146
+ d2l-table-wrapper[sticky-headers]:not([sticky-headers-scroll-wrapper]) .d2l-table > * > tr[header] > * {
121
147
  position: -webkit-sticky;
122
148
  position: sticky;
123
149
  z-index: 2;
@@ -126,17 +152,12 @@ export const tableStyles = css`
126
152
  /* header cells that are also sticky */
127
153
  d2l-table-wrapper[sticky-headers] .d2l-table > thead > tr > th.d2l-table-sticky-cell,
128
154
  d2l-table-wrapper[sticky-headers] .d2l-table > thead > tr > th[sticky],
129
- d2l-table-wrapper[sticky-headers] .d2l-table > * > tr.d2l-table-header > .d2l-table-sticky-cell,
130
- d2l-table-wrapper[sticky-headers] .d2l-table > * > tr.d2l-table-header > [sticky],
131
- d2l-table-wrapper[sticky-headers] .d2l-table > * > tr[header] > .d2l-table-sticky-cell,
132
- d2l-table-wrapper[sticky-headers] .d2l-table > * > tr[header] > [sticky] {
133
- left: 0;
155
+ d2l-table-wrapper[sticky-headers]:not([sticky-headers-scroll-wrapper]) .d2l-table > * > tr.d2l-table-header > .d2l-table-sticky-cell,
156
+ d2l-table-wrapper[sticky-headers]:not([sticky-headers-scroll-wrapper]) .d2l-table > * > tr.d2l-table-header > [sticky],
157
+ d2l-table-wrapper[sticky-headers]:not([sticky-headers-scroll-wrapper]) .d2l-table > * > tr[header] > .d2l-table-sticky-cell,
158
+ d2l-table-wrapper[sticky-headers]:not([sticky-headers-scroll-wrapper]) .d2l-table > * > tr[header] > [sticky] {
134
159
  z-index: 3;
135
160
  }
136
- d2l-table-wrapper[dir="rtl"][sticky-headers] .d2l-table > * > tr > .d2l-table-sticky-cell,
137
- d2l-table-wrapper[dir="rtl"][sticky-headers] .d2l-table > * > tr > [sticky] {
138
- right: 0;
139
- }
140
161
 
141
162
  /* first column that's sticky: offset by size of border-radius so top/bottom border doesn't show through (default style only) */
142
163
  d2l-table-wrapper[sticky-headers][type="default"]:not([dir="rtl"]) .d2l-table > * > tr > .d2l-table-sticky-cell.d2l-table-cell-first,
@@ -148,17 +169,20 @@ export const tableStyles = css`
148
169
  right: var(--d2l-table-border-radius-sticky-offset, 0);
149
170
  }
150
171
 
151
- /* non-header sticky cells */
152
- d2l-table-wrapper[sticky-headers] .d2l-table > * > tr:not([selected]) {
153
- background-color: inherit; /* white background so sticky cells layer on top of non-sticky cells */
172
+ /* sticky + scroll-wrapper */
173
+ d2l-table-wrapper[sticky-headers][sticky-headers-scroll-wrapper] .d2l-table {
174
+ display: block;
154
175
  }
155
- d2l-table-wrapper[sticky-headers] .d2l-table > tbody > tr:not([header]):not(.d2l-table-header) > .d2l-table-sticky-cell,
156
- d2l-table-wrapper[sticky-headers] .d2l-table > tbody > tr:not([header]):not(.d2l-table-header) > [sticky] {
157
- background-color: inherit;
158
- left: 0;
159
- position: -webkit-sticky;
176
+
177
+ d2l-table-wrapper[sticky-headers][sticky-headers-scroll-wrapper] .d2l-table > thead {
178
+ display: block;
160
179
  position: sticky;
161
- z-index: 1;
180
+ top: calc(var(--d2l-table-sticky-top, 0px) + var(--d2l-table-border-radius-sticky-offset, 0px));
181
+ z-index: 2;
182
+ }
183
+
184
+ d2l-table-wrapper[sticky-headers][sticky-headers-scroll-wrapper] .d2l-table > tbody {
185
+ display: block;
162
186
  }
163
187
  `;
164
188
 
@@ -190,6 +214,15 @@ export class TableWrapper extends RtlMixin(PageableMixin(SelectionMixin(LitEleme
190
214
  reflect: true,
191
215
  type: Boolean
192
216
  },
217
+ /**
218
+ * When used in combo with `sticky-headers`, whether to additionally wrap the table in a scroll-wrapper. Requires sticky headers to be in a separate thead.
219
+ * @type {boolean}
220
+ */
221
+ stickyHeadersScrollWrapper: {
222
+ attribute: 'sticky-headers-scroll-wrapper',
223
+ reflect: true,
224
+ type: Boolean
225
+ },
193
226
  /**
194
227
  * Type of table style to apply. The "light" style has fewer borders and tighter padding.
195
228
  * @type {'default'|'light'}
@@ -257,6 +290,7 @@ export class TableWrapper extends RtlMixin(PageableMixin(SelectionMixin(LitEleme
257
290
  super();
258
291
  this.noColumnBorder = false;
259
292
  this.stickyHeaders = false;
293
+ this.stickyHeadersScrollWrapper = false;
260
294
  this.type = 'default';
261
295
 
262
296
  this._controls = null;
@@ -266,6 +300,7 @@ export class TableWrapper extends RtlMixin(PageableMixin(SelectionMixin(LitEleme
266
300
  this._table = null;
267
301
  this._tableIntersectionObserver = null;
268
302
  this._tableMutationObserver = null;
303
+ this._tableScrollers = {};
269
304
  }
270
305
 
271
306
  disconnectedCallback() {
@@ -275,14 +310,16 @@ export class TableWrapper extends RtlMixin(PageableMixin(SelectionMixin(LitEleme
275
310
  this._controlsScrolledMutationObserver?.disconnect();
276
311
  this._tableMutationObserver?.disconnect();
277
312
  this._tableIntersectionObserver?.disconnect();
313
+ this._tableResizeObserver?.disconnect();
278
314
  }
279
315
 
280
316
  render() {
281
317
  const slot = html`<slot @slotchange="${this._handleSlotChange}"></slot>`;
318
+ const useScrollWrapper = this.stickyHeadersScrollWrapper || !this.stickyHeaders;
282
319
  return html`
283
320
  <slot name="controls" @slotchange="${this._handleControlsSlotChange}"></slot>
284
321
  ${this.stickyHeaders && this._controlsScrolled ? html`<div class="d2l-sticky-headers-backdrop"></div>` : nothing}
285
- ${this.stickyHeaders ? slot : html`<d2l-scroll-wrapper>${slot}</d2l-scroll-wrapper>`}
322
+ ${useScrollWrapper ? html`<d2l-scroll-wrapper .customScrollers="${this._tableScrollers}">${slot}</d2l-scroll-wrapper>` : slot}
286
323
  ${this._renderPagerContainer()}
287
324
  `;
288
325
  }
@@ -388,6 +425,10 @@ export class TableWrapper extends RtlMixin(PageableMixin(SelectionMixin(LitEleme
388
425
  this._table = e.target.assignedNodes({ flatten: true }).find(
389
426
  node => (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'TABLE' && node.classList.contains('d2l-table'))
390
427
  );
428
+ this._tableScrollers = (this.stickyHeadersScrollWrapper && this.stickyHeaders) ? {
429
+ primary: this._table?.querySelector('tbody'),
430
+ secondary: this._table?.querySelector('thead'),
431
+ } : {};
391
432
 
392
433
  // observes mutations to <table>'s direct children and also
393
434
  // its subtree (rows or cells added/removed to any descendant)
@@ -397,6 +438,7 @@ export class TableWrapper extends RtlMixin(PageableMixin(SelectionMixin(LitEleme
397
438
  childList: true,
398
439
  subtree: true
399
440
  });
441
+ this._tableResizeObserver?.disconnect();
400
442
 
401
443
  if (!this._table) return;
402
444
 
@@ -418,6 +460,9 @@ export class TableWrapper extends RtlMixin(PageableMixin(SelectionMixin(LitEleme
418
460
  this._tableIntersectionObserver.observe(this._table);
419
461
  }
420
462
 
463
+ if (!this._tableResizeObserver) this._tableResizeObserver = new ResizeObserver(() => this._syncColumnWidths());
464
+ this._tableResizeObserver.observe(this._table);
465
+
421
466
  this._handleTableChange();
422
467
  }
423
468
 
@@ -426,6 +471,7 @@ export class TableWrapper extends RtlMixin(PageableMixin(SelectionMixin(LitEleme
426
471
 
427
472
  this._updateItemShowingCount();
428
473
  this._applyClassNames();
474
+ this._syncColumnWidths();
429
475
  this._updateStickyTops();
430
476
  }
431
477
 
@@ -438,12 +484,35 @@ export class TableWrapper extends RtlMixin(PageableMixin(SelectionMixin(LitEleme
438
484
  if (target) this[observerName].observe(target, options);
439
485
  }
440
486
 
487
+ _syncColumnWidths() {
488
+ if (!this._table || !this.stickyHeaders || !this.stickyHeadersScrollWrapper) return;
489
+
490
+ const head = this._table.querySelector('thead');
491
+ const body = this._table.querySelector('tbody');
492
+ if (!head || !body) return;
493
+
494
+ const firstRowHead = head.rows[0];
495
+ const firstRowBody = body.rows[0];
496
+ if (!firstRowHead || !firstRowBody || firstRowHead.cells.length !== firstRowBody.cells.length) return;
497
+
498
+ for (let i = 0; i < firstRowHead.cells.length; i++) {
499
+ const headCell = firstRowHead.cells[i];
500
+ const bodyCell = firstRowBody.cells[i];
501
+
502
+ if (headCell.clientWidth > bodyCell.clientWidth) {
503
+ bodyCell.style.minWidth = getComputedStyle(headCell).width;
504
+ } else if (headCell.clientWidth < bodyCell.clientWidth) {
505
+ headCell.style.minWidth = getComputedStyle(bodyCell).width;
506
+ }
507
+ }
508
+ }
509
+
441
510
  _updateStickyTops() {
442
511
  const hasStickyControls = this._controls && !this._controls.noSticky;
443
512
  let rowTop = hasStickyControls ? this._controls.offsetHeight + 6 : 0; // +6 for the internal `margin-bottom`.
444
513
  this.style.setProperty('--d2l-table-sticky-top', `${rowTop}px`);
445
514
 
446
- if (!this._table || !this.stickyHeaders) return;
515
+ if (!this._table || !this.stickyHeaders || this.stickyHeadersScrollWrapper) return;
447
516
 
448
517
  const stickyRows = Array.from(this._table.querySelectorAll('tr.d2l-table-header, tr[header], thead tr'));
449
518
  stickyRows.forEach(r => {
@@ -2,6 +2,7 @@ import '../button/button-icon.js';
2
2
  import '../colors/colors.js';
3
3
  import '../tooltip/tooltip.js';
4
4
  import { css, html, nothing } from 'lit';
5
+ import { findComposedAncestor, isComposedAncestor } from '../../helpers/dom.js';
5
6
  import { heading4Styles, labelStyles } from '../typography/styles.js';
6
7
  import { announce } from '../../helpers/announce.js';
7
8
  import { classMap } from 'lit/directives/class-map.js';
@@ -30,7 +31,10 @@ export const TagListItemMixin = superclass => class extends LocalizeCoreElement(
30
31
  * @ignore
31
32
  */
32
33
  keyboardTooltipItem: { type: Boolean, attribute: 'keyboard-tooltip-item' },
33
- _displayKeyboardTooltip: { state: true }
34
+ /**
35
+ * @ignore
36
+ */
37
+ keyboardTooltipShown: { type: Boolean, attribute: 'keyboard-tooltip-shown' }
34
38
  };
35
39
  }
36
40
 
@@ -127,9 +131,8 @@ export const TagListItemMixin = superclass => class extends LocalizeCoreElement(
127
131
  this.clearable = false;
128
132
  /** @ignore */
129
133
  this.keyboardTooltipItem = false;
130
- this._displayKeyboardTooltip = false;
134
+ this.keyboardTooltipShown = false;
131
135
  this._id = getUniqueId();
132
- this._keyboardTooltipShown = false;
133
136
  }
134
137
 
135
138
  firstUpdated(changedProperties) {
@@ -140,17 +143,26 @@ export const TagListItemMixin = superclass => class extends LocalizeCoreElement(
140
143
  this.addEventListener('focus', async(e) => {
141
144
  // ignore focus events coming from inside the tag content
142
145
  if (e.composedPath()[0] !== this) return;
146
+ const tagList = findComposedAncestor(this, elem => elem.tagName === 'D2L-TAG-LIST');
147
+ if (this.keyboardTooltipItem && this.keyboardTooltipShown && !isComposedAncestor(tagList, e.relatedTarget)) {
148
+ const arrows = this.localize('components.tag-list-item.tooltip-arrow-keys');
149
+ const arrowsDescription = this.localize('components.tag-list-item.tooltip-arrow-keys-desc');
150
+
151
+ let message = `${arrows} - ${arrowsDescription}`;
152
+ if (this.clearable) {
153
+ const del = this.localize('components.tag-list-item.tooltip-delete-key');
154
+ const delDescription = this.localize('components.tag-list-item.tooltip-delete-key-desc');
155
+ message += `; ${del} - ${delDescription}`;
156
+ }
157
+
158
+ announce(message);
159
+ }
143
160
 
144
- this._displayKeyboardTooltip = (this.keyboardTooltipItem && !this._keyboardTooltipShown);
145
161
  await this.updateComplete;
146
162
 
147
163
  container.focus();
148
164
  });
149
165
 
150
- this.addEventListener('blur', () => {
151
- this._displayKeyboardTooltip = false;
152
- });
153
-
154
166
  this.addEventListener('keydown', this._handleKeydown);
155
167
  }
156
168
 
@@ -167,11 +179,10 @@ export const TagListItemMixin = superclass => class extends LocalizeCoreElement(
167
179
  }
168
180
 
169
181
  _handleKeyboardTooltipHide() {
170
- if (this._keyboardTooltipShown) this._displayKeyboardTooltip = false;
182
+ this.keyboardTooltipShown = true;
171
183
  }
172
184
 
173
185
  _handleKeyboardTooltipShow() {
174
- this._keyboardTooltipShown = true;
175
186
  /** @ignore */
176
187
  this.dispatchEvent(new CustomEvent(
177
188
  'd2l-tag-list-item-tooltip-show',
@@ -181,7 +192,7 @@ export const TagListItemMixin = superclass => class extends LocalizeCoreElement(
181
192
 
182
193
  _handleKeydown(e) {
183
194
  const openKeys = e.keyCode === keyCodes.SPACE || e.keyCode === keyCodes.ENTER;
184
- if (this._displayKeyboardTooltip && openKeys) this._displayKeyboardTooltip = false;
195
+ if (this.keyboardTooltipItem && !this.keyboardTooltipShown && openKeys) this.keyboardTooltipShown = true;
185
196
 
186
197
  const clearKeys = e.keyCode === keyCodes.BACKSPACE || e.keyCode === keyCodes.DELETE;
187
198
  if (!this.clearable || !clearKeys) return;
@@ -194,7 +205,9 @@ export const TagListItemMixin = superclass => class extends LocalizeCoreElement(
194
205
  <div class="d2l-tag-list-item-tooltip-title-key">${this.localize('components.tag-list-item.tooltip-title')}</div>
195
206
  <ul>
196
207
  <li><span class="d2l-tag-list-item-tooltip-title-key">${this.localize('components.tag-list-item.tooltip-arrow-keys')}</span> - ${this.localize('components.tag-list-item.tooltip-arrow-keys-desc')}</li>
197
- <li><span class="d2l-tag-list-item-tooltip-title-key">${this.localize('components.tag-list-item.tooltip-delete-key')}</span> - ${this.localize('components.tag-list-item.tooltip-delete-key-desc')}</li>
208
+ ${this.clearable ? html`
209
+ <li><span class="d2l-tag-list-item-tooltip-title-key">${this.localize('components.tag-list-item.tooltip-delete-key')}</span> - ${this.localize('components.tag-list-item.tooltip-delete-key-desc')}</li>
210
+ ` : nothing}
198
211
  </ul>
199
212
  `;
200
213
  }
@@ -209,7 +222,7 @@ export const TagListItemMixin = superclass => class extends LocalizeCoreElement(
209
222
  const hasDescription = !!options.description;
210
223
 
211
224
  let tooltip = nothing;
212
- if (this._displayKeyboardTooltip) {
225
+ if (this.keyboardTooltipItem && !this.keyboardTooltipShown) {
213
226
  tooltip = html`
214
227
  <d2l-tooltip
215
228
  align="start"
@@ -355,7 +355,7 @@ class TagList extends LocalizeCoreElement(InteractiveMixin(ArrowKeysMixin(LitEle
355
355
  }
356
356
 
357
357
  _handleKeyboardTooltipShown() {
358
- this._hasShownKeyboardTooltip = true;
358
+ if (!this._hasShownKeyboardTooltip) this._hasShownKeyboardTooltip = true;
359
359
  }
360
360
 
361
361
  async _handleResize() {
@@ -398,7 +398,8 @@ class TagList extends LocalizeCoreElement(InteractiveMixin(ArrowKeysMixin(LitEle
398
398
  this._chomp();
399
399
 
400
400
  this._contentReady = true;
401
- if (!this._hasShownKeyboardTooltip) this._items[0].setAttribute('keyboard-tooltip-item', 'keyboard-tooltip-item');
401
+ this._items[0].setAttribute('keyboard-tooltip-item', true);
402
+ if (this._hasShownKeyboardTooltip) this._items[0].setAttribute('keyboard-tooltip-shown', true);
402
403
  }
403
404
 
404
405
  async _toggleHiddenTagVisibility(e) {
@@ -7992,6 +7992,11 @@
7992
7992
  "path": "./components/list/list-controls.js",
7993
7993
  "description": "Controls for list components containing select-all, etc.",
7994
7994
  "attributes": [
7995
+ {
7996
+ "name": "no-selection-text",
7997
+ "description": "ADVANCED: Text to display if no items are selected (overrides pageable counts)",
7998
+ "type": "string"
7999
+ },
7995
8000
  {
7996
8001
  "name": "no-selection",
7997
8002
  "description": "Whether to render select-all and selection summary",
@@ -8022,6 +8027,12 @@
8022
8027
  }
8023
8028
  ],
8024
8029
  "properties": [
8030
+ {
8031
+ "name": "noSelectionText",
8032
+ "attribute": "no-selection-text",
8033
+ "description": "ADVANCED: Text to display if no items are selected (overrides pageable counts)",
8034
+ "type": "string"
8035
+ },
8025
8036
  {
8026
8037
  "name": "noSelection",
8027
8038
  "attribute": "no-selection",
@@ -10157,6 +10168,11 @@
10157
10168
  "type": "number",
10158
10169
  "default": "0"
10159
10170
  },
10171
+ {
10172
+ "name": "split-scrollers",
10173
+ "type": "boolean",
10174
+ "default": "false"
10175
+ },
10160
10176
  {
10161
10177
  "name": "width",
10162
10178
  "type": "number",
@@ -10176,6 +10192,12 @@
10176
10192
  "type": "number",
10177
10193
  "default": "0"
10178
10194
  },
10195
+ {
10196
+ "name": "splitScrollers",
10197
+ "attribute": "split-scrollers",
10198
+ "type": "boolean",
10199
+ "default": "false"
10200
+ },
10179
10201
  {
10180
10202
  "name": "width",
10181
10203
  "attribute": "width",
@@ -10197,6 +10219,12 @@
10197
10219
  }
10198
10220
  ],
10199
10221
  "properties": [
10222
+ {
10223
+ "name": "customScrollers",
10224
+ "description": "An object containing custom primary/secondary scroll containers",
10225
+ "type": "Object",
10226
+ "default": "{}"
10227
+ },
10200
10228
  {
10201
10229
  "name": "hideActions",
10202
10230
  "attribute": "hide-actions",
@@ -10621,6 +10649,11 @@
10621
10649
  "path": "./components/selection/selection-controls.js",
10622
10650
  "description": "Controls for selection components (e.g. list, table-wrapper) containing select-all, etc.",
10623
10651
  "attributes": [
10652
+ {
10653
+ "name": "no-selection-text",
10654
+ "description": "ADVANCED: Text to display if no items are selected (overrides pageable counts)",
10655
+ "type": "string"
10656
+ },
10624
10657
  {
10625
10658
  "name": "no-selection",
10626
10659
  "description": "Whether to render select-all and selection summary",
@@ -10651,6 +10684,12 @@
10651
10684
  }
10652
10685
  ],
10653
10686
  "properties": [
10687
+ {
10688
+ "name": "noSelectionText",
10689
+ "attribute": "no-selection-text",
10690
+ "description": "ADVANCED: Text to display if no items are selected (overrides pageable counts)",
10691
+ "type": "string"
10692
+ },
10654
10693
  {
10655
10694
  "name": "noSelection",
10656
10695
  "attribute": "no-selection",
@@ -11503,6 +11542,12 @@
11503
11542
  "type": "boolean",
11504
11543
  "default": "false"
11505
11544
  },
11545
+ {
11546
+ "name": "sticky-headers-scroll-wrapper",
11547
+ "description": "When used in combo with `sticky-headers`, whether to additionally wrap the table in a scroll-wrapper. Requires sticky headers to be in a separate thead.",
11548
+ "type": "boolean",
11549
+ "default": "false"
11550
+ },
11506
11551
  {
11507
11552
  "name": "type",
11508
11553
  "description": "Type of table style to apply. The \"light\" style has fewer borders and tighter padding.",
@@ -11559,6 +11604,13 @@
11559
11604
  "type": "boolean",
11560
11605
  "default": "false"
11561
11606
  },
11607
+ {
11608
+ "name": "stickyHeadersScrollWrapper",
11609
+ "attribute": "sticky-headers-scroll-wrapper",
11610
+ "description": "When used in combo with `sticky-headers`, whether to additionally wrap the table in a scroll-wrapper. Requires sticky headers to be in a separate thead.",
11611
+ "type": "boolean",
11612
+ "default": "false"
11613
+ },
11562
11614
  {
11563
11615
  "name": "type",
11564
11616
  "attribute": "type",
@@ -11647,6 +11699,11 @@
11647
11699
  "path": "./components/table/table-controls.js",
11648
11700
  "description": "Controls for table components containing a selection summary and selection actions.",
11649
11701
  "attributes": [
11702
+ {
11703
+ "name": "no-selection-text",
11704
+ "description": "ADVANCED: Text to display if no items are selected (overrides pageable counts)",
11705
+ "type": "string"
11706
+ },
11650
11707
  {
11651
11708
  "name": "no-selection",
11652
11709
  "description": "Whether to render the selection summary",
@@ -11677,6 +11734,12 @@
11677
11734
  }
11678
11735
  ],
11679
11736
  "properties": [
11737
+ {
11738
+ "name": "noSelectionText",
11739
+ "attribute": "no-selection-text",
11740
+ "description": "ADVANCED: Text to display if no items are selected (overrides pageable counts)",
11741
+ "type": "string"
11742
+ },
11680
11743
  {
11681
11744
  "name": "noSelection",
11682
11745
  "attribute": "no-selection",
@@ -11747,6 +11810,12 @@
11747
11810
  "type": "boolean",
11748
11811
  "default": "false"
11749
11812
  },
11813
+ {
11814
+ "name": "sticky-headers-scroll-wrapper",
11815
+ "description": "When used in combo with `sticky-headers`, whether to additionally wrap the table in a scroll-wrapper. Requires sticky headers to be in a separate thead.",
11816
+ "type": "boolean",
11817
+ "default": "false"
11818
+ },
11750
11819
  {
11751
11820
  "name": "type",
11752
11821
  "description": "Type of table style to apply. The \"light\" style has fewer borders and tighter padding.",
@@ -11785,6 +11854,13 @@
11785
11854
  "type": "boolean",
11786
11855
  "default": "false"
11787
11856
  },
11857
+ {
11858
+ "name": "stickyHeadersScrollWrapper",
11859
+ "attribute": "sticky-headers-scroll-wrapper",
11860
+ "description": "When used in combo with `sticky-headers`, whether to additionally wrap the table in a scroll-wrapper. Requires sticky headers to be in a separate thead.",
11861
+ "type": "boolean",
11862
+ "default": "false"
11863
+ },
11788
11864
  {
11789
11865
  "name": "type",
11790
11866
  "attribute": "type",
@@ -12056,6 +12132,11 @@
12056
12132
  "type": "boolean",
12057
12133
  "default": "false"
12058
12134
  },
12135
+ {
12136
+ "name": "keyboardTooltipShown",
12137
+ "type": "boolean",
12138
+ "default": "false"
12139
+ },
12059
12140
  {
12060
12141
  "name": "documentLocaleSettings",
12061
12142
  "default": "\"getDocumentLocaleSettings()\""
@@ -12161,6 +12242,11 @@
12161
12242
  "type": "boolean",
12162
12243
  "default": "false"
12163
12244
  },
12245
+ {
12246
+ "name": "keyboardTooltipShown",
12247
+ "type": "boolean",
12248
+ "default": "false"
12249
+ },
12164
12250
  {
12165
12251
  "name": "documentLocaleSettings",
12166
12252
  "default": "\"getDocumentLocaleSettings()\""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "2.112.8",
3
+ "version": "2.114.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",