@brightspace-ui/core 2.113.0 → 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);
@@ -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) {
@@ -10168,6 +10168,11 @@
10168
10168
  "type": "number",
10169
10169
  "default": "0"
10170
10170
  },
10171
+ {
10172
+ "name": "split-scrollers",
10173
+ "type": "boolean",
10174
+ "default": "false"
10175
+ },
10171
10176
  {
10172
10177
  "name": "width",
10173
10178
  "type": "number",
@@ -10187,6 +10192,12 @@
10187
10192
  "type": "number",
10188
10193
  "default": "0"
10189
10194
  },
10195
+ {
10196
+ "name": "splitScrollers",
10197
+ "attribute": "split-scrollers",
10198
+ "type": "boolean",
10199
+ "default": "false"
10200
+ },
10190
10201
  {
10191
10202
  "name": "width",
10192
10203
  "attribute": "width",
@@ -10208,6 +10219,12 @@
10208
10219
  }
10209
10220
  ],
10210
10221
  "properties": [
10222
+ {
10223
+ "name": "customScrollers",
10224
+ "description": "An object containing custom primary/secondary scroll containers",
10225
+ "type": "Object",
10226
+ "default": "{}"
10227
+ },
10211
10228
  {
10212
10229
  "name": "hideActions",
10213
10230
  "attribute": "hide-actions",
@@ -11525,6 +11542,12 @@
11525
11542
  "type": "boolean",
11526
11543
  "default": "false"
11527
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
+ },
11528
11551
  {
11529
11552
  "name": "type",
11530
11553
  "description": "Type of table style to apply. The \"light\" style has fewer borders and tighter padding.",
@@ -11581,6 +11604,13 @@
11581
11604
  "type": "boolean",
11582
11605
  "default": "false"
11583
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
+ },
11584
11614
  {
11585
11615
  "name": "type",
11586
11616
  "attribute": "type",
@@ -11780,6 +11810,12 @@
11780
11810
  "type": "boolean",
11781
11811
  "default": "false"
11782
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
+ },
11783
11819
  {
11784
11820
  "name": "type",
11785
11821
  "description": "Type of table style to apply. The \"light\" style has fewer borders and tighter padding.",
@@ -11818,6 +11854,13 @@
11818
11854
  "type": "boolean",
11819
11855
  "default": "false"
11820
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
+ },
11821
11864
  {
11822
11865
  "name": "type",
11823
11866
  "attribute": "type",
@@ -12089,6 +12132,11 @@
12089
12132
  "type": "boolean",
12090
12133
  "default": "false"
12091
12134
  },
12135
+ {
12136
+ "name": "keyboardTooltipShown",
12137
+ "type": "boolean",
12138
+ "default": "false"
12139
+ },
12092
12140
  {
12093
12141
  "name": "documentLocaleSettings",
12094
12142
  "default": "\"getDocumentLocaleSettings()\""
@@ -12194,6 +12242,11 @@
12194
12242
  "type": "boolean",
12195
12243
  "default": "false"
12196
12244
  },
12245
+ {
12246
+ "name": "keyboardTooltipShown",
12247
+ "type": "boolean",
12248
+ "default": "false"
12249
+ },
12197
12250
  {
12198
12251
  "name": "documentLocaleSettings",
12199
12252
  "default": "\"getDocumentLocaleSettings()\""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "2.113.0",
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",